diff options
author | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-10 00:18:25 +0000 |
---|---|---|
committer | tpearson <tpearson@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2010-01-10 00:18:25 +0000 |
commit | f21e5792b5084f5d008bf46f6316030c6dfb31e5 (patch) | |
tree | d51583b36aa1672bac78d98a682cdc330df27e4d /src | |
download | basket-f21e5792b5084f5d008bf46f6316030c6dfb31e5.tar.gz basket-f21e5792b5084f5d008bf46f6316030c6dfb31e5.zip |
Add author-abandoned basket application
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/applications/basket@1072339 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'src')
127 files changed, 41401 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..a392f0e --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,106 @@ +# set the include path for X, qt and KDE +INCLUDES = $(all_includes) + +# let automoc handle all of the meta source files (moc) +METASOURCES = AUTO + +messages: rc.cpp + $(XGETTEXT) *.cpp -o $(podir)/basket.pot + +KDE_ICON = AUTO + +# these are the headers for your project + +noinst_HEADERS = basket.h basketproperties.h mainwindow.h basket_part.h \ + debugwindow.h linklabel.h variouswidgets.h xmlwork.h keyboard.h global.h settings.h \ + focusedwidgets.h popupmenu.h basketfactory.h exporterdialog.h tag.h kcolorcombo2.h \ + newbasketdialog.h tagsedit.h note.h notecontent.h notedrag.h noteedit.h notefactory.h \ + filter.h tools.h backgroundmanager.h regiongrabber.h softwareimporters.h \ + kicondialog.h kiconcanvas.h kgpgme.h crashhandler.h password.h bnpview.h \ + systemtray.h clickablelabel.h colorpicker.h basketlistview.h qeffects.h likeback.h \ + formatimporter.h aboutdata.h basketstatusbar.h basketdcopiface.h basket_options.h \ + likeback_private.h application.h archive.h htmlexporter.h backup.h + +######################################################################### +# COMMON FILES SECTION +######################################################################### + +lib_LTLIBRARIES = libbasketcommon.la + +AM_CPPFLAGS = $(GPGME_CFLAGS) + +libbasketcommon_la_LDFLAGS = -avoid-version -no-undefined $(all_libraries) + +libbasketcommon_la_LIBADD = $(LIB_ARTSKDE) -lkio $(LIB_KDEUI) $(LIB_KDECORE) \ + $(LIB_QT) $(LIBSOCKET) $(GPGME_LIBS) $(LIB_KPARTS) -lDCOP -lkdefx -lkdeui + +libbasketcommon_la_SOURCES = \ + archive.cpp bnpview.cpp settings.cpp basket.cpp basketproperties.cpp \ + linklabel.cpp variouswidgets.cpp xmlwork.cpp keyboard.cpp \ + global.cpp exporterdialog.cpp htmlexporter.cpp notefactory.cpp softwareimporters.cpp \ + focusedwidgets.cpp popupmenu.cpp basketfactory.cpp \ + tag.cpp qeffects.cpp kcolorcombo2.cpp newbasketdialog.cpp tagsedit.cpp \ + formatimporter.cpp note.cpp notecontent.cpp notedrag.cpp noteedit.cpp \ + filter.cpp tools.cpp backgroundmanager.cpp regiongrabber.cpp \ + kicondialogui.ui kicondialog.cpp kiconcanvas.cpp kgpgme.cpp likeback.cpp \ + crashhandler.cpp passwordlayout.ui password.cpp colorpicker.cpp \ + basketlistview.cpp debugwindow.cpp systemtray.cpp aboutdata.cpp \ + basketstatusbar.cpp clickablelabel.cpp basketdcopiface.skel backup.cpp + +######################################################################### +# APPLICATION SECTION +######################################################################### +# this is the program that gets installed. it's name is used for all +# of the other Makefile.am variables +bin_PROGRAMS = basket + +# the application source, library search path, and link libraries +basket_LDFLAGS = $(all_libraries) $(KDE_RPATH) $(LIB_KUTILS) +basket_LDADD = libbasketcommon.la + +# this is where the desktop file will go +shelldesktopdir = $(xdg_appsdir) +shelldesktop_DATA = basket.desktop + +# this is where the shell's XML-GUI resource file goes +rcdir = $(kde_datadir)/basket +rc_DATA = basketui.rc + +basket_SOURCES = main.cpp mainwindow.cpp application.cpp + +kde_icon_KDEICON = cr128-app-basket.png cr16-app-basket.png cr22-app-basket.png \ + cr32-app-basket.png cr48-app-basket.png cr64-app-basket.png hi16-app-basket_old.png \ + hi32-app-basket_old.png crsc-app-basket.svg \ + cr16-action-likeback_like.png cr16-action-likeback_dislike.png \ + cr16-action-likeback_bug.png cr16-action-likeback_feature.png + +################## +# KPART SECTION +################## + +kde_module_LTLIBRARIES = kcm_basket.la libbasketpart.la + +libbasketpart_la_SOURCES = basket_part.cpp +libbasketpart_la_LDFLAGS = $(all_libraries) $(KDE_RPATH) -module -avoid-version -no-undefined +libbasketpart_la_LIBADD = libbasketcommon.la -lkparts -lkdeui $(LIB_KDECORE) $(LIB_QT) -lDCOP + +kcm_basket_la_SOURCES = kcm_basket.cpp +kcm_basket_la_LDFLAGS = $(all_libraries) -module -avoid-version -no-undefined +kcm_basket_la_LIBADD = libbasketcommon.la $(LIB_KDECORE) $(LIB_QT) -lkdeui + +# this is where the desktop file will go +partdesktopdir = $(kde_servicesdir) +partdesktop_DATA = basket_part.desktop + +# this is where the part's XML-GUI resource file goes +partdir = $(kde_datadir)/basket +part_DATA = basket_part.rc + +kde_services_DATA = \ + basket_config_general.desktop \ + basket_config_baskets.desktop \ + basket_config_new_notes.desktop \ + basket_config_notes_appearance.desktop \ + basket_config_apps.desktop \ + basket_config_features.desktop \ + basket_config_notes.desktop diff --git a/src/aboutdata.cpp b/src/aboutdata.cpp new file mode 100644 index 0000000..fa70b6f --- /dev/null +++ b/src/aboutdata.cpp @@ -0,0 +1,59 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "aboutdata.h" +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +static const char description[] = I18N_NOOP( + "<p><b>Taking care of your ideas.</b></p>" + "<p>A note-taking application that makes it easy to record ideas as you think, and quickly find them later. " + "Organizing your notes has never been so easy.</p>"); + + // Or how to make order of disorganized toughts. + +AboutData::AboutData() + : KAboutData( "basket", I18N_NOOP("BasKet Note Pads"), + VERSION, description, KAboutData::License_GPL_V2, + "(c) 2003-2007, S\303\251bastien Lao\303\273t", 0, + "http://basket.kde.org/", + "[email protected]" ) +{ + addAuthor( "Kelvie Wong", + I18N_NOOP("Maintainer"), + "[email protected]" ); + + addAuthor( "S\303\251bastien Lao\303\273t", + I18N_NOOP("Original Author"), + "[email protected]" ); + + addAuthor( "Petri Damst\303\251n", + I18N_NOOP("Basket encryption, Kontact integration, KnowIt importer"), + "[email protected]" ); + + addAuthor( "Alex Gontmakher", + I18N_NOOP("Baskets auto lock, save-status icon, HTML copy/paste, basket name tooltip, drop to basket name"), + "[email protected]" ); + + addAuthor( "Marco Martin", + I18N_NOOP("Icon"), + "[email protected]" ); +} diff --git a/src/aboutdata.h b/src/aboutdata.h new file mode 100644 index 0000000..f8cfea4 --- /dev/null +++ b/src/aboutdata.h @@ -0,0 +1,35 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef ABOUTDATA_H +#define ABOUTDATA_H + +#include <kaboutdata.h> + +/** + @author Sébastien Laoût <[email protected]> +*/ + +class AboutData : public KAboutData +{ + public: + AboutData(); +}; + +#endif diff --git a/src/application.cpp b/src/application.cpp new file mode 100644 index 0000000..f15cdf4 --- /dev/null +++ b/src/application.cpp @@ -0,0 +1,61 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kcmdlineargs.h> +#include <qstring.h> +#include <qfile.h> +#include <qfileinfo.h> +#include <qtimer.h> + +#include "application.h" +#include "global.h" +#include "bnpview.h" + +Application::Application() + : KUniqueApplication() +{ +} + +Application::~Application() +{ +} + +int Application::newInstance() +{ + KUniqueApplication::newInstance(); + + // Open the basket archive or template file supplied as argument: + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + if (args && args->count() >= 1) { + QString fileName = QFile::decodeName(args->arg(args->count() - 1)); + if (QFile::exists(fileName)) { + QFileInfo fileInfo(fileName); + if (!fileInfo.isDir()) { // Do not mis-interpret data-folder param! + // Tags are not loaded until Global::bnpView::lateInit() is called. + // It is called 0ms after the application start. + BNPView::s_fileToOpen = fileName; + QTimer::singleShot( 100, Global::bnpView, SLOT(delayedOpenArchive()) ); +// Global::bnpView->openArchive(fileName); + args->clear(); + } + } + } + return 0; +} diff --git a/src/application.h b/src/application.h new file mode 100644 index 0000000..fd8d5ea --- /dev/null +++ b/src/application.h @@ -0,0 +1,38 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef APPLICATION_H +#define APPLICATION_H + +#include <kuniqueapplication.h> +#include <qstring.h> + +/** + * @author Sébastien Laoût <[email protected]> + */ +class Application : public KUniqueApplication +{ + public: + Application(); + ~Application(); + int newInstance(); +}; + +#endif // APPLICATION_H diff --git a/src/archive.cpp b/src/archive.cpp new file mode 100644 index 0000000..7365501 --- /dev/null +++ b/src/archive.cpp @@ -0,0 +1,637 @@ +/*************************************************************************** + * Copyright (C) 2006 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qstring.h> +#include <qstringlist.h> +#include <qvaluelist.h> +#include <qmap.h> +#include <qdir.h> +#include <ktar.h> +#include <qdom.h> +#include <kmessagebox.h> +#include <qpixmap.h> +#include <qpainter.h> +#include <kstandarddirs.h> +#include <kapplication.h> +#include <kiconloader.h> +#include <kprogress.h> +#include <kmainwindow.h> + +#include "archive.h" +#include "global.h" +#include "bnpview.h" +#include "basket.h" +#include "basketlistview.h" +#include "basketfactory.h" +#include "tag.h" +#include "xmlwork.h" +#include "tools.h" +#include "backgroundmanager.h" +#include "formatimporter.h" + +#include <iostream> + +void Archive::save(Basket *basket, bool withSubBaskets, const QString &destination) +{ + QDir dir; + + KProgressDialog dialog(0, 0, i18n("Save as Basket Archive"), i18n("Saving as basket archive. Please wait..."), /*Not modal, for password dialogs!*/false); + dialog.showCancelButton(false); + dialog.setAutoClose(true); + dialog.show(); + KProgress *progress = dialog.progressBar(); + progress->setTotalSteps(/*Preparation:*/1 + /*Finishing:*/1 + /*Basket:*/1 + /*SubBaskets:*/(withSubBaskets ? Global::bnpView->basketCount(Global::bnpView->listViewItemForBasket(basket)) : 0)); + progress->setValue(0); + + // Create the temporar folder: + QString tempFolder = Global::savesFolder() + "temp-archive/"; + dir.mkdir(tempFolder); + + // Create the temporar archive file: + QString tempDestination = tempFolder + "temp-archive.tar.gz"; + KTar tar(tempDestination, "application/x-gzip"); + tar.open(IO_WriteOnly); + tar.writeDir("baskets", "", ""); + + progress->advance(1); // Preparation finished + std::cout << "Preparation finished out of " << progress->totalSteps() << std::endl; + + // Copy the baskets data into the archive: + QStringList backgrounds; + saveBasketToArchive(basket, withSubBaskets, &tar, backgrounds, tempFolder, progress); + + // Create a Small baskets.xml Document: + QDomDocument document("basketTree"); + QDomElement root = document.createElement("basketTree"); + document.appendChild(root); + Global::bnpView->saveSubHierarchy(Global::bnpView->listViewItemForBasket(basket), document, root, withSubBaskets); + Basket::safelySaveToFile(tempFolder + "baskets.xml", "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + document.toString()); + tar.addLocalFile(tempFolder + "baskets.xml", "baskets/baskets.xml"); + dir.remove(tempFolder + "baskets.xml"); + + // Save a Small tags.xml Document: + QValueList<Tag*> tags; + listUsedTags(basket, withSubBaskets, tags); + Tag::saveTagsTo(tags, tempFolder + "tags.xml"); + tar.addLocalFile(tempFolder + "tags.xml", "tags.xml"); + dir.remove(tempFolder + "tags.xml"); + + // Save Tag Emblems (in case they are loaded on a computer that do not have those icons): + QString tempIconFile = tempFolder + "icon.png"; + for (Tag::List::iterator it = tags.begin(); it != tags.end(); ++it) { + State::List states = (*it)->states(); + for (State::List::iterator it2 = states.begin(); it2 != states.end(); ++it2) { + State *state = (*it2); + QPixmap icon = kapp->iconLoader()->loadIcon(state->emblem(), KIcon::Small, 16, KIcon::DefaultState, /*path_store=*/0L, /*canReturnNull=*/true); + if (!icon.isNull()) { + icon.save(tempIconFile, "PNG"); + QString iconFileName = state->emblem().replace('/', '_'); + tar.addLocalFile(tempIconFile, "tag-emblems/" + iconFileName); + } + } + } + dir.remove(tempIconFile); + + // Finish Tar.Gz Exportation: + tar.close(); + + // Computing the File Preview: + Basket *previewBasket = basket; // FIXME: Use the first non-empty basket! + QPixmap previewPixmap(previewBasket->visibleWidth(), previewBasket->visibleHeight()); + QPainter painter(&previewPixmap); + // Save old state, and make the look clean ("smile, you are filmed!"): + NoteSelection *selection = previewBasket->selectedNotes(); + previewBasket->unselectAll(); + Note *focusedNote = previewBasket->focusedNote(); + previewBasket->setFocusedNote(0); + previewBasket->doHoverEffects(0, Note::None); + // Take the screenshot: + previewBasket->drawContents(&painter, 0, 0, previewPixmap.width(), previewPixmap.height()); + // Go back to the old look: + previewBasket->selectSelection(selection); + previewBasket->setFocusedNote(focusedNote); + previewBasket->doHoverEffects(); + // End and save our splandid painting: + painter.end(); + QImage previewImage = previewPixmap.convertToImage(); + const int PREVIEW_SIZE = 256; + previewImage = previewImage.scale(PREVIEW_SIZE, PREVIEW_SIZE, QImage::ScaleMin); + previewImage.save(tempFolder + "preview.png", "PNG"); + + // Finaly Save to the Real Destination file: + QFile file(destination); + if (file.open(IO_WriteOnly)) { + ulong previewSize = QFile(tempFolder + "preview.png").size(); + ulong archiveSize = QFile(tempDestination).size(); + QTextStream stream(&file); + stream.setEncoding(QTextStream::Latin1); + stream << "BasKetNP:archive\n" + << "version:0.6.1\n" +// << "read-compatible:0.6.1\n" +// << "write-compatible:0.6.1\n" + << "preview*:" << previewSize << "\n"; + // Copy the Preview File: + const Q_ULONG BUFFER_SIZE = 1024; + char *buffer = new char[BUFFER_SIZE]; + Q_LONG sizeRead; + QFile previewFile(tempFolder + "preview.png"); + if (previewFile.open(IO_ReadOnly)) { + while ((sizeRead = previewFile.readBlock(buffer, BUFFER_SIZE)) > 0) + file.writeBlock(buffer, sizeRead); + } + stream << "archive*:" << archiveSize << "\n"; + // Copy the Archive File: + QFile archiveFile(tempDestination); + if (archiveFile.open(IO_ReadOnly)) { + while ((sizeRead = archiveFile.readBlock(buffer, BUFFER_SIZE)) > 0) + file.writeBlock(buffer, sizeRead); + } + // Clean Up: + delete buffer; + buffer = 0; + file.close(); + } + + progress->advance(1); // Finishing finished + std::cout << "Finishing finished" << std::endl; + + // Clean Up Everything: + dir.remove(tempFolder + "preview.png"); + dir.remove(tempDestination); + dir.rmdir(tempFolder); +} + +void Archive::saveBasketToArchive(Basket *basket, bool recursive, KTar *tar, QStringList &backgrounds, const QString &tempFolder, KProgress *progress) +{ + // Basket need to be loaded for tags exportation. + // We load it NOW so that the progress bar really reflect the state of the exportation: + if (!basket->isLoaded()) { + basket->load(); + } + + QDir dir; + // Save basket data: + tar->addLocalDirectory(basket->fullPath(), "baskets/" + basket->folderName()); + tar->addLocalFile(basket->fullPath() + ".basket", "baskets/" + basket->folderName() + ".basket"); // The hidden files were not added + // Save basket icon: + QString tempIconFile = tempFolder + "icon.png"; + if (!basket->icon().isEmpty() && basket->icon() != "basket") { + QPixmap icon = kapp->iconLoader()->loadIcon(basket->icon(), KIcon::Small, 16, KIcon::DefaultState, /*path_store=*/0L, /*canReturnNull=*/true); + if (!icon.isNull()) { + icon.save(tempIconFile, "PNG"); + QString iconFileName = basket->icon().replace('/', '_'); + tar->addLocalFile(tempIconFile, "basket-icons/" + iconFileName); + } + } + // Save basket backgorund image: + QString imageName = basket->backgroundImageName(); + if (!basket->backgroundImageName().isEmpty() && !backgrounds.contains(imageName)) { + QString backgroundPath = Global::backgroundManager->pathForImageName(imageName); + if (!backgroundPath.isEmpty()) { + // Save the background image: + tar->addLocalFile(backgroundPath, "backgrounds/" + imageName); + // Save the preview image: + QString previewPath = Global::backgroundManager->previewPathForImageName(imageName); + if (!previewPath.isEmpty()) + tar->addLocalFile(previewPath, "backgrounds/previews/" + imageName); + // Save the configuration file: + QString configPath = backgroundPath + ".config"; + if (dir.exists(configPath)) + tar->addLocalFile(configPath, "backgrounds/" + imageName + ".config"); + } + backgrounds.append(imageName); + } + + progress->advance(1); // Basket exportation finished + std::cout << basket->basketName() << " finished" << std::endl; + + // Recursively save child baskets: + BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); + if (recursive && item->firstChild()) { + for (BasketListViewItem *child = (BasketListViewItem*) item->firstChild(); child; child = (BasketListViewItem*) child->nextSibling()) { + saveBasketToArchive(child->basket(), recursive, tar, backgrounds, tempFolder, progress); + } + } +} + +void Archive::listUsedTags(Basket *basket, bool recursive, QValueList<Tag*> &list) +{ + basket->listUsedTags(list); + BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); + if (recursive && item->firstChild()) { + for (BasketListViewItem *child = (BasketListViewItem*) item->firstChild(); child; child = (BasketListViewItem*) child->nextSibling()) { + listUsedTags(child->basket(), recursive, list); + } + } +} + +void Archive::open(const QString &path) +{ + // Create the temporar folder: + QString tempFolder = Global::savesFolder() + "temp-archive/"; + QDir dir; + dir.mkdir(tempFolder); + const Q_ULONG BUFFER_SIZE = 1024; + + QFile file(path); + if (file.open(IO_ReadOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::Latin1); + QString line = stream.readLine(); + if (line != "BasKetNP:archive") { + KMessageBox::error(0, i18n("This file is not a basket archive."), i18n("Basket Archive Error")); + file.close(); + Tools::deleteRecursively(tempFolder); + return; + } + QString version; + QStringList readCompatibleVersions; + QStringList writeCompatibleVersions; + while (!stream.atEnd()) { + // Get Key/Value Pair From the Line to Read: + line = stream.readLine(); + int index = line.find(':'); + QString key; + QString value; + if (index >= 0) { + key = line.left(index); + value = line.right(line.length() - index - 1); + } else { + key = line; + value = ""; + } + if (key == "version") { + version = value; + } else if (key == "read-compatible") { + readCompatibleVersions = QStringList::split(value, ";"); + } else if (key == "write-compatible") { + writeCompatibleVersions = QStringList::split(value, ";"); + } else if (key == "preview*") { + bool ok; + ulong size = value.toULong(&ok); + if (!ok) { + KMessageBox::error(0, i18n("This file is corrupted. It can not be opened."), i18n("Basket Archive Error")); + file.close(); + Tools::deleteRecursively(tempFolder); + return; + } + // Get the preview file: +//FIXME: We do not need the preview for now +// QFile previewFile(tempFolder + "preview.png"); +// if (previewFile.open(IO_WriteOnly)) { + char *buffer = new char[BUFFER_SIZE]; + Q_LONG sizeRead; + while ((sizeRead = file.readBlock(buffer, QMIN(BUFFER_SIZE, size))) > 0) { +// previewFile.writeBlock(buffer, sizeRead); + size -= sizeRead; + } +// previewFile.close(); + delete buffer; +// } + } else if (key == "archive*") { + if (version != "0.6.1" && readCompatibleVersions.contains("0.6.1") && !writeCompatibleVersions.contains("0.6.1")) { + KMessageBox::information( + 0, + i18n("This file was created with a recent version of %1. " + "It can be opened but not every information will be available to you. " + "For instance, some notes may be missing because they are of a type only available in new versions. " + "When saving the file back, consider to save it to another file, to preserve the original one.") + .arg(kapp->aboutData()->programName()), + i18n("Basket Archive Error") + ); + } + if (version != "0.6.1" && !readCompatibleVersions.contains("0.6.1") && !writeCompatibleVersions.contains("0.6.1")) { + KMessageBox::error( + 0, + i18n("This file was created with a recent version of %1. Please upgrade to a newer version to be able to open that file.") + .arg(kapp->aboutData()->programName()), + i18n("Basket Archive Error") + ); + file.close(); + Tools::deleteRecursively(tempFolder); + return; + } + + bool ok; + ulong size = value.toULong(&ok); + if (!ok) { + KMessageBox::error(0, i18n("This file is corrupted. It can not be opened."), i18n("Basket Archive Error")); + file.close(); + Tools::deleteRecursively(tempFolder); + return; + } + + Global::mainWindow()->raise(); + + // Get the archive file: + QString tempArchive = tempFolder + "temp-archive.tar.gz"; + QFile archiveFile(tempArchive); + if (archiveFile.open(IO_WriteOnly)) { + char *buffer = new char[BUFFER_SIZE]; + Q_LONG sizeRead; + while ((sizeRead = file.readBlock(buffer, QMIN(BUFFER_SIZE, size))) > 0) { + archiveFile.writeBlock(buffer, sizeRead); + size -= sizeRead; + } + archiveFile.close(); + delete buffer; + + // Extract the Archive: + QString extractionFolder = tempFolder + "extraction/"; + QDir dir; + dir.mkdir(extractionFolder); + KTar tar(tempArchive, "application/x-gzip"); + tar.open(IO_ReadOnly); + tar.directory()->copyTo(extractionFolder); + tar.close(); + + // Import the Tags: + importTagEmblems(extractionFolder); // Import and rename tag emblems BEFORE loading them! + QMap<QString, QString> mergedStates = Tag::loadTags(extractionFolder + "tags.xml"); + QMap<QString, QString>::Iterator it; + if (mergedStates.count() > 0) { + Tag::saveTags(); + } + + // Import the Background Images: + importArchivedBackgroundImages(extractionFolder); + + // Import the Baskets: + renameBasketFolders(extractionFolder, mergedStates); + + } + } else if (key.endsWith("*")) { + // We do not know what it is, but we should read the embedded-file in order to discard it: + bool ok; + ulong size = value.toULong(&ok); + if (!ok) { + KMessageBox::error(0, i18n("This file is corrupted. It can not be opened."), i18n("Basket Archive Error")); + file.close(); + Tools::deleteRecursively(tempFolder); + return; + } + // Get the archive file: + char *buffer = new char[BUFFER_SIZE]; + Q_LONG sizeRead; + while ((sizeRead = file.readBlock(buffer, QMIN(BUFFER_SIZE, size))) > 0) { + size -= sizeRead; + } + delete buffer; + } else { + // We do not know what it is, and we do not care. + } + // Analyse the Value, if Understood: + } + file.close(); + } + Tools::deleteRecursively(tempFolder); +} + +/** + * When opening a basket archive that come from another computer, + * it can contains tags that use icons (emblems) that are not present on that computer. + * Fortunately, basket archives contains a copy of every used icons. + * This method check for every emblems and import the missing ones. + * It also modify the tags.xml copy for the emblems to point to the absolute path of the impported icons. + */ +void Archive::importTagEmblems(const QString &extractionFolder) +{ + QDomDocument *document = XMLWork::openFile("basketTags", extractionFolder + "tags.xml"); + if (document == 0) + return; + QDomElement docElem = document->documentElement(); + + QDir dir; + dir.mkdir(Global::savesFolder() + "tag-emblems/"); + FormatImporter copier; // Only used to copy files synchronously + + QDomNode node = docElem.firstChild(); + while (!node.isNull()) { + QDomElement element = node.toElement(); + if ( (!element.isNull()) && element.tagName() == "tag" ) { + QDomNode subNode = element.firstChild(); + while (!subNode.isNull()) { + QDomElement subElement = subNode.toElement(); + if ( (!subElement.isNull()) && subElement.tagName() == "state" ) { + QString emblemName = XMLWork::getElementText(subElement, "emblem"); + if (!emblemName.isEmpty()) { + QPixmap emblem = kapp->iconLoader()->loadIcon(emblemName, KIcon::NoGroup, 16, KIcon::DefaultState, 0L, /*canReturnNull=*/true); + // The icon does not exists on that computer, import it: + if (emblem.isNull()) { + // Of the emblem path was eg. "/home/seb/emblem.png", it was exported as "tag-emblems/_home_seb_emblem.png". + // So we need to copy that image to "~/.kde/share/apps/basket/tag-emblems/emblem.png": + int slashIndex = emblemName.findRev("/"); + QString emblemFileName = (slashIndex < 0 ? emblemName : emblemName.right(slashIndex - 2)); + QString source = extractionFolder + "tag-emblems/" + emblemName.replace('/', '_'); + QString destination = Global::savesFolder() + "tag-emblems/" + emblemFileName; + if (!dir.exists(destination)) + copier.copyFolder(source, destination); + // Replace the emblem path in the tags.xml copy: + QDomElement emblemElement = XMLWork::getElement(subElement, "emblem"); + subElement.removeChild(emblemElement); + XMLWork::addElement(*document, subElement, "emblem", destination); + } + } + } + subNode = subNode.nextSibling(); + } + } + node = node.nextSibling(); + } + Basket::safelySaveToFile(extractionFolder + "tags.xml", document->toString()); +} + +void Archive::importArchivedBackgroundImages(const QString &extractionFolder) +{ + FormatImporter copier; // Only used to copy files synchronously + QString destFolder = KGlobal::dirs()->saveLocation("data", "basket/backgrounds/"); + + QDir dir(extractionFolder + "backgrounds/", /*nameFilder=*/"*.png", /*sortSpec=*/QDir::Name | QDir::IgnoreCase, /*filterSpec=*/QDir::Files | QDir::NoSymLinks); + QStringList files = dir.entryList(); + for (QStringList::Iterator it = files.begin(); it != files.end(); ++it) { + QString image = *it; + if (!Global::backgroundManager->exists(image)) { + // Copy images: + QString imageSource = extractionFolder + "backgrounds/" + image; + QString imageDest = destFolder + image; + copier.copyFolder(imageSource, imageDest); + // Copy configuration file: + QString configSource = extractionFolder + "backgrounds/" + image + ".config"; + QString configDest = destFolder + image; + if (dir.exists(configSource)) + copier.copyFolder(configSource, configDest); + // Copy preview: + QString previewSource = extractionFolder + "backgrounds/previews/" + image; + QString previewDest = destFolder + "previews/" + image; + if (dir.exists(previewSource)) { + dir.mkdir(destFolder + "previews/"); // Make sure the folder exists! + copier.copyFolder(previewSource, previewDest); + } + // Append image to database: + Global::backgroundManager->addImage(imageDest); + } + } +} + +void Archive::renameBasketFolders(const QString &extractionFolder, QMap<QString, QString> &mergedStates) +{ + QDomDocument *doc = XMLWork::openFile("basketTree", extractionFolder + "baskets/baskets.xml"); + if (doc != 0) { + QMap<QString, QString> folderMap; + QDomElement docElem = doc->documentElement(); + QDomNode node = docElem.firstChild(); + renameBasketFolder(extractionFolder, node, folderMap, mergedStates); + loadExtractedBaskets(extractionFolder, node, folderMap, 0); + } +} + +void Archive::renameBasketFolder(const QString &extractionFolder, QDomNode &basketNode, QMap<QString, QString> &folderMap, QMap<QString, QString> &mergedStates) +{ + QDomNode n = basketNode; + while ( ! n.isNull() ) { + QDomElement element = n.toElement(); + if ( (!element.isNull()) && element.tagName() == "basket" ) { + QString folderName = element.attribute("folderName"); + if (!folderName.isEmpty()) { + // Find a folder name: + QString newFolderName = BasketFactory::newFolderName(); + folderMap[folderName] = newFolderName; + // Reserve the folder name: + QDir dir; + dir.mkdir(Global::basketsFolder() + newFolderName); + // Rename the merged tag ids: +// if (mergedStates.count() > 0) { + renameMergedStatesAndBasketIcon(extractionFolder + "baskets/" + folderName + ".basket", mergedStates, extractionFolder); +// } + // Child baskets: + QDomNode node = element.firstChild(); + renameBasketFolder(extractionFolder, node, folderMap, mergedStates); + } + } + n = n.nextSibling(); + } +} + +void Archive::renameMergedStatesAndBasketIcon(const QString &fullPath, QMap<QString, QString> &mergedStates, const QString &extractionFolder) +{ + QDomDocument *doc = XMLWork::openFile("basket", fullPath); + if (doc == 0) + return; + QDomElement docElem = doc->documentElement(); + QDomElement properties = XMLWork::getElement(docElem, "properties"); + importBasketIcon(properties, extractionFolder); + QDomElement notes = XMLWork::getElement(docElem, "notes"); + if (mergedStates.count() > 0) + renameMergedStates(notes, mergedStates); + Basket::safelySaveToFile(fullPath, /*"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + */doc->toString()); +} + +void Archive::importBasketIcon(QDomElement properties, const QString &extractionFolder) +{ + QString iconName = XMLWork::getElementText(properties, "icon"); + if (!iconName.isEmpty() && iconName != "basket") { + QPixmap icon = kapp->iconLoader()->loadIcon(iconName, KIcon::NoGroup, 16, KIcon::DefaultState, 0L, /*canReturnNull=*/true); + // The icon does not exists on that computer, import it: + if (icon.isNull()) { + QDir dir; + dir.mkdir(Global::savesFolder() + "basket-icons/"); + FormatImporter copier; // Only used to copy files synchronously + // Of the icon path was eg. "/home/seb/icon.png", it was exported as "basket-icons/_home_seb_icon.png". + // So we need to copy that image to "~/.kde/share/apps/basket/basket-icons/icon.png": + int slashIndex = iconName.findRev("/"); + QString iconFileName = (slashIndex < 0 ? iconName : iconName.right(slashIndex - 2)); + QString source = extractionFolder + "basket-icons/" + iconName.replace('/', '_'); + QString destination = Global::savesFolder() + "basket-icons/" + iconFileName; + if (!dir.exists(destination)) + copier.copyFolder(source, destination); + // Replace the emblem path in the tags.xml copy: + QDomElement iconElement = XMLWork::getElement(properties, "icon"); + properties.removeChild(iconElement); + QDomDocument document = properties.ownerDocument(); + XMLWork::addElement(document, properties, "icon", destination); + } + } +} + +void Archive::renameMergedStates(QDomNode notes, QMap<QString, QString> &mergedStates) +{ + QDomNode n = notes.firstChild(); + while ( ! n.isNull() ) { + QDomElement element = n.toElement(); + if (!element.isNull()) { + if (element.tagName() == "group" ) { + renameMergedStates(n, mergedStates); + } else if (element.tagName() == "note") { + QString tags = XMLWork::getElementText(element, "tags"); + if (!tags.isEmpty()) { + QStringList tagNames = QStringList::split(";", tags); + for (QStringList::Iterator it = tagNames.begin(); it != tagNames.end(); ++it) { + QString &tag = *it; + if (mergedStates.contains(tag)) { + tag = mergedStates[tag]; + } + } + QString newTags = tagNames.join(";"); + QDomElement tagsElement = XMLWork::getElement(element, "tags"); + element.removeChild(tagsElement); + QDomDocument document = element.ownerDocument(); + XMLWork::addElement(document, element, "tags", newTags); + } + } + } + n = n.nextSibling(); + } +} + +void Archive::loadExtractedBaskets(const QString &extractionFolder, QDomNode &basketNode, QMap<QString, QString> &folderMap, Basket *parent) +{ + bool basketSetAsCurrent = (parent != 0); + QDomNode n = basketNode; + while ( ! n.isNull() ) { + QDomElement element = n.toElement(); + if ( (!element.isNull()) && element.tagName() == "basket" ) { + QString folderName = element.attribute("folderName"); + if (!folderName.isEmpty()) { + // Move the basket folder to its destination, while renaming it uniquely: + QString newFolderName = folderMap[folderName]; + FormatImporter copier; + // The folder has been "reserved" by creating it. Avoid asking the user to override: + QDir dir; + dir.rmdir(Global::basketsFolder() + newFolderName); + copier.moveFolder(extractionFolder + "baskets/" + folderName, Global::basketsFolder() + newFolderName); + // Append and load the basket in the tree: + Basket *basket = Global::bnpView->loadBasket(newFolderName); + BasketListViewItem *basketItem = Global::bnpView->appendBasket(basket, (basket && parent ? Global::bnpView->listViewItemForBasket(parent) : 0)); + basketItem->setOpen(!XMLWork::trueOrFalse(element.attribute("folded", "false"), false)); + QDomElement properties = XMLWork::getElement(element, "properties"); + importBasketIcon(properties, extractionFolder); // Rename the icon fileName if necessary + basket->loadProperties(properties); + // Open the first basket of the archive: + if (!basketSetAsCurrent) { + Global::bnpView->setCurrentBasket(basket); + basketSetAsCurrent = true; + } + QDomNode node = element.firstChild(); + loadExtractedBaskets(extractionFolder, node, folderMap, basket); + } + } + n = n.nextSibling(); + } +} diff --git a/src/archive.h b/src/archive.h new file mode 100644 index 0000000..5389015 --- /dev/null +++ b/src/archive.h @@ -0,0 +1,59 @@ +/*************************************************************************** + * Copyright (C) 2006 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef ARCHIVE_H +#define ARCHIVE_H + +#include <qvaluelist.h> +#include <qmap.h> + +class Basket; +class Tag; + +class QString; +class QStringList; +class QDomNode; +class KTar; +class KProgress; + +/** + * @author Sébastien Laoût <[email protected]> + */ +class Archive +{ + public: + static void save(Basket *basket, bool withSubBaskets, const QString &destination); + static void open(const QString &path); + private: + // Convenient Methods for Saving: + static void saveBasketToArchive(Basket *basket, bool recursive, KTar *tar, QStringList &backgrounds, const QString &tempFolder, KProgress *progress); + static void listUsedTags(Basket *basket, bool recursive, QValueList<Tag*> &list); + // Convenient Methods for Loading: + static void renameBasketFolders(const QString &extractionFolder, QMap<QString, QString> &mergedStates); + static void renameBasketFolder(const QString &extractionFolder, QDomNode &basketNode, QMap<QString, QString> &folderMap, QMap<QString, QString> &mergedStates); + static void renameMergedStatesAndBasketIcon(const QString &fullPath, QMap<QString, QString> &mergedStates, const QString &extractionFolder); + static void renameMergedStates(QDomNode notes, QMap<QString, QString> &mergedStates); + static void importBasketIcon(QDomElement properties, const QString &extractionFolder); + static void loadExtractedBaskets(const QString &extractionFolder, QDomNode &basketNode, QMap<QString, QString> &folderMap, Basket *parent); + static void importTagEmblems(const QString &extractionFolder); + static void importArchivedBackgroundImages(const QString &extractionFolder); +}; + +#endif diff --git a/src/backgroundmanager.cpp b/src/backgroundmanager.cpp new file mode 100644 index 0000000..c289eca --- /dev/null +++ b/src/backgroundmanager.cpp @@ -0,0 +1,394 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kurl.h> +#include <kglobal.h> +#include <kstandarddirs.h> +#include <ksimpleconfig.h> +#include <qpainter.h> +#include <qdir.h> +#include <qimage.h> + +#include <iostream> + +#include "backgroundmanager.h" + +/** class BackgroundEntry: */ + +BackgroundEntry::BackgroundEntry(const QString &location) +{ + this->location = location; + name = KURL(location).fileName(); + tiled = false; + pixmap = 0; + preview = 0; + customersCount = 0; +} + +BackgroundEntry::~BackgroundEntry() +{ + delete pixmap; + delete preview; +} + +/** class OpaqueBackgroundEntry: */ + +OpaqueBackgroundEntry::OpaqueBackgroundEntry(const QString &name, const QColor &color) +{ + this->name = name; + this->color = color; + pixmap = 0; + customersCount = 0; +} + +OpaqueBackgroundEntry::~OpaqueBackgroundEntry() +{ + delete pixmap; +} + +/** class BackgroundManager: */ + +BackgroundManager::BackgroundManager() +{ +/// std::cout << "BackgroundManager: Found the following background images in "; + QStringList directories = KGlobal::dirs()->resourceDirs("data"); // eg. { "/home/seb/.kde/share/apps/", "/usr/share/apps/" } + // For each folder: + for (QStringList::Iterator it = directories.begin(); it != directories.end(); ++it) { + // For each file in those directories: + QDir dir(*it + "basket/backgrounds/", /*nameFilder=*/"*.png", /*sortSpec=*/QDir::Name | QDir::IgnoreCase, /*filterSpec=*/QDir::Files | QDir::NoSymLinks); +/// std::cout << *it + "basket/backgrounds/ "; + QStringList files = dir.entryList(); + for (QStringList::Iterator it2 = files.begin(); it2 != files.end(); ++it2) // TODO: If an image name is present in two folders? + addImage(*it + "basket/backgrounds/" + *it2); + } + +/// std::cout << ":" << std::endl; +/// for (BackgroundsList::Iterator it = m_backgroundsList.begin(); it != m_backgroundsList.end(); ++it) +/// std::cout << "* " << (*it)->location << " [ref: " << (*it)->name << "]" << std::endl; + + connect( &m_garbageTimer, SIGNAL(timeout()), this, SLOT(doGarbage()) ); +} + +BackgroundManager::~BackgroundManager() +{ +} + +void BackgroundManager::addImage(const QString &fullPath) +{ + m_backgroundsList.append(new BackgroundEntry(fullPath)); +} + +BackgroundEntry* BackgroundManager::backgroundEntryFor(const QString &image) +{ + for (BackgroundsList::Iterator it = m_backgroundsList.begin(); it != m_backgroundsList.end(); ++it) + if ((*it)->name == image) + return *it; + return 0; +} + +OpaqueBackgroundEntry* BackgroundManager::opaqueBackgroundEntryFor(const QString &image, const QColor &color) +{ + for (OpaqueBackgroundsList::Iterator it = m_opaqueBackgroundsList.begin(); it != m_opaqueBackgroundsList.end(); ++it) + if ((*it)->name == image && (*it)->color == color) + return *it; + return 0; +} + +bool BackgroundManager::subscribe(const QString &image) +{ + BackgroundEntry *entry = backgroundEntryFor(image); + if (entry) { + // If it's the first time something subscribe to this image: + if (!entry->pixmap) { + // Try to load the pixmap: + entry->pixmap = new QPixmap(entry->location); + // Try to figure out if it's a tiled background image or not (default to NO): + KSimpleConfig config(entry->location + ".config", /*readOnly=*/true); + config.setGroup("BasKet Background Image Configuration"); + entry->tiled = config.readBoolEntry("tiled", false); + } + // Return if the image loading has failed: + if (entry->pixmap->isNull()) { +/// std::cout << "BackgroundManager: Failed to load " << entry->location << std::endl; + return false; + } + // Success: effectively subscribe: + ++entry->customersCount; + return true; + } else { + // Don't exist: subscription failed: +/// std::cout << "BackgroundManager: Requested unexisting image: " << image << std::endl; + return false; + } +} + +bool BackgroundManager::subscribe(const QString &image, const QColor &color) +{ + BackgroundEntry *backgroundEntry = backgroundEntryFor(image); + + // First, if the image doesn't exist, isn't subscribed, or failed to load then we don't go further: + if (!backgroundEntry || !backgroundEntry->pixmap || backgroundEntry->pixmap->isNull()) { +/// std::cout << "BackgroundManager: Requested an unexisting or unsubscribed image: (" << image << "," << color.name() << ")..." << std::endl; + return false; + } + + OpaqueBackgroundEntry *opaqueBackgroundEntry = opaqueBackgroundEntryFor(image, color); + + // If this couple is requested for the first time or it haven't been subscribed for a long time enough, create it: + if (!opaqueBackgroundEntry) { +/// std::cout << "BackgroundManager: Computing (" << image << "," << color.name() << ")..." << std::endl; + opaqueBackgroundEntry = new OpaqueBackgroundEntry(image, color); + opaqueBackgroundEntry->pixmap = new QPixmap(backgroundEntry->pixmap->size()); + opaqueBackgroundEntry->pixmap->fill(color); + QPainter painter(opaqueBackgroundEntry->pixmap); + painter.drawPixmap(0, 0, *(backgroundEntry->pixmap)); + painter.end(); + m_opaqueBackgroundsList.append(opaqueBackgroundEntry); + } + + // We are now sure the entry exist, do the subscription: + ++opaqueBackgroundEntry->customersCount; + return true; +} + +void BackgroundManager::unsubscribe(const QString &image) +{ + BackgroundEntry *entry = backgroundEntryFor(image); + + if (!entry) { +/// std::cout << "BackgroundManager: Wanted to unsuscribe a not subscribed image: " << image << std::endl; + return; + } + + --entry->customersCount; + if (entry->customersCount <= 0) + requestDelayedGarbage(); +} + +void BackgroundManager::unsubscribe(const QString &image, const QColor &color) +{ + OpaqueBackgroundEntry *entry = opaqueBackgroundEntryFor(image, color); + + if (!entry) { +/// std::cout << "BackgroundManager: Wanted to unsuscribe a not subscribed colored image: (" << image << "," << color.name() << ")" << std::endl; + return; + } + + --entry->customersCount; + if (entry->customersCount <= 0) + requestDelayedGarbage(); +} + +QPixmap* BackgroundManager::pixmap(const QString &image) +{ + BackgroundEntry *entry = backgroundEntryFor(image); + + if (!entry || !entry->pixmap || entry->pixmap->isNull()) { +/// std::cout << "BackgroundManager: Requested an unexisting or unsubscribed image: " << image << std::endl; + return 0; + } + + return entry->pixmap; +} + +QPixmap* BackgroundManager::opaquePixmap(const QString &image, const QColor &color) +{ + OpaqueBackgroundEntry *entry = opaqueBackgroundEntryFor(image, color); + + if (!entry || !entry->pixmap || entry->pixmap->isNull()) { +/// std::cout << "BackgroundManager: Requested an unexisting or unsubscribed colored image: (" << image << "," << color.name() << ")" << std::endl; + return 0; + } + + return entry->pixmap; +} + +bool BackgroundManager::tiled(const QString &image) +{ + BackgroundEntry *entry = backgroundEntryFor(image); + + if (!entry || !entry->pixmap || entry->pixmap->isNull()) { +/// std::cout << "BackgroundManager: Requested an unexisting or unsubscribed image: " << image << std::endl; + return false; + } + + return entry->tiled; +} + +bool BackgroundManager::exists(const QString &image) +{ + for (BackgroundsList::iterator it = m_backgroundsList.begin(); it != m_backgroundsList.end(); ++it) + if ((*it)->name == image) + return true; + return false; +} + +QStringList BackgroundManager::imageNames() +{ + QStringList list; + for (BackgroundsList::iterator it = m_backgroundsList.begin(); it != m_backgroundsList.end(); ++it) + list.append((*it)->name); + return list; +} + +QPixmap* BackgroundManager::preview(const QString &image) +{ + static const int MAX_WIDTH = 100; + static const int MAX_HEIGHT = 75; + static const QColor PREVIEW_BG = Qt::white; + + BackgroundEntry *entry = backgroundEntryFor(image); + + if (!entry) { +/// std::cout << "BackgroundManager: Requested the preview of an unexisting image: " << image << std::endl; + return false; + } + + // The easiest way: already computed: + if (entry->preview) + return entry->preview; + + // Then, try to load the preview from file: + QString previewPath = KGlobal::dirs()->findResource("data", "basket/backgrounds/previews/" + entry->name); + QPixmap *previewPixmap = new QPixmap(previewPath); + // Success: + if (!previewPixmap->isNull()) { +/// std::cout << "BackgroundManager: Loaded image preview for " << entry->location << " from file " << previewPath << std::endl; + entry->preview = previewPixmap; + return entry->preview; + } + + // We failed? Then construct it: + // Note: if a preview is requested, it's because the user is currently choosing an image. + // Since we need that image to create the preview, we keep the image in memory. + // Then, it will already be loaded when user press [OK] in the background image chooser. + // BUT we also delay a garbage because we don't want EVERY images to be loaded if the user use only a few of them, of course: + + // Already used? Good: we don't have to load it... + if (!entry->pixmap) { + // Note: it's a code duplication from BackgroundManager::subscribe(const QString &image), + // Because, as we are loading the pixmap we ALSO need to know if it's a tile or not, in case that image will soon be used (and not destroyed by the garbager): + entry->pixmap = new QPixmap(entry->location); + // Try to figure out if it's a tiled background image or not (default to NO): + KSimpleConfig config(entry->location + ".config", /*readOnly=*/true); + config.setGroup("BasKet Background Image Configuration"); + entry->tiled = config.readBoolEntry("tiled", false); + } + + // The image cannot be loaded, we failed: + if (entry->pixmap->isNull()) + return 0; + + // Good that we are still alive: entry->pixmap contains the pixmap to rescale down for the preview: + // Compute new size: + int width = entry->pixmap->width(); + int height = entry->pixmap->height(); + if (width > MAX_WIDTH) { + height = height * MAX_WIDTH / width; + width = MAX_WIDTH; + } + if (height > MAX_HEIGHT) { + width = width * MAX_HEIGHT / height; + height = MAX_HEIGHT; + } + // And create the resulting pixmap: + QPixmap *result = new QPixmap(width, height); + result->fill(PREVIEW_BG); + QImage imageToScale = entry->pixmap->convertToImage(); + QPixmap pmScaled; + pmScaled.convertFromImage(imageToScale.smoothScale(width, height)); + QPainter painter(result); + painter.drawPixmap(0, 0, pmScaled); + painter.end(); + + // Saving it to file for later: + QString folder = KGlobal::dirs()->saveLocation("data", "basket/backgrounds/previews/"); + result->save(folder + entry->name, "PNG"); + + // Ouf! That's done: + entry->preview = result; + requestDelayedGarbage(); + return entry->preview; +} + +QString BackgroundManager::pathForImageName(const QString &image) +{ + BackgroundEntry *entry = backgroundEntryFor(image); + if (entry == 0) + return ""; + else + return entry->location; +} + +QString BackgroundManager::previewPathForImageName(const QString &image) +{ + BackgroundEntry *entry = backgroundEntryFor(image); + if (entry == 0) + return ""; + else { + QString previewPath = KGlobal::dirs()->findResource("data", "basket/backgrounds/previews/" + entry->name); + QDir dir; + if (!dir.exists(previewPath)) + return ""; + else + return previewPath; + } +} + +void BackgroundManager::requestDelayedGarbage() +{ + static const int DELAY = 60/*seconds*/; + + if (!m_garbageTimer.isActive()) + m_garbageTimer.start(DELAY * 1000/*ms*/, /*singleShot=*/true); +} + +void BackgroundManager::doGarbage() +{ +/// std::cout << "BackgroundManager: Doing garbage..." << std::endl; + +/// std::cout << "BackgroundManager: Images:" << std::endl; + for (BackgroundsList::Iterator it = m_backgroundsList.begin(); it != m_backgroundsList.end(); ++it) { + BackgroundEntry *entry = *it; +/// std::cout << "* " << entry->name << ": used " << entry->customersCount << " times"; + if (entry->customersCount <= 0 && entry->pixmap) { +/// std::cout << " [Deleted cached pixmap]"; + delete entry->pixmap; + entry->pixmap = 0; + } +/// std::cout << std::endl; + } + +/// std::cout << "BackgroundManager: Opaque Cached Images:" << std::endl; + for (OpaqueBackgroundsList::Iterator it = m_opaqueBackgroundsList.begin(); it != m_opaqueBackgroundsList.end(); ) { + OpaqueBackgroundEntry *entry = *it; +/// std::cout << "* " << entry->name << "," << entry->color.name() << ": used " << entry->customersCount << " times"; + if (entry->customersCount <= 0) { +/// std::cout << " [Deleted entry]"; + delete entry->pixmap; + entry->pixmap = 0; + it = m_opaqueBackgroundsList.remove(it); + } else + ++it; +/// std::cout << std::endl; + } +} + +#include "backgroundmanager.moc" diff --git a/src/backgroundmanager.h b/src/backgroundmanager.h new file mode 100644 index 0000000..0db7898 --- /dev/null +++ b/src/backgroundmanager.h @@ -0,0 +1,132 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BACKGROUNDMANAGER_H +#define BACKGROUNDMANAGER_H + +#include <qobject.h> +#include <qvaluelist.h> +#include <qstring.h> +#include <qpixmap.h> +#include <qcolor.h> +#include <qtimer.h> + +/** A node in the list of background images of BackgroundManager. + * It can only be used by BackgroundManager because it is an internal structure of this manager. + * @author S�astien Laot + */ +class BackgroundEntry +{ + friend class BackgroundManager; + + protected: + BackgroundEntry(const QString &location); + ~BackgroundEntry(); + + QString name; + QString location; + bool tiled; /// << Only valid after some object subscribed to this image! Because it's only read at this time. + QPixmap *pixmap; /// << Only valid (non-null) after some object subscribed to this image! Because it's only read at this time. + QPixmap *preview; /// << Only valid (non-null) after some object requested the preview. + int customersCount; +}; + +/** A node in the list of opaque background images (with a background color applyed to an image) of BackgroundManager. + * It can only be used by BackgroundManager because it is an internal structure of this manager. + * @author S�astien Laot + */ +class OpaqueBackgroundEntry +{ + friend class BackgroundManager; + + protected: + OpaqueBackgroundEntry(const QString &name, const QColor &color); + ~OpaqueBackgroundEntry(); + + QString name; + QColor color; + QPixmap *pixmap; + int customersCount; +}; + +/** Manage the list of background images. + * BASIC FUNCTIONNING OF A BACKGROUND CHOOSER: + * It get all image names with imageNames() to put them in eg. a QComboBox and then, + * when it's time to get the preview of an image it call preview() with the image name to get it. + * Preview are only computed on demand and then cached to fast the next demands (only the pointer will have to be returned). + * Previews are scalled to fit in a rectangle of 100 by 75 pixels, and with a white background color. + * They are also saved to files, so that the scalling/opaquification has not to be done later (they will be directly loaded from file). + * Previews are saved in Global::backgroundsFolder()+"previews/", so that emptying the folder is sufficient to remove them. + * BASIC FUNCTIONNING OF AN IMAGE REQUESTER: + * When eg. a basket is assigned an image name, it register it with subscribe(). + * The full pixmap is then loaded from file and cached (if it was not already loaded) and the "tiled" property is read from the image configuration file. + * If this object want to have the pixmap applyed on a background color (for no transparency => really faster drawing), + * it should register for the couple (imageName,color) with suscribe(): the pixmap will be created in the cache. + * Then, the object can get the subscribed images with pixmap() or opaquePixmap() and know if it's tiled with tiled(). + * When the user removed the object background image (or when the object/basket/... is removed), the object should call unsubscribe() for + * EVERY subscribed image and image couples. Usage count is decreased for those images and a garbage collector will remove the cached images + * if nothing is subscribed to them (to free memory). + * @author S�astien Laot + */ +class BackgroundManager : private QObject +{ + Q_OBJECT + private: + /// LIST OF IMAGES: + typedef QValueList<BackgroundEntry*> BackgroundsList; + typedef QValueList<OpaqueBackgroundEntry*> OpaqueBackgroundsList; + + public: + /// CONTRUCTOR AND DESTRUCTOR: + BackgroundManager(); + ~BackgroundManager(); + /// SUBSCRIPTION TO IMAGES: + bool subscribe(const QString &image); /// << @Return true if the loading is a success. In the counter-case, calling methods below is unsafe with this @p image name. + bool subscribe(const QString &image, const QColor &color); /// << Idem. + void unsubscribe(const QString &image); + void unsubscribe(const QString &image, const QColor &color); + /// GETTING THE IMAGES AND PROPERTIES: + QPixmap* pixmap(const QString &image); + QPixmap* opaquePixmap(const QString &image, const QColor &color); + bool tiled(const QString &image); + /// LIST OF IMAGES AND PREVIEWS: + bool exists(const QString &image); + QStringList imageNames(); + QPixmap* preview(const QString &image); + /// USED FOR EXPORTATION: + QString pathForImageName(const QString &image); /// << It is STRONGLY advised to not use those two methods unless it's to copy (export) the images or something like that... + QString previewPathForImageName(const QString &image); + /// USED FOR IMPORTATION: + void addImage(const QString &fullPath); + + private: + BackgroundEntry* backgroundEntryFor(const QString &image); + OpaqueBackgroundEntry* opaqueBackgroundEntryFor(const QString &image, const QColor &color); + + private: + BackgroundsList m_backgroundsList; + OpaqueBackgroundsList m_opaqueBackgroundsList; + QTimer m_garbageTimer; + private slots: + void requestDelayedGarbage(); + void doGarbage(); +}; + +#endif // BACKGROUNDMANAGER_H diff --git a/src/backup.cpp b/src/backup.cpp new file mode 100644 index 0000000..00bc660 --- /dev/null +++ b/src/backup.cpp @@ -0,0 +1,414 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "backup.h" + +#include "global.h" +#include "variouswidgets.h" +#include "settings.h" +#include "tools.h" +#include "formatimporter.h" // To move a folder + +#include <qhbox.h> +#include <qvbox.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qpushbutton.h> +#include <klocale.h> +#include <qdir.h> +#include <kapplication.h> +#include <kaboutdata.h> +#include <qgroupbox.h> +#include <kdirselectdialog.h> +#include <krun.h> +#include <kconfig.h> +#include <ktar.h> +#include <kfiledialog.h> +#include <kprogress.h> +#include <kmessagebox.h> +#include <cstdlib> +#include <unistd.h> // usleep() + +/** + * Backups are wrapped in a .tar.gz, inside that folder name. + * An archive is not a backup or is corrupted if data are not in that folder! + */ +const QString backupMagicFolder = "BasKet-Note-Pads_Backup"; + +/** class BackupDialog: */ + +BackupDialog::BackupDialog(QWidget *parent, const char *name) + : KDialogBase(parent, name, /*modal=*/true, i18n("Backup & Restore"), + KDialogBase::Close, KDialogBase::Close, /*separator=*/false) +{ + QVBox *page = makeVBoxMainWidget(); +// page->setSpacing(spacingHint()); + + QString savesFolder = Global::savesFolder(); + savesFolder = savesFolder.left(savesFolder.length() - 1); // savesFolder ends with "/" + + QGroupBox *folderGroup = new QGroupBox(1, Qt::Horizontal, i18n("Save Folder"), page); + new QLabel("<qt><nobr>" + i18n("Your baskets are currently stored in that folder:<br><b>%1</b>").arg(savesFolder), folderGroup); + QWidget *folderWidget = new QWidget(folderGroup); + QHBoxLayout *folderLayout = new QHBoxLayout(folderWidget, 0, spacingHint()); + QPushButton *moveFolder = new QPushButton(i18n("&Move to Another Folder..."), folderWidget); + QPushButton *useFolder = new QPushButton(i18n("&Use Another Existing Folder..."), folderWidget); + HelpLabel *helpLabel = new HelpLabel(i18n("Why to do that?"), i18n( + "<p>You can move the folder where %1 store your baskets to:</p><ul>" + "<li>Store your baskets in a visible place in your home folder, like ~/Notes or ~/Baskets, so you can manually backup them when you want.</li>" + "<li>Store your baskets on a server to share them between two computers.<br>" + "In this case, mount the shared-folder to the local file system and ask %2 to use that mount point.<br>" + "Warning: you should not run %3 at the same time on both computers, or you risk to loss data while the two applications are desynced.</li>" + "</ul><p>Please remember that you should not change the content of that folder manually (eg. adding a file in a basket folder will not add that file to the basket).</p>") + .arg(kapp->aboutData()->programName()) + .arg(kapp->aboutData()->programName()) + .arg(kapp->aboutData()->programName()), + folderWidget); + folderLayout->addWidget(moveFolder); + folderLayout->addWidget(useFolder); + folderLayout->addWidget(helpLabel); + folderLayout->addStretch(); + connect( moveFolder, SIGNAL(clicked()), this, SLOT(moveToAnotherFolder()) ); + connect( useFolder, SIGNAL(clicked()), this, SLOT(useAnotherExistingFolder()) ); + + QGroupBox *backupGroup = new QGroupBox(1, Qt::Horizontal, i18n("Backups"), page); + QWidget *backupWidget = new QWidget(backupGroup); + QHBoxLayout *backupLayout = new QHBoxLayout(backupWidget, 0, spacingHint()); + QPushButton *backupButton = new QPushButton(i18n("&Backup..."), backupWidget); + QPushButton *restoreButton = new QPushButton(i18n("&Restore a Backup..."), backupWidget); + m_lastBackup = new QLabel("", backupWidget); + backupLayout->addWidget(backupButton); + backupLayout->addWidget(restoreButton); + backupLayout->addWidget(m_lastBackup); + backupLayout->addStretch(); + connect( backupButton, SIGNAL(clicked()), this, SLOT(backup()) ); + connect( restoreButton, SIGNAL(clicked()), this, SLOT(restore()) ); + + populateLastBackup(); + + (new QWidget(page))->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +BackupDialog::~BackupDialog() +{ +} + +void BackupDialog::populateLastBackup() +{ + QString lastBackupText = i18n("Last backup: never"); + if (Settings::lastBackup().isValid()) + lastBackupText = i18n("Last backup: %1").arg(Settings::lastBackup().toString(Qt::LocalDate)); + + m_lastBackup->setText(lastBackupText); +} + +void BackupDialog::moveToAnotherFolder() +{ + KURL selectedURL = KDirSelectDialog::selectDirectory( + /*startDir=*/Global::savesFolder(), /*localOnly=*/true, /*parent=*/0, + /*caption=*/i18n("Choose a Folder Where to Move Baskets")); + + if (!selectedURL.isEmpty()) { + QString folder = selectedURL.path(); + QDir dir(folder); + // The folder should not exists, or be empty (because KDirSelectDialog will likely create it anyway): + if (dir.exists()) { + // Get the content of the folder: + QStringList content = dir.entryList(); + if (content.count() > 2) { // "." and ".." + int result = KMessageBox::questionYesNo( + 0, + "<qt>" + i18n("The folder <b>%1</b> is not empty. Do you want to override it?").arg(folder), + i18n("Override Folder?"), + KGuiItem(i18n("&Override"), "filesave") + ); + if (result == KMessageBox::No) + return; + } + Tools::deleteRecursively(folder); + } + FormatImporter copier; + copier.moveFolder(Global::savesFolder(), folder); + Backup::setFolderAndRestart(folder, i18n("Your baskets have been successfuly moved to <b>%1</b>. %2 is going to be restarted to take this change into account.")); + } +} + +void BackupDialog::useAnotherExistingFolder() +{ + KURL selectedURL = KDirSelectDialog::selectDirectory( + /*startDir=*/Global::savesFolder(), /*localOnly=*/true, /*parent=*/0, + /*caption=*/i18n("Choose an Existing Folder to Store Baskets")); + + if (!selectedURL.isEmpty()) { + Backup::setFolderAndRestart(selectedURL.path(), i18n("Your basket save folder has been successfuly changed to <b>%1</b>. %2 is going to be restarted to take this change into account.")); + } +} + +void BackupDialog::backup() +{ + QDir dir; + + // Compute a default file name & path (eg. "Baskets_2007-01-31.tar.gz"): + KConfig *config = KGlobal::config(); + config->setGroup("Backups"); + QString folder = config->readEntry("lastFolder", QDir::homeDirPath()) + "/"; + QString fileName = i18n("Backup filename (without extension), %1 is the date", "Baskets_%1") + .arg(QDate::currentDate().toString(Qt::ISODate)); + QString url = folder + fileName; + + // Ask a file name & path to the user: + QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files"); + QString destination = url; + for (bool askAgain = true; askAgain; ) { + // Ask: + destination = KFileDialog::getSaveFileName(destination, filter, 0, i18n("Backup Baskets")); + // User canceled? + if (destination.isEmpty()) + return; + // File already existing? Ask for overriding: + if (dir.exists(destination)) { + int result = KMessageBox::questionYesNoCancel( + 0, + "<qt>" + i18n("The file <b>%1</b> already exists. Do you really want to override it?") + .arg(KURL(destination).fileName()), + i18n("Override File?"), + KGuiItem(i18n("&Override"), "filesave") + ); + if (result == KMessageBox::Cancel) + return; + else if (result == KMessageBox::Yes) + askAgain = false; + } else + askAgain = false; + } + + KProgressDialog dialog(0, 0, i18n("Backup Baskets"), i18n("Backing up baskets. Please wait..."), /*modal=*/true); + dialog.setAllowCancel(false); + dialog.setAutoClose(true); + dialog.show(); + KProgress *progress = dialog.progressBar(); + progress->setTotalSteps(0/*Busy/Undefined*/); + progress->setProgress(0); + progress->setPercentageVisible(false); + + BackupThread thread(destination, Global::savesFolder()); + thread.start(); + while (thread.running()) { + progress->advance(1); // Or else, the animation is not played! + kapp->processEvents(); + usleep(300); // Not too long because if the backup process is finished, we wait for nothing + } + + Settings::setLastBackup(QDate::currentDate()); + Settings::saveConfig(); + populateLastBackup(); +} + +void BackupDialog::restore() +{ + // Get last backup folder: + KConfig *config = KGlobal::config(); + config->setGroup("Backups"); + QString folder = config->readEntry("lastFolder", QDir::homeDirPath()) + "/"; + + // Ask a file name to the user: + QString filter = "*.tar.gz|" + i18n("Tar Archives Compressed by Gzip") + "\n*|" + i18n("All Files"); + QString path = KFileDialog::getOpenFileName(folder, filter, this, i18n("Open Basket Archive")); + if (path.isEmpty()) // User has canceled + return; + + // Before replacing the basket data folder with the backup content, we safely backup the current baskets to the home folder. + // So if the backup is corrupted or something goes wrong while restoring (power cut...) the user will be able to restore the old working data: + QString safetyPath = Backup::newSafetyFolder(); + FormatImporter copier; + copier.moveFolder(Global::savesFolder(), safetyPath); + + // Add the README file for user to cancel a bad restoration: + QString readmePath = safetyPath + i18n("README.txt"); + QFile file(readmePath); + if (file.open(IO_WriteOnly)) { + QTextStream stream(&file); + stream << i18n("This is a safety copy of your baskets like they were before you started to restore the backup %1.").arg(KURL(path).fileName()) + "\n\n" + << i18n("If the restoration was a success and you restored what you wanted to restore, you can remove this folder.") + "\n\n" + << i18n("If something went wrong during the restoration process, you can re-use this folder to store your baskets and nothing will be lost.") + "\n\n" + << i18n("Choose \"Basket\" -> \"Backup & Restore...\" -> \"Use Another Existing Folder...\" and select that folder.") + "\n"; + file.close(); + } + + QString message = + "<p><nobr>" + i18n("Restoring <b>%1</b>. Please wait...").arg(KURL(path).fileName()) + "</nobr></p><p>" + + i18n("If something goes wrong during the restoration process, read the file <b>%1</b>.").arg(readmePath); + + KProgressDialog *dialog = new KProgressDialog(0, 0, i18n("Restore Baskets"), message, /*modal=*/true); + dialog->setAllowCancel(false); + dialog->setAutoClose(true); + dialog->show(); + KProgress *progress = dialog->progressBar(); + progress->setTotalSteps(0/*Busy/Undefined*/); + progress->setProgress(0); + progress->setPercentageVisible(false); + + // Uncompress: + RestoreThread thread(path, Global::savesFolder()); + thread.start(); + while (thread.running()) { + progress->advance(1); // Or else, the animation is not played! + kapp->processEvents(); + usleep(300); // Not too long because if the restore process is finished, we wait for nothing + } + + dialog->hide(); // The restore is finished, do not continue to show it while telling the user the application is going to be restarted + delete dialog; // If we only hidden it, it reappeared just after having restored a small backup... Very strange. + dialog = 0; // This was annoying since it is modal and the "BasKet Note Pads is going to be restarted" message was not reachable. + //kapp->processEvents(); + + // Check for errors: + if (!thread.success()) { + // Restore the old baskets: + QDir dir; + dir.remove(readmePath); + copier.moveFolder(safetyPath, Global::savesFolder()); + // Tell the user: + KMessageBox::error(0, i18n("This archive is either not a backup of baskets or is corrupted. It cannot be imported. Your old baskets have been preserved instead."), i18n("Restore Error")); + return; + } + + // Note: The safety backup is not removed now because the code can has been wrong, somehow, or the user perhapse restored an older backup by error... + // The restore process will not be called very often (it is possible it will only be called once or twice arround the world during the next years). + // So it is rare enough to force the user to remove the safety folder, but keep him in control and let him safely recover from restoration errors. + + Backup::setFolderAndRestart(Global::savesFolder()/*No change*/, i18n("Your backup has been successfuly restored to <b>%1</b>. %2 is going to be restarted to take this change into account.")); +} + +/** class Backup: */ + +QString Backup::binaryPath = ""; + +#include <iostream> + +void Backup::figureOutBinaryPath(const char *argv0, QApplication &app) +{ + /* + The application can be launched by two ways: + - Globaly (app.applicationFilePath() is good) + - In KDevelop or with an absolute path (app.applicationFilePath() is wrong) + This function is called at the very start of main() so that the current directory has not been changed yet. + + Command line (argv[0]) QDir(argv[0]).canonicalPath() app.applicationFilePath() + ====================== ============================================= ========================= + "basket" "" "/opt/kde3/bin/basket" + "./src/.libs/basket" "/home/seb/prog/basket/debug/src/.lib/basket" "/opt/kde3/bin/basket" + */ + + binaryPath = QDir(argv0).canonicalPath(); + if (binaryPath.isEmpty()) + binaryPath = app.applicationFilePath(); +} + +void Backup::setFolderAndRestart(const QString &folder, const QString &message) +{ + // Set the folder: + Settings::setDataFolder(folder); + Settings::saveConfig(); + + // Rassure the user that the application main window disapearition is not a crash, but a normal restart. + // This is important for users to trust the application in such a critical phase and understands what's happening: + KMessageBox::information( + 0, + "<qt>" + message.arg( + (folder.endsWith("/") ? folder.left(folder.length() - 1) : folder), + kapp->aboutData()->programName()), + i18n("Restart") + ); + + // Restart the application: + KRun::runCommand(binaryPath, kapp->aboutData()->programName(), kapp->iconName()); + exit(0); +} + +QString Backup::newSafetyFolder() +{ + QDir dir; + QString fullPath; + + fullPath = QDir::homeDirPath() + "/" + i18n("Safety folder name before restoring a basket data archive", "Baskets Before Restoration") + "/"; + if (!dir.exists(fullPath)) + return fullPath; + + for (int i = 2; ; ++i) { + fullPath = QDir::homeDirPath() + "/" + i18n("Safety folder name before restoring a basket data archive", "Baskets Before Restoration (%1)").arg(i) + "/"; + if (!dir.exists(fullPath)) + return fullPath; + } + + return ""; +} + +/** class BackupThread: */ + +BackupThread::BackupThread(const QString &tarFile, const QString &folderToBackup) + : m_tarFile(tarFile), m_folderToBackup(folderToBackup) +{ +} + +void BackupThread::run() +{ + KTar tar(m_tarFile, "application/x-gzip"); + tar.open(IO_WriteOnly); + tar.addLocalDirectory(m_folderToBackup, backupMagicFolder); + // KArchive does not add hidden files. Basket description files (".basket") are hidden, we add them manually: + QDir dir(m_folderToBackup + "baskets/"); + QStringList baskets = dir.entryList(QDir::Dirs); + for (QStringList::Iterator it = baskets.begin(); it != baskets.end(); ++it) { + tar.addLocalFile( + m_folderToBackup + "baskets/" + *it + "/.basket", + backupMagicFolder + "/baskets/" + *it + "/.basket" + ); + } + // We finished: + tar.close(); +} + +/** class RestoreThread: */ + +RestoreThread::RestoreThread(const QString &tarFile, const QString &destFolder) + : m_tarFile(tarFile), m_destFolder(destFolder) +{ +} + +void RestoreThread::run() +{ + m_success = false; + KTar tar(m_tarFile, "application/x-gzip"); + tar.open(IO_ReadOnly); + if (tar.isOpened()) { + const KArchiveDirectory *directory = tar.directory(); + if (directory->entries().contains(backupMagicFolder)) { + const KArchiveEntry *entry = directory->entry(backupMagicFolder); + if (entry->isDirectory()) { + ((const KArchiveDirectory*) entry)->copyTo(m_destFolder); + m_success = true; + } + } + tar.close(); + } +} + +#include "backup.moc" diff --git a/src/backup.h b/src/backup.h new file mode 100644 index 0000000..092aba9 --- /dev/null +++ b/src/backup.h @@ -0,0 +1,85 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BACKUP_H +#define BACKUP_H + +#include <kdialogbase.h> +#include <qapplication.h> +#include <qthread.h> + +/** + * @author S�bastien Lao�t + */ +class BackupDialog : public KDialogBase +{ + Q_OBJECT + public: + BackupDialog(QWidget *parent = 0, const char *name = 0); + ~BackupDialog(); + private slots: + void moveToAnotherFolder(); + void useAnotherExistingFolder(); + void backup(); + void restore(); + void populateLastBackup(); + private: + QLabel *m_lastBackup; +}; + +/** + * @author S�bastien Lao�t <[email protected]> + */ +class Backup +{ + public: + static void figureOutBinaryPath(const char *argv0, QApplication &app); + static void setFolderAndRestart(const QString &folder, const QString &message); + static QString newSafetyFolder(); + + private: + static QString binaryPath; +}; + +class BackupThread : public QThread +{ + public: + BackupThread(const QString &tarFile, const QString &folderToBackup); + protected: + virtual void run(); + private: + QString m_tarFile; + QString m_folderToBackup; +}; + +class RestoreThread : public QThread +{ + public: + RestoreThread(const QString &tarFile, const QString &destFolder); + inline bool success() { return m_success; } + protected: + virtual void run(); + private: + QString m_tarFile; + QString m_destFolder; + bool m_success; +}; + +#endif // BACKUP_H diff --git a/src/basket.cpp b/src/basket.cpp new file mode 100644 index 0000000..95a3614 --- /dev/null +++ b/src/basket.cpp @@ -0,0 +1,5734 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qdragobject.h> +#include <qdom.h> +#include <qpainter.h> +#include <qstyle.h> +#include <kstyle.h> +#include <qtooltip.h> +#include <qlistview.h> +#include <qcursor.h> +#include <qsimplerichtext.h> +#include <qpushbutton.h> +#include <ktextedit.h> +#include <qpoint.h> +#include <qstringlist.h> +#include <kapplication.h> +#include <kglobalsettings.h> +#include <kopenwith.h> +#include <kservice.h> +#include <klocale.h> +#include <kglobalaccel.h> +#include <qdir.h> +#include <qfile.h> +#include <qfileinfo.h> +#include <kfiledialog.h> +#include <kaboutdata.h> +#include <klineedit.h> +#include <ksavefile.h> +#include <kdebug.h> +#include <qvbox.h> + +#include <unistd.h> // For sleep() + +#include <kpopupmenu.h> +#include <kiconloader.h> +#include <krun.h> + +#include <qtoolbar.h> +#include <qclipboard.h> + +#include <kmessagebox.h> +#include <qinputdialog.h> + +#include <qlayout.h> + +#include <stdlib.h> // rand() function +#include <qdatetime.h> // seed for rand() + +#include "basket.h" +#include "note.h" +#include "notedrag.h" +#include "notefactory.h" +#include "noteedit.h" +#include "tagsedit.h" +#include "xmlwork.h" +#include "global.h" +#include "backgroundmanager.h" +#include "settings.h" +#include "tools.h" +#include "debugwindow.h" +#include "exporterdialog.h" +#include "config.h" +#include "popupmenu.h" +#ifdef HAVE_LIBGPGME +#include "kgpgme.h" +#endif + +#include <iostream> + +/** Class NoteSelection: */ + +NoteSelection* NoteSelection::nextStacked() +{ + // First, search in the childs: + if (firstChild) + if (firstChild->note && firstChild->note->content()) + return firstChild; + else + return firstChild->nextStacked(); + + // Then, in the next: + if (next) + if (next->note && next->note->content()) + return next; + else + return next->nextStacked(); + + // And finally, in the parent: + NoteSelection *node = parent; + while (node) + if (node->next) + if (node->next->note && node->next->note->content()) + return node->next; + else + return node->next->nextStacked(); + else + node = node->parent; + + // Not found: + return 0; +} + +NoteSelection* NoteSelection::firstStacked() +{ + if (!this) + return 0; + + if (note && note->content()) + return this; + else + return nextStacked(); +} + +void NoteSelection::append(NoteSelection *node) +{ + if (!this || !node) + return; + + if (firstChild) { + NoteSelection *last = firstChild; + while (last->next) + last = last->next; + last->next = node; + } else + firstChild = node; + + while (node) { + node->parent = this; + node = node->next; + } +} + +int NoteSelection::count() +{ + if (!this) + return 0; + + int count = 0; + + for (NoteSelection *node = this; node; node = node->next) + if (node->note && node->note->content()) + ++count; + else + count += node->firstChild->count(); + + return count; +} + +QValueList<Note*> NoteSelection::parentGroups() +{ + QValueList<Note*> groups; + + // For each note: + for (NoteSelection *node = firstStacked(); node; node = node->nextStacked()) + // For each parent groups of the note: + for (Note *note = node->note->parentNote(); note; note = note->parentNote()) + // Add it (if it was not already in the list): + if (!note->isColumn() && !groups.contains(note)) + groups.append(note); + + return groups; +} + +/** Class DecoratedBasket: */ + +DecoratedBasket::DecoratedBasket(QWidget *parent, const QString &folderName, const char *name, WFlags fl) + : QWidget(parent, name, fl) +{ + m_layout = new QVBoxLayout(this); + m_filter = new FilterBar(this); + m_basket = new Basket(this, folderName); + m_layout->addWidget(m_basket); + setFilterBarPosition(Settings::filterOnTop()); + + m_filter->setShown(true); + m_basket->setFocus(); // To avoid the filter bar have focus on load + + connect( m_filter, SIGNAL(newFilter(const FilterData&)), m_basket, SLOT(newFilter(const FilterData&)) ); + connect( m_filter, SIGNAL(escapePressed()), m_basket, SLOT(cancelFilter()) ); + connect( m_filter, SIGNAL(returnPressed()), m_basket, SLOT(validateFilter()) ); + + connect( m_basket, SIGNAL(postMessage(const QString&)), Global::bnpView, SLOT(postStatusbarMessage(const QString&)) ); + connect( m_basket, SIGNAL(setStatusBarText(const QString&)), Global::bnpView, SLOT(setStatusBarHint(const QString&)) ); + connect( m_basket, SIGNAL(resetStatusBarText()), Global::bnpView, SLOT(updateStatusBarHint()) ); +} + +DecoratedBasket::~DecoratedBasket() +{ +} + +void DecoratedBasket::setFilterBarPosition(bool onTop) +{ + m_layout->remove(m_filter); + if (onTop) { + m_layout->insertWidget(0, m_filter); + setTabOrder(this/*(QWidget*)parent()*/, m_filter); + setTabOrder(m_filter, m_basket); + setTabOrder(m_basket, (QWidget*)parent()); + } else { + m_layout->addWidget(m_filter); + setTabOrder(this/*(QWidget*)parent()*/, m_basket); + setTabOrder(m_basket, m_filter); + setTabOrder(m_filter, (QWidget*)parent()); + } +} + +void DecoratedBasket::setFilterBarShown(bool show, bool switchFocus) +{ +// m_basket->setShowFilterBar(true);//show); +// m_basket->save(); + // In this order (m_basket and then m_filter) because setShown(false) + // will call resetFilter() that will update actions, and then check the + // Ctrl+F action whereas it should be unchecked + // FIXME: It's very uggly all those things + m_filter->setShown(true);//show); + if (show) { + if (switchFocus) + m_filter->setEditFocus(); + } else if (m_filter->hasEditFocus()) + m_basket->setFocus(); +} + +void DecoratedBasket::resetFilter() +{ + m_filter->reset(); +} + +/** Class TransparentWidget */ + +TransparentWidget::TransparentWidget(Basket *basket) + : QWidget(basket->viewport(), "", Qt::WNoAutoErase), m_basket(basket) +{ + setFocusPolicy(QWidget::NoFocus); + setWFlags(Qt::WNoAutoErase); + setMouseTracking(true); // To receive mouseMoveEvents + + basket->viewport()->installEventFilter(this); +} + +/*void TransparentWidget::reparent(QWidget *parent, WFlags f, const QPoint &p, bool showIt) +{ + QWidget::reparent(parent, Qt::WNoAutoErase, p, showIt); +}*/ + +void TransparentWidget::setPosition(int x, int y) +{ + m_x = x; + m_y = y; +} + +void TransparentWidget::paintEvent(QPaintEvent*event) +{ + QWidget::paintEvent(event); + QPainter painter(this); + +// painter.save(); + + painter.translate(-m_x, -m_y); + m_basket->drawContents(&painter, m_x, m_y, width(), height()); + +// painter.restore(); +// painter.setPen(Qt::blue); +// painter.drawRect(0, 0, width(), height()); +} + +void TransparentWidget::mouseMoveEvent(QMouseEvent *event) +{ + QMouseEvent *translated = new QMouseEvent(QEvent::MouseMove, event->pos() + QPoint(m_x, m_y), event->button(), event->state()); + m_basket->contentsMouseMoveEvent(translated); + delete translated; +} + +bool TransparentWidget::eventFilter(QObject */*object*/, QEvent *event) +{ + // If the parent basket viewport has changed, we should change too: + if (event->type() == QEvent::Paint) + update(); + + return false; // Event not consumed, in every cases (because it's only a notification)! +} + +/** Class Basket: */ + +const int Basket::FRAME_DELAY = 50/*1500*/; // Delay between two animation "frames" in milliseconds + +/* + * Convenient function (defined in note.cpp !): + */ +void drawGradient( QPainter *p, const QColor &colorTop, const QColor & colorBottom, + int x, int y, int w, int h, + bool sunken, bool horz, bool flat ); + +/* + * Defined in note.cpp: + */ +extern void substractRectOnAreas(const QRect &rectToSubstract, QValueList<QRect> &areas, bool andRemove = true); + +void debugZone(int zone) +{ + QString s; + switch (zone) { + case Note::Handle: s = "Handle"; break; + case Note::Group: s = "Group"; break; + case Note::TagsArrow: s = "TagsArrow"; break; + case Note::Custom0: s = "Custom0"; break; + case Note::GroupExpander: s = "GroupExpander"; break; + case Note::Content: s = "Content"; break; + case Note::Link: s = "Link"; break; + case Note::TopInsert: s = "TopInsert"; break; + case Note::TopGroup: s = "TopGroup"; break; + case Note::BottomInsert: s = "BottomInsert"; break; + case Note::BottomGroup: s = "BottomGroup"; break; + case Note::BottomColumn: s = "BottomColumn"; break; + case Note::None: s = "None"; break; + default: + if (zone == Note::Emblem0) + s = "Emblem0"; + else + s = "Emblem0+" + QString::number(zone - Note::Emblem0); + break; + } + std::cout << s << std::endl; +} + +#define FOR_EACH_NOTE(noteVar) \ + for (Note *noteVar = firstNote(); noteVar; noteVar = noteVar->next()) + +void Basket::prependNoteIn(Note *note, Note *in) +{ + if (!note) + // No note to prepend: + return; + + if (in) { + // The normal case: + preparePlug(note); + + Note *last = note->lastSibling(); + + for (Note *n = note; n; n = n->next()) + n->setParentNote(in); +// note->setPrev(0L); + last->setNext(in->firstChild()); + + if (in->firstChild()) + in->firstChild()->setPrev(last); + + in->setFirstChild(note); + + if (m_loaded) + signalCountsChanged(); + } else + // Prepend it directly in the basket: + appendNoteBefore(note, firstNote()); +} + +void Basket::appendNoteIn(Note *note, Note *in) +{ + if (!note) + // No note to append: + return; + + if (in) { + // The normal case: + preparePlug(note); + +// Note *last = note->lastSibling(); + Note *lastChild = in->lastChild(); + + for (Note *n = note; n; n = n->next()) + n->setParentNote(in); + note->setPrev(lastChild); +// last->setNext(0L); + + if (!in->firstChild()) + in->setFirstChild(note); + + if (lastChild) + lastChild->setNext(note); + + if (m_loaded) + signalCountsChanged(); + } else + // Prepend it directly in the basket: + appendNoteAfter(note, lastNote()); +} + +void Basket::appendNoteAfter(Note *note, Note *after) +{ + if (!note) + // No note to append: + return; + + if (!after) + // By default, insert after the last note: + after = lastNote(); + + if (m_loaded && after && !after->isFree() && !after->isColumn()) + for (Note *n = note; n; n = n->next()) + n->inheritTagsOf(after); + +// if (!alreadyInBasket) + preparePlug(note); + + Note *last = note->lastSibling(); + if (after) { + // The normal case: + for (Note *n = note; n; n = n->next()) + n->setParentNote(after->parentNote()); + note->setPrev(after); + last->setNext(after->next()); + after->setNext(note); + if (last->next()) + last->next()->setPrev(last); + } else { + // There is no note in the basket: + for (Note *n = note; n; n = n->next()) + n->setParentNote(0); + m_firstNote = note; +// note->setPrev(0); +// last->setNext(0); + } + +// if (!alreadyInBasket) + if (m_loaded) + signalCountsChanged(); +} + +void Basket::appendNoteBefore(Note *note, Note *before) +{ + if (!note) + // No note to append: + return; + + if (!before) + // By default, insert before the first note: + before = firstNote(); + + if (m_loaded && before && !before->isFree() && !before->isColumn()) + for (Note *n = note; n; n = n->next()) + n->inheritTagsOf(before); + + preparePlug(note); + + Note *last = note->lastSibling(); + if (before) { + // The normal case: + for (Note *n = note; n; n = n->next()) + n->setParentNote(before->parentNote()); + note->setPrev(before->prev()); + last->setNext(before); + before->setPrev(last); + if (note->prev()) + note->prev()->setNext(note); + else { + if (note->parentNote()) + note->parentNote()->setFirstChild(note); + else + m_firstNote = note; + } + } else { + // There is no note in the basket: + for (Note *n = note; n; n = n->next()) + n->setParentNote(0); + m_firstNote = note; +// note->setPrev(0); +// last->setNext(0); + } + + if (m_loaded) + signalCountsChanged(); +} + +DecoratedBasket* Basket::decoration() +{ + return (DecoratedBasket*)parent(); +} + +void Basket::preparePlug(Note *note) +{ + // Select only the new notes, compute the new notes count and the new number of found notes: + if (m_loaded) + unselectAll(); + int count = 0; + int founds = 0; + Note *last = 0; + for (Note *n = note; n; n = n->next()) { + if (m_loaded) + n->setSelectedRecursivly(true); // Notes should have a parent basket (and they have, so that's OK). + count += n->count(); + founds += n->newFilter(decoration()->filterData()); + last = n; + } + m_count += count; + m_countFounds += founds; + + // Focus the last inserted note: + if (m_loaded && last) { + setFocusedNote(last); + m_startOfShiftSelectionNote = (last->isGroup() ? last->lastRealChild() : last); + } + + // If some notes don't match (are hidden), tell it to the user: + if (m_loaded && founds < count) { + if (count == 1) postMessage( i18n("The new note does not match the filter and is hidden.") ); + else if (founds == count - 1) postMessage( i18n("A new note does not match the filter and is hidden.") ); + else if (founds > 0) postMessage( i18n("Some new notes do not match the filter and are hidden.") ); + else postMessage( i18n("The new notes do not match the filter and are hidden.") ); + } +} + +void Basket::unplugNote(Note *note) +{ + // If there is nothing to do... + if (!note) + return; + +// if (!willBeReplugged) { + note->setSelectedRecursivly(false); // To removeSelectedNote() and decrease the selectedsCount. + m_count -= note->count(); + m_countFounds -= note->newFilter(decoration()->filterData()); + signalCountsChanged(); +// } + + // If it was the first note, change the first note: + if (m_firstNote == note) + m_firstNote = note->next(); + + // Change previous and next notes: + if (note->prev()) + note->prev()->setNext(note->next()); + if (note->next()) + note->next()->setPrev(note->prev()); + + if (note->parentNote()) { + // If it was the first note of a group, change the first note of the group: + if (note->parentNote()->firstChild() == note) + note->parentNote()->setFirstChild( note->next() ); + + if (!note->parentNote()->isColumn()) { + // Ungroup if still 0 note inside parent group: + if ( ! note->parentNote()->firstChild() ) + unplugNote(note->parentNote()); // TODO delete + + // Ungroup if still 1 note inside parent group: + else if ( ! note->parentNote()->firstChild()->next() ) + ungroupNote(note->parentNote()); + } + } + + note->setParentNote(0); + note->setPrev(0); + note->setNext(0); + +// recomputeBlankRects(); // FIXME: called too much time. It's here because when dragging and moving a note to another basket and then go back to the original basket, the note is deleted but the note rect is not painter anymore. +} + +void Basket::ungroupNote(Note *group) +{ + Note *note = group->firstChild(); + Note *lastGroupedNote = group; + Note *nextNote; + + // Move all notes after the group (not before, to avoid to change m_firstNote or group->m_firstChild): + while (note) { + nextNote = note->next(); + + if (lastGroupedNote->next()) + lastGroupedNote->next()->setPrev(note); + note->setNext(lastGroupedNote->next()); + lastGroupedNote->setNext(note); + note->setParentNote(group->parentNote()); + note->setPrev(lastGroupedNote); + + note->setGroupWidth(group->groupWidth() - Note::GROUP_WIDTH); + lastGroupedNote = note; + note = nextNote; + } + + // Unplug the group: + group->setFirstChild(0); + unplugNote(group); // TODO: delete + + relayoutNotes(true); +} + +void Basket::groupNoteBefore(Note *note, Note *with) +{ + if (!note || !with) + // No note to group or nowhere to group it: + return; + +// if (m_loaded && before && !with->isFree() && !with->isColumn()) + for (Note *n = note; n; n = n->next()) + n->inheritTagsOf(with); + + preparePlug(note); + + Note *last = note->lastSibling(); + + Note *group = new Note(this); + group->setPrev(with->prev()); + group->setNext(with->next()); + group->setX(with->x()); + group->setY(with->y()); + if (with->parentNote() && with->parentNote()->firstChild() == with) + with->parentNote()->setFirstChild(group); + else if (m_firstNote == with) + m_firstNote = group; + group->setParentNote(with->parentNote()); + group->setFirstChild(note); + group->setGroupWidth(with->groupWidth() + Note::GROUP_WIDTH); + + if (with->prev()) + with->prev()->setNext(group); + if (with->next()) + with->next()->setPrev(group); + with->setParentNote(group); + with->setPrev(last); + with->setNext(0L); + + for (Note *n = note; n; n = n->next()) + n->setParentNote(group); +// note->setPrev(0L); + last->setNext(with); + + if (m_loaded) + signalCountsChanged(); +} + +void Basket::groupNoteAfter(Note *note, Note *with) +{ + if (!note || !with) + // No note to group or nowhere to group it: + return; + +// if (m_loaded && before && !with->isFree() && !with->isColumn()) + for (Note *n = note; n; n = n->next()) + n->inheritTagsOf(with); + + preparePlug(note); + +// Note *last = note->lastSibling(); + + Note *group = new Note(this); + group->setPrev(with->prev()); + group->setNext(with->next()); + group->setX(with->x()); + group->setY(with->y()); + if (with->parentNote() && with->parentNote()->firstChild() == with) + with->parentNote()->setFirstChild(group); + else if (m_firstNote == with) + m_firstNote = group; + group->setParentNote(with->parentNote()); + group->setFirstChild(with); + group->setGroupWidth(with->groupWidth() + Note::GROUP_WIDTH); + + if (with->prev()) + with->prev()->setNext(group); + if (with->next()) + with->next()->setPrev(group); + with->setParentNote(group); + with->setPrev(0L); + with->setNext(note); + + for (Note *n = note; n; n = n->next()) + n->setParentNote(group); + note->setPrev(with); +// last->setNext(0L); + + if (m_loaded) + signalCountsChanged(); +} + +void Basket::loadNotes(const QDomElement ¬es, Note *parent) +{ + Note *note; + for (QDomNode n = notes.firstChild(); !n.isNull(); n = n.nextSibling()) { + QDomElement e = n.toElement(); + if (e.isNull()) // Cannot handle that! + continue; + note = 0; + // Load a Group: + if (e.tagName() == "group") { + note = new Note(this); // 1. Create the group... + loadNotes(e, note); // 3. ... And populate it with child notes. + int noteCount = note->count(); + if (noteCount > 0 || (parent == 0 && !isFreeLayout())) { // But don't remove columns! + appendNoteIn(note, parent); // 2. ... Insert it... FIXME: Initially, the if() the insrtion was the step 2. Was it on purpose? + // The notes in the group are counted two times (it's why appendNoteIn() was called before loadNotes): + m_count -= noteCount;// TODO: Recompute note count every time noteCount() is emitted! + m_countFounds -= noteCount; + } + } + // Load a Content-Based Note: + if (e.tagName() == "note" || e.tagName() == "item") { // Keep compatible with 0.6.0 Alpha 1 + note = new Note(this); // Create the note... + NoteFactory__loadNode(XMLWork::getElement(e, "content"), e.attribute("type"), note, /*lazyLoad=*/m_finishLoadOnFirstShow); // ... Populate it with content... + if (e.attribute("type") == "text") + m_shouldConvertPlainTextNotes = true; // Convert Pre-0.6.0 baskets: plain text notes should be converted to rich text ones once all is loaded! + appendNoteIn(note, parent); // ... And insert it. + // Load dates: + if (e.hasAttribute("added")) + note->setAddedDate( QDateTime::fromString(e.attribute("added"), Qt::ISODate)); + if (e.hasAttribute("lastModification")) + note->setLastModificationDate(QDateTime::fromString(e.attribute("lastModification"), Qt::ISODate)); + } + // If we successfully loaded a note: + if (note) { + // Free Note Properties: + if (note->isFree()) { + int x = e.attribute("x").toInt(); + int y = e.attribute("y").toInt(); + note->setX(x < 0 ? 0 : x); + note->setY(y < 0 ? 0 : y); + } + // Resizeable Note Properties: + if (note->hasResizer() || note->isColumn()) + note->setGroupWidth(e.attribute("width", "200").toInt()); + // Group Properties: + if (note->isGroup() && !note->isColumn() && XMLWork::trueOrFalse(e.attribute("folded", "false"))) + note->toggleFolded(false); + // Tags: + if (note->content()) { + QString tagsString = XMLWork::getElementText(e, "tags", ""); + QStringList tagsId = QStringList::split(";", tagsString); + for (QStringList::iterator it = tagsId.begin(); it != tagsId.end(); ++it) { + State *state = Tag::stateForId(*it); + if (state) + note->addState(state, /*orReplace=*/true); + } + } + } +// kapp->processEvents(); + } +} + +void Basket::saveNotes(QDomDocument &document, QDomElement &element, Note *parent) +{ + Note *note = (parent ? parent->firstChild() : firstNote()); + while (note) { + // Create Element: + QDomElement noteElement = document.createElement(note->isGroup() ? "group" : "note"); + element.appendChild(noteElement); + // Free Note Properties: + if (note->isFree()) { + noteElement.setAttribute("x", note->finalX()); + noteElement.setAttribute("y", note->finalY()); + } + // Resizeable Note Properties: + if (note->hasResizer()) + noteElement.setAttribute("width", note->groupWidth()); + // Group Properties: + if (note->isGroup() && !note->isColumn()) + noteElement.setAttribute("folded", XMLWork::trueOrFalse(note->isFolded())); + // Save Content: + if (note->content()) { + // Save Dates: + noteElement.setAttribute("added", note->addedDate().toString(Qt::ISODate) ); + noteElement.setAttribute("lastModification", note->lastModificationDate().toString(Qt::ISODate)); + // Save Content: + noteElement.setAttribute("type", note->content()->lowerTypeName()); + QDomElement content = document.createElement("content"); + noteElement.appendChild(content); + note->content()->saveToNode(document, content); + // Save Tags: + if (note->states().count() > 0) { + QString tags; + for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) + tags += (tags.isEmpty() ? "" : ";") + (*it)->id(); + XMLWork::addElement(document, noteElement, "tags", tags); + } + } else + // Save Child Notes: + saveNotes(document, noteElement, note); + // Go to the Next One: + note = note->next(); + } +} + +void Basket::loadProperties(const QDomElement &properties) +{ + // Compute Default Values for When Loading the Properties: + QString defaultBackgroundColor = (backgroundColorSetting().isValid() ? backgroundColorSetting().name() : ""); + QString defaultTextColor = (textColorSetting().isValid() ? textColorSetting().name() : ""); + + // Load the Properties: + QString icon = XMLWork::getElementText(properties, "icon", this->icon() ); + QString name = XMLWork::getElementText(properties, "name", basketName() ); + + QDomElement appearance = XMLWork::getElement(properties, "appearance"); + // In 0.6.0-Alpha versions, there was a typo error: "backround" instead of "background" + QString backgroundImage = appearance.attribute( "backgroundImage", appearance.attribute( "backroundImage", backgroundImageName() ) ); + QString backgroundColorString = appearance.attribute( "backgroundColor", appearance.attribute( "backroundColor", defaultBackgroundColor ) ); + QString textColorString = appearance.attribute( "textColor", defaultTextColor ); + QColor backgroundColor = (backgroundColorString.isEmpty() ? QColor() : QColor(backgroundColorString)); + QColor textColor = (textColorString.isEmpty() ? QColor() : QColor(textColorString) ); + + QDomElement disposition = XMLWork::getElement(properties, "disposition"); + bool free = XMLWork::trueOrFalse( disposition.attribute( "free", XMLWork::trueOrFalse(isFreeLayout()) ) ); + int columnCount = disposition.attribute( "columnCount", QString::number(this->columnsCount()) ).toInt(); + bool mindMap = XMLWork::trueOrFalse( disposition.attribute( "mindMap", XMLWork::trueOrFalse(isMindMap()) ) ); + + QDomElement shortcut = XMLWork::getElement(properties, "shortcut"); + QString actionStrings[] = { "show", "globalShow", "globalSwitch" }; + KShortcut combination = KShortcut( shortcut.attribute( "combination", m_action->shortcut().toStringInternal() ) ); + QString actionString = shortcut.attribute( "action" ); + int action = shortcutAction(); + if (actionString == actionStrings[0]) action = 0; + if (actionString == actionStrings[1]) action = 1; + if (actionString == actionStrings[2]) action = 2; + + QDomElement protection = XMLWork::getElement(properties, "protection"); + m_encryptionType = protection.attribute( "type" ).toInt(); + m_encryptionKey = protection.attribute( "key" ); + + // Apply the Properties: + setDisposition((free ? (mindMap ? 2 : 1) : 0), columnCount); + setShortcut(combination, action); + setAppearance(icon, name, backgroundImage, backgroundColor, textColor); // Will emit propertiesChanged(this) +} + +void Basket::saveProperties(QDomDocument &document, QDomElement &properties) +{ + XMLWork::addElement( document, properties, "name", basketName() ); + XMLWork::addElement( document, properties, "icon", icon() ); + + QDomElement appearance = document.createElement("appearance"); + properties.appendChild(appearance); + appearance.setAttribute( "backgroundImage", backgroundImageName() ); + appearance.setAttribute( "backgroundColor", backgroundColorSetting().isValid() ? backgroundColorSetting().name() : "" ); + appearance.setAttribute( "textColor", textColorSetting().isValid() ? textColorSetting().name() : "" ); + + QDomElement disposition = document.createElement("disposition"); + properties.appendChild(disposition); + disposition.setAttribute( "free", XMLWork::trueOrFalse(isFreeLayout()) ); + disposition.setAttribute( "columnCount", QString::number(columnsCount()) ); + disposition.setAttribute( "mindMap", XMLWork::trueOrFalse(isMindMap()) ); + + QDomElement shortcut = document.createElement("shortcut"); + properties.appendChild(shortcut); + QString actionStrings[] = { "show", "globalShow", "globalSwitch" }; + shortcut.setAttribute( "combination", m_action->shortcut().toStringInternal() ); + shortcut.setAttribute( "action", actionStrings[shortcutAction()] ); + + QDomElement protection = document.createElement("protection"); + properties.appendChild(protection); + protection.setAttribute( "type", m_encryptionType ); + protection.setAttribute( "key", m_encryptionKey ); +} + +void Basket::subscribeBackgroundImages() +{ + if (!m_backgroundImageName.isEmpty()) { + Global::backgroundManager->subscribe(m_backgroundImageName); + Global::backgroundManager->subscribe(m_backgroundImageName, this->backgroundColor()); + Global::backgroundManager->subscribe(m_backgroundImageName, selectionRectInsideColor()); + m_backgroundPixmap = Global::backgroundManager->pixmap(m_backgroundImageName); + m_opaqueBackgroundPixmap = Global::backgroundManager->opaquePixmap(m_backgroundImageName, this->backgroundColor()); + m_selectedBackgroundPixmap = Global::backgroundManager->opaquePixmap(m_backgroundImageName, selectionRectInsideColor()); + m_backgroundTiled = Global::backgroundManager->tiled(m_backgroundImageName); + } +} + +void Basket::unsubscribeBackgroundImages() +{ + if (hasBackgroundImage()) { + Global::backgroundManager->unsubscribe(m_backgroundImageName); + Global::backgroundManager->unsubscribe(m_backgroundImageName, this->backgroundColor()); + Global::backgroundManager->unsubscribe(m_backgroundImageName, selectionRectInsideColor()); + m_backgroundPixmap = 0; + m_opaqueBackgroundPixmap = 0; + m_selectedBackgroundPixmap = 0; + } +} + +void Basket::setAppearance(const QString &icon, const QString &name, const QString &backgroundImage, const QColor &backgroundColor, const QColor &textColor) +{ + unsubscribeBackgroundImages(); + + m_icon = icon; + m_basketName = name; + m_backgroundImageName = backgroundImage; + m_backgroundColorSetting = backgroundColor; + m_textColorSetting = textColor; + + m_action->setText("BASKET SHORTCUT: " + name); + + // Basket should ALWAYS have an icon (the "basket" icon by default): + QPixmap iconTest = kapp->iconLoader()->loadIcon(m_icon, KIcon::NoGroup, 16, KIcon::DefaultState, 0L, /*canReturnNull=*/true); + if (iconTest.isNull()) + m_icon = "basket"; + + // We don't request the background images if it's not loaded yet (to make the application startup fast). + // When the basket is loading (because requested by the user: he/she want to access it) + // it load the properties, subscribe to (and then load) the images, update the "Loading..." message with the image, + // load all the notes and it's done! + if (m_loadingLaunched) + subscribeBackgroundImages(); + + recomputeAllStyles(); // If a note have a tag with the same background color as the basket one, then display a "..." + recomputeBlankRects(); // See the drawing of blank areas in Basket::drawContents() + unbufferizeAll(); + updateContents(); + + if (isDuringEdit() && m_editor->widget()) { + m_editor->widget()->setPaletteBackgroundColor( m_editor->note()->backgroundColor() ); + m_editor->widget()->setPaletteForegroundColor( m_editor->note()->textColor() ); + } + + emit propertiesChanged(this); +} + +void Basket::setDisposition(int disposition, int columnCount) +{ + static const int COLUMNS_LAYOUT = 0; + static const int FREE_LAYOUT = 1; + static const int MINDMAPS_LAYOUT = 2; + + int currentDisposition = (isFreeLayout() ? (isMindMap() ? MINDMAPS_LAYOUT : FREE_LAYOUT) : COLUMNS_LAYOUT); + + if (currentDisposition == COLUMNS_LAYOUT && disposition == COLUMNS_LAYOUT) { + if (firstNote() && columnCount > m_columnsCount) { + // Insert each new columns: + for (int i = m_columnsCount; i < columnCount; ++i) { + Note *newColumn = new Note(this); + insertNote(newColumn, /*clicked=*/lastNote(), /*zone=*/Note::BottomInsert, QPoint(), /*animateNewPosition=*/false); + } + } else if (firstNote() && columnCount < m_columnsCount) { + Note *column = firstNote(); + Note *cuttedNotes = 0; + for (int i = 1; i <= m_columnsCount; ++i) { + Note *columnToRemove = column; + column = column->next(); + if (i > columnCount) { + // Remove the columns that are too much: + unplugNote(columnToRemove); + // "Cut" the content in the columns to be deleted: + if (columnToRemove->firstChild()) { + for (Note *it = columnToRemove->firstChild(); it; it = it->next()) + it->setParentNote(0); + if (!cuttedNotes) + cuttedNotes = columnToRemove->firstChild(); + else { + Note *lastCuttedNote = cuttedNotes; + while (lastCuttedNote->next()) + lastCuttedNote = lastCuttedNote->next(); + lastCuttedNote->setNext(columnToRemove->firstChild()); + columnToRemove->firstChild()->setPrev(lastCuttedNote); + } + columnToRemove->setFirstChild(0); + } + } + } + // Paste the content in the last column: + if (cuttedNotes) + insertNote(cuttedNotes, /*clicked=*/lastNote(), /*zone=*/Note::BottomColumn, QPoint(), /*animateNewPosition=*/true); + unselectAll(); + } + if (columnCount != m_columnsCount) { + m_columnsCount = (columnCount <= 0 ? 1 : columnCount); + equalizeColumnSizes(); // Will relayoutNotes() + } + } else if (currentDisposition == COLUMNS_LAYOUT && (disposition == FREE_LAYOUT || disposition == MINDMAPS_LAYOUT)) { + Note *column = firstNote(); + m_columnsCount = 0; // Now, so relayoutNotes() will not relayout the free notes as if they were columns! + while (column) { + // Move all childs on the first level: + Note *nextColumn = column->next(); + ungroupNote(column); + column = nextColumn; + } + unselectAll(); + m_mindMap = (disposition == MINDMAPS_LAYOUT); + relayoutNotes(true); + } else if ((currentDisposition == FREE_LAYOUT || currentDisposition == MINDMAPS_LAYOUT) && disposition == COLUMNS_LAYOUT) { + if (firstNote()) { + // TODO: Reorder notes! + // Remove all notes (but keep a reference to them, we're not crazy ;-) ): + Note *notes = m_firstNote; + m_firstNote = 0; + m_count = 0; + m_countFounds = 0; + // Insert the number of columns that is needed: + Note *lastInsertedColumn = 0; + for (int i = 0; i < columnCount; ++i) { + Note *column = new Note(this); + if (lastInsertedColumn) + insertNote(column, /*clicked=*/lastInsertedColumn, /*zone=*/Note::BottomInsert, QPoint(), /*animateNewPosition=*/false); + else + m_firstNote = column; + lastInsertedColumn = column; + } + // Reinsert the old notes in the first column: + insertNote(notes, /*clicked=*/firstNote(), /*zone=*/Note::BottomColumn, QPoint(), /*animateNewPosition=*/true); + unselectAll(); + } else { + // Insert the number of columns that is needed: + Note *lastInsertedColumn = 0; + for (int i = 0; i < columnCount; ++i) { + Note *column = new Note(this); + if (lastInsertedColumn) + insertNote(column, /*clicked=*/lastInsertedColumn, /*zone=*/Note::BottomInsert, QPoint(), /*animateNewPosition=*/false); + else + m_firstNote = column; + lastInsertedColumn = column; + } + } + m_columnsCount = (columnCount <= 0 ? 1 : columnCount); + equalizeColumnSizes(); // Will relayoutNotes() + } +} + +void Basket::equalizeColumnSizes() +{ + if (!firstNote()) + return; + + // Necessary to know the available space; + relayoutNotes(true); + + int availableSpace = visibleWidth(); + int columnWidth = (visibleWidth() - (columnsCount()-1)*Note::GROUP_WIDTH) / columnsCount(); + int columnCount = columnsCount(); + Note *column = firstNote(); + while (column) { + int minGroupWidth = column->minRight() - column->x(); + if (minGroupWidth > columnWidth) { + availableSpace -= minGroupWidth; + --columnCount; + } + column = column->next(); + } + columnWidth = (availableSpace - (columnsCount()-1)*Note::GROUP_WIDTH) / columnCount; + + column = firstNote(); + while (column) { + int minGroupWidth = column->minRight() - column->x(); + if (minGroupWidth > columnWidth) + column->setGroupWidth(minGroupWidth); + else + column->setGroupWidth(columnWidth); + column = column->next(); + } + + relayoutNotes(true); +} + +void Basket::enableActions() +{ + Global::bnpView->enableActions(); + setFocusPolicy(isLocked() ? QWidget::NoFocus : QWidget::StrongFocus); + if (isLocked()) + viewport()->setCursor(Qt::ArrowCursor); // When locking, the cursor stays the last form it was +} + +bool Basket::save() +{ + if (!m_loaded) + return false; + + DEBUG_WIN << "Basket[" + folderName() + "]: Saving..."; + + // Create Document: + QDomDocument document(/*doctype=*/"basket"); + QDomElement root = document.createElement("basket"); + document.appendChild(root); + + // Create Properties Element and Populate It: + QDomElement properties = document.createElement("properties"); + saveProperties(document, properties); + root.appendChild(properties); + + // Create Notes Element and Populate It: + QDomElement notes = document.createElement("notes"); + saveNotes(document, notes, 0); + root.appendChild(notes); + + // Write to Disk: + if(!saveToFile(fullPath() + ".basket", "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + document.toString())) + { + DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to save</font>!"; + return false; + } + + Global::bnpView->setUnsavedStatus(false); + return true; +} + +void Basket::aboutToBeActivated() +{ + if (m_finishLoadOnFirstShow) { + FOR_EACH_NOTE (note) + note->finishLazyLoad(); + + //relayoutNotes(/*animate=*/false); + setFocusedNote(0); // So that during the focusInEvent that will come shortly, the FIRST note is focused. + + if (Settings::playAnimations() && !decoration()->filterBar()->filterData().isFiltering && Global::bnpView->currentBasket() == this) // No animation when filtering all! + animateLoad();//QTimer::singleShot( 0, this, SLOT(animateLoad()) ); + + m_finishLoadOnFirstShow = false; + } +} + +void Basket::load() +{ + // Load only once: + if (m_loadingLaunched) + return; + m_loadingLaunched = true; + +// StopWatch::start(10); + + DEBUG_WIN << "Basket[" + folderName() + "]: Loading..."; + QDomDocument *doc = 0; + QString content; + +// StopWatch::start(0); + + if (loadFromFile(fullPath() + ".basket", &content)) { + doc = new QDomDocument("basket"); + if ( ! doc->setContent(content) ) { + DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to parse XML</font>!"; + delete doc; + doc = 0; + } + } + if(isEncrypted()) + DEBUG_WIN << "Basket is encrypted."; + if ( ! doc) { + DEBUG_WIN << "Basket[" + folderName() + "]: <font color=red>FAILED to load</font>!"; + m_loadingLaunched = false; + if (isEncrypted()) + m_locked = true; + Global::bnpView->notesStateChanged(); // Show "Locked" instead of "Loading..." in the statusbar + return; + } + m_locked = false; + + QDomElement docElem = doc->documentElement(); + QDomElement properties = XMLWork::getElement(docElem, "properties"); + + loadProperties(properties); // Since we are loading, this time the background image will also be loaded! + // Now that the background image is loaded and subscribed, we display it during the load process: + delete doc; + updateContents(); +// kapp->processEvents(); + + //BEGIN Compatibility with 0.6.0 Pre-Alpha versions: + QDomElement notes = XMLWork::getElement(docElem, "notes"); + if (notes.isNull()) + notes = XMLWork::getElement(docElem, "items"); + m_watcher->stopScan(); + m_shouldConvertPlainTextNotes = false; // Convert Pre-0.6.0 baskets: plain text notes should be converted to rich text ones once all is loaded! +// StopWatch::check(0); + +// StopWatch::start(1); + m_finishLoadOnFirstShow = (Global::bnpView->currentBasket() != this); + loadNotes(notes, 0L); +// StopWatch::check(1); +// StopWatch::start(2); + + if (m_shouldConvertPlainTextNotes) + convertTexts(); + m_watcher->startScan(); + //loadNotes(XMLWork::getElement(docElem, "notes"), 0L); + //END + +// StopWatch::check(0); + + signalCountsChanged(); + if (isColumnsLayout()) { + // Count the number of columns: + int columnsCount = 0; + Note *column = firstNote(); + while (column) { + ++columnsCount; + column = column->next(); + } + m_columnsCount = columnsCount; + } + + relayoutNotes(false); + + // On application start, the current basket is not focused yet, so the focus rectangle is not shown when calling focusANote(): + if (Global::bnpView->currentBasket() == this) + setFocus(); + focusANote(); + + if (Settings::playAnimations() && !decoration()->filterBar()->filterData().isFiltering && Global::bnpView->currentBasket() == this) // No animation when filtering all! + animateLoad();//QTimer::singleShot( 0, this, SLOT(animateLoad()) ); + else + m_loaded = true; + enableActions(); +// StopWatch::check(2); + +// StopWatch::check(10); +} + +void Basket::filterAgain(bool andEnsureVisible/* = true*/) +{ + newFilter(decoration()->filterData(), andEnsureVisible); +} + +void Basket::filterAgainDelayed() +{ + QTimer::singleShot( 0, this, SLOT(filterAgain()) ); +} + +void Basket::newFilter(const FilterData &data, bool andEnsureVisible/* = true*/) +{ + if (!isLoaded()) + return; + +//StopWatch::start(20); + + m_countFounds = 0; + for (Note *note = firstNote(); note; note = note->next()) + m_countFounds += note->newFilter(data); + + relayoutNotes(true); + signalCountsChanged(); + + if (hasFocus()) // if (!hasFocus()), focusANote() will be called at focusInEvent() + focusANote(); // so, we avoid de-focus a note if it will be re-shown soon + if (andEnsureVisible && m_focusedNote != 0L) + ensureNoteVisible(m_focusedNote); + + Global::bnpView->setFiltering(data.isFiltering); + +//StopWatch::check(20); +} + +void Basket::cancelFilter() +{ + decoration()->filterBar()->reset(); + validateFilter(); +} + +void Basket::validateFilter() +{ + if (isDuringEdit()) + m_editor->widget()->setFocus(); + else + setFocus(); +} + +bool Basket::isFiltering() +{ + return decoration()->filterBar()->filterData().isFiltering; +} + + + +QString Basket::fullPath() +{ + return Global::basketsFolder() + folderName(); +} + +QString Basket::fullPathForFileName(const QString &fileName) +{ + return fullPath() + fileName; +} + +/*static*/ QString Basket::fullPathForFolderName(const QString &folderName) +{ + return Global::basketsFolder() + folderName; +} + + +void Basket::setShortcut(KShortcut shortcut, int action) +{ + if(!Global::globalAccel) + return; + QString sAction = "global_basket_activate_" + folderName(); + Global::globalAccel->remove(sAction); + Global::globalAccel->updateConnections(); + + m_action->setShortcut(shortcut); + m_shortcutAction = action; + + if (action > 0) + Global::globalAccel->insert(sAction, m_action->text(), /*whatsThis=*/"", m_action->shortcut(), KShortcut(), this, SLOT(activatedShortcut()), /*configurable=*/false); + Global::globalAccel->updateConnections(); +} + +void Basket::activatedShortcut() +{ + Global::bnpView->setCurrentBasket(this); + + if (m_shortcutAction == 1) + Global::bnpView->setActive(true); +} + +void Basket::signalCountsChanged() +{ + if (!m_timerCountsChanged.isActive()) + m_timerCountsChanged.start(0/*ms*/, /*singleShot=*/true); +} + +void Basket::countsChangedTimeOut() +{ + emit countsChanged(this); +} + + +Basket::Basket(QWidget *parent, const QString &folderName) + : QScrollView(parent), + QToolTip(viewport()), + m_noActionOnMouseRelease(false), m_ignoreCloseEditorOnNextMouseRelease(false), m_pressPos(-100, -100), m_canDrag(false), + m_firstNote(0), m_columnsCount(1), m_mindMap(false), m_resizingNote(0L), m_pickedResizer(0), m_movingNote(0L), m_pickedHandle(0, 0), + m_clickedToInsert(0), m_zoneToInsert(0), m_posToInsert(-1, -1), + m_isInsertPopupMenu(false), + m_loaded(false), m_loadingLaunched(false), m_locked(false), m_decryptBox(0), m_button(0), m_encryptionType(NoEncryption), +#ifdef HAVE_LIBGPGME + m_gpg(0), +#endif + m_backgroundPixmap(0), m_opaqueBackgroundPixmap(0), m_selectedBackgroundPixmap(0), + m_action(0), m_shortcutAction(0), + m_hoveredNote(0), m_hoveredZone(Note::None), m_lockedHovering(false), m_underMouse(false), + m_inserterRect(), m_inserterShown(false), m_inserterSplit(true), m_inserterTop(false), m_inserterGroup(false), + m_isSelecting(false), m_selectionStarted(false), + m_count(0), m_countFounds(0), m_countSelecteds(0), + m_folderName(folderName), + m_editor(0), m_leftEditorBorder(0), m_rightEditorBorder(0), m_redirectEditActions(false), m_editorWidth(-1), m_editorHeight(-1), + m_doNotCloseEditor(false), m_editParagraph(0), m_editIndex(0), + m_isDuringDrag(false), m_draggedNotes(), + m_focusedNote(0), m_startOfShiftSelectionNote(0), + m_finishLoadOnFirstShow(false), m_relayoutOnNextShow(false) +{ + QString sAction = "local_basket_activate_" + folderName; + m_action = new KAction("FAKE TEXT", "FAKE ICON", KShortcut(), this, SLOT(activatedShortcut()), Global::bnpView->actionCollection(), sAction); + m_action->setShortcutConfigurable(false); // We do it in the basket properties dialog (and keep it in sync with the global one) + + if (!m_folderName.endsWith("/")) + m_folderName += "/"; + + setFocusPolicy(QWidget::StrongFocus); + setWFlags(Qt::WNoAutoErase); + setDragAutoScroll(true); + + // By default, there is no corner widget: we set one for the corner area to be painted! + // If we don't set one and there are two scrollbars present, slowly resizing up the window show graphical glitches in that area! + m_cornerWidget = new QWidget(this); + setCornerWidget(m_cornerWidget); + + viewport()->setAcceptDrops(true); + viewport()->setMouseTracking(true); + viewport()->setBackgroundMode(NoBackground); // Do not clear the widget before paintEvent() because we always draw every pixels (faster and flicker-free) + + // File Watcher: + m_watcher = new KDirWatch(this); + connect( m_watcher, SIGNAL(dirty(const QString&)), this, SLOT(watchedFileModified(const QString&)) ); + connect( m_watcher, SIGNAL(deleted(const QString&)), this, SLOT(watchedFileDeleted(const QString&)) ); + connect( &m_watcherTimer, SIGNAL(timeout()), this, SLOT(updateModifiedNotes()) ); + + // Various Connections: + connect( &m_animationTimer, SIGNAL(timeout()), this, SLOT(animateObjects()) ); + connect( &m_autoScrollSelectionTimer, SIGNAL(timeout()), this, SLOT(doAutoScrollSelection()) ); + connect( &m_timerCountsChanged, SIGNAL(timeout()), this, SLOT(countsChangedTimeOut()) ); + connect( &m_inactivityAutoSaveTimer, SIGNAL(timeout()), this, SLOT(inactivityAutoSaveTimeout()) ); + connect( &m_inactivityAutoLockTimer, SIGNAL(timeout()), this, SLOT(inactivityAutoLockTimeout()) ); + connect( this, SIGNAL(contentsMoving(int, int)), this, SLOT(contentsMoved()) ); +#ifdef HAVE_LIBGPGME + m_gpg = new KGpgMe(); +#endif + m_locked = isFileEncrypted(); +} + +void Basket::contentsMoved() +{ + // This slot is called BEFORE the content move, so we delay the hover effects: + QTimer::singleShot(0, this, SLOT(doHoverEffects())); +} + +void Basket::enterEvent(QEvent *) +{ + m_underMouse = true; + doHoverEffects(); +} + +void Basket::leaveEvent(QEvent *) +{ + m_underMouse = false; + doHoverEffects(); + + if (m_lockedHovering) + return; + + removeInserter(); + if (m_hoveredNote) { + m_hoveredNote->setHovered(false); + m_hoveredNote->setHoveredZone(Note::None); + updateNote(m_hoveredNote); + } + m_hoveredNote = 0; +} + +void Basket::setFocusIfNotInPopupMenu() +{ + if (!kapp->activePopupWidget()) + if (isDuringEdit()) + m_editor->widget()->setFocus(); + else + setFocus(); +} + +void Basket::contentsMousePressEvent(QMouseEvent *event) +{ + // If user click the basket, focus it! + // The focus is delayed because if the click results in showing a popup menu, + // the interface flicker by showing the focused rectangle (as the basket gets focus) + // and immediatly removing it (because the popup menu now have focus). + if (!isDuringEdit()) + QTimer::singleShot(0, this, SLOT(setFocusIfNotInPopupMenu())); + + // Convenient variables: + bool controlPressed = event->stateAfter() & Qt::ControlButton; + bool shiftPressed = event->stateAfter() & Qt::ShiftButton; + + // Do nothing if we disabled the click some milliseconds sooner. + // For instance when a popup menu has been closed with click, we should not do action: + if (event->button() == Qt::LeftButton && (kapp->activePopupWidget() || m_lastDisableClick.msecsTo(QTime::currentTime()) <= 80)) { + doHoverEffects(); + m_noActionOnMouseRelease = true; + // But we allow to select: + // The code is the same as at the bottom of this method: + if (event->button() == Qt::LeftButton) { + m_selectionStarted = true; + m_selectionBeginPoint = event->pos(); + m_selectionInvert = controlPressed || shiftPressed; + } + return; + } + + // Figure out what is the clicked note and zone: + Note *clicked = noteAt(event->pos().x(), event->pos().y()); + Note::Zone zone = (clicked ? clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ) : Note::None); + + // Popup Tags menu: + if (zone == Note::TagsArrow && !controlPressed && !shiftPressed && event->button() != Qt::MidButton) { + if (!clicked->isSelected()) + unselectAllBut(clicked); + setFocusedNote(clicked); /// /// /// + m_startOfShiftSelectionNote = clicked; + m_noActionOnMouseRelease = true; + popupTagsMenu(clicked); + return; + } + + if (event->button() == Qt::LeftButton) { + // Prepare to allow drag and drop when moving mouse further: + if ( (zone == Note::Handle || zone == Note::Group) || + (clicked && clicked->isSelected() && + (zone == Note::TagsArrow || zone == Note::Custom0 || zone == Note::Content || zone == Note::Link /**/ || zone >= Note::Emblem0 /**/)) ) { + if (!shiftPressed && !controlPressed) { + m_pressPos = event->pos(); // TODO: Allow to drag emblems to assign them to other notes. Then don't allow drag at Emblem0!! + m_canDrag = true; + // Saving where we were editing, because during a drag, the mouse can fly over the text edit and move the cursor position: + if (m_editor && m_editor->textEdit()) { + QTextEdit *editor = m_editor->textEdit(); + editor->getCursorPosition(&m_editParagraph, &m_editIndex); + } + } + } + + // Initializing Resizer move: + if (zone == Note::Resizer) { + m_resizingNote = clicked; + m_pickedResizer = event->pos().x() - clicked->rightLimit(); + m_noActionOnMouseRelease = true; + m_lockedHovering = true; + return; + } + + // Select note(s): + if (zone == Note::Handle || zone == Note::Group || (zone == Note::GroupExpander && (controlPressed || shiftPressed))) { + Note *end = clicked; + if (clicked->isGroup() && shiftPressed) { + if (clicked->contains(m_startOfShiftSelectionNote)) { + m_startOfShiftSelectionNote = clicked->firstRealChild(); + end = clicked->lastRealChild(); + } else if (clicked->firstRealChild()->isAfter(m_startOfShiftSelectionNote)) + end = clicked->lastRealChild(); + else + end = clicked->firstRealChild(); + } + if (controlPressed && shiftPressed) + selectRange(m_startOfShiftSelectionNote, end, /*unselectOthers=*/false); + else if (shiftPressed) + selectRange(m_startOfShiftSelectionNote, end); + else if (controlPressed) + clicked->setSelectedRecursivly(!clicked->allSelected()); + else if (!clicked->allSelected()) + unselectAllBut(clicked); + setFocusedNote(end); /// /// /// + m_startOfShiftSelectionNote = (end->isGroup() ? end->firstRealChild() : end); + //m_noActionOnMouseRelease = false; + m_noActionOnMouseRelease = true; + return; + } + // MOVED TO RELEASE EVENT: + /* else if (clicked && zone != Note::None && zone != Note::BottomColumn && zone != Note::Resizer && (controlPressed || shiftPressed)) { + if (controlPressed && shiftPressed) + selectRange(m_startOfShiftSelectionNote, clicked, / *unselectOthers=* /false); + else if (shiftPressed) + selectRange(m_startOfShiftSelectionNote, clicked); + else if (controlPressed) + clicked->setSelectedRecursivly(!clicked->allSelected()); + setFocusedNote(clicked); /// /// /// + m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); + m_noActionOnMouseRelease = true; + return; + }*/ + + // Initializing Note move: +/* if ((zone == Note::Group || zone == Note::Handle) && clicked->isFree()) { + m_movingNote = clicked; + m_pickedHandle = QPoint(event->pos().x() - clicked->x(), event->pos().y() - clicked->y()); + m_noActionOnMouseRelease = true; + m_lockedHovering = true; + return; + } +*/ + + // Folding/Unfolding group: + if (zone == Note::GroupExpander) { + clicked->toggleFolded(Settings::playAnimations()); + relayoutNotes(true); + m_noActionOnMouseRelease = true; + return; + } + } + + // Popup menu for tag emblems: + if (event->button() == Qt::RightButton && zone >= Note::Emblem0) { + if (!clicked->isSelected()) + unselectAllBut(clicked); + setFocusedNote(clicked); /// /// /// + m_startOfShiftSelectionNote = clicked; + popupEmblemMenu(clicked, zone - Note::Emblem0); + m_noActionOnMouseRelease = true; + return; + } + + // Insertion Popup Menu: + if ( (event->button() == Qt::RightButton) && + ((!clicked && isFreeLayout()) || + (clicked && (zone == Note::TopInsert || zone == Note::TopGroup || zone == Note::BottomInsert || zone == Note::BottomGroup || zone == Note::BottomColumn))) ) { + unselectAll(); + m_clickedToInsert = clicked; + m_zoneToInsert = zone; + m_posToInsert = event->pos(); + KPopupMenu* menu = (KPopupMenu*)(Global::bnpView->popupMenu("insert_popup")); + if (!menu->title(/*id=*/120).isEmpty()) // If we already added a title, remove it because it would be kept and then added several times: + menu->removeItem(/*id=*/120); + menu->insertTitle((zone == Note::TopGroup || zone == Note::BottomGroup ? i18n("The verb (Group New Note)", "Group") : i18n("The verb (Insert New Note)", "Insert")), /*id=*/120, /*index=*/0); + setInsertPopupMenu(); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(delayedCancelInsertPopupMenu()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(hideInsertPopupMenu()) ); + doHoverEffects(clicked, zone); // In the case where another popup menu was open, we should do that manually! + m_lockedHovering = true; + menu->exec(QCursor::pos()); + m_noActionOnMouseRelease = true; + return; + } + + // Note Context Menu: + if (event->button() == Qt::RightButton && clicked && !clicked->isColumn() && zone != Note::Resizer) { + if (!clicked->isSelected()) + unselectAllBut(clicked); + setFocusedNote(clicked); /// /// /// + if (editedNote() == clicked) { + closeEditor(); + clicked->setSelected(true); + } + m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); + QPopupMenu* menu = Global::bnpView->popupMenu("note_popup"); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + doHoverEffects(clicked, zone); // In the case where another popup menu was open, we should do that manually! + m_lockedHovering = true; + menu->exec(QCursor::pos()); + m_noActionOnMouseRelease = true; + return; + } + + // Paste selection under cursor (but not "create new primary note under cursor" because this is on moveRelease): + if (event->button() == Qt::MidButton && zone != Note::Resizer && (!isDuringEdit() || clicked != editedNote())) { + if ((Settings::middleAction() != 0) && (event->state() == Qt::ShiftButton)) { + m_clickedToInsert = clicked; + m_zoneToInsert = zone; + m_posToInsert = event->pos(); + closeEditor(); + removeInserter(); // If clicked at an insertion line and the new note shows a dialog for editing, + NoteType::Id type = (NoteType::Id)0; // hide that inserter before the note edition instead of after the dialog is closed + switch (Settings::middleAction()) { + case 1: + m_isInsertPopupMenu = true; + pasteNote(); + break; + case 2: type = NoteType::Image; break; + case 3: type = NoteType::Link; break; + case 4: type = NoteType::Launcher; break; + default: + m_noActionOnMouseRelease = false; + return; // Other options should be done on mouse release (to avoid mouse release to cancel them!) +/* case 5: type = NoteType::Color; break; + case 6: + Global::bnpView->grabScreenshot(); + break; + case 7: + Global::bnpView->slotColorFromScreen(); + break; + case 8: + Global::bnpView->insertWizard(3); // loadFromFile + break; + case 9: + Global::bnpView->insertWizard(1); // importKMenuLauncher + break; + case 10: + Global::bnpView->insertWizard(2); // importIcon + break; +*/ } + if (type != 0) { + m_ignoreCloseEditorOnNextMouseRelease = true; + Global::bnpView->insertEmpty(type); + } + } else { + if (clicked) + zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()), true ); + closeEditor(); + clickedToInsert(event, clicked, zone); + save(); + } + m_noActionOnMouseRelease = true; + return; + } + + // Finally, no action has been done durint pressEvent, so an action can be done on releaseEvent: + m_noActionOnMouseRelease = false; + + /* Selection scenario: + * On contentsMousePressEvent, put m_selectionStarted to true and init Begin and End selection point. + * On contentsMouseMoveEvent, if m_selectionStarted, update End selection point, update selection rect, + * and if it's larger, switching to m_isSelecting mode: we can draw the selection rectangle. + */ + // Prepare selection: + if (event->button() == Qt::LeftButton) { + m_selectionStarted = true; + m_selectionBeginPoint = event->pos(); + // We usualy invert the selection with the Ctrl key, but some environements (like GNOME or The Gimp) do it with the Shift key. + // Since the Shift key has no specific usage, we allow to invert selection ALSO with Shift for Gimp people + m_selectionInvert = controlPressed || shiftPressed; + } +} + +void Basket::delayedCancelInsertPopupMenu() +{ + QTimer::singleShot( 0, this, SLOT(cancelInsertPopupMenu()) ); +} + +void Basket::contentsContextMenuEvent(QContextMenuEvent *event) +{ + if (event->reason() == QContextMenuEvent::Keyboard) { + if (countFounds/*countShown*/() == 0) { // TODO: Count shown!! + QRect basketRect( mapToGlobal(QPoint(0,0)), size() ); + QPopupMenu *menu = Global::bnpView->popupMenu("insert_popup"); + setInsertPopupMenu(); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(delayedCancelInsertPopupMenu()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + removeInserter(); + m_lockedHovering = true; + PopupMenu::execAtRectCenter(*menu, basketRect); // Popup at center or the basket + } else { + if ( ! m_focusedNote->isSelected() ) + unselectAllBut(m_focusedNote); + setFocusedNote(m_focusedNote); /// /// /// + m_startOfShiftSelectionNote = (m_focusedNote->isGroup() ? m_focusedNote->firstRealChild() : m_focusedNote); + // Popup at bottom (or top) of the focused note, if visible : + QPopupMenu *menu = Global::bnpView->popupMenu("note_popup"); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + doHoverEffects(m_focusedNote, Note::Content); // In the case where another popup menu was open, we should do that manually! + m_lockedHovering = true; + PopupMenu::execAtRectBottom(*menu, noteVisibleRect(m_focusedNote), true); + } + } +} + +QRect Basket::noteVisibleRect(Note *note) +{ + QRect rect( contentsToViewport(QPoint(note->x(), note->y())), QSize(note->width(),note->height()) ); + QPoint basketPoint = mapToGlobal(QPoint(0,0)); + rect.moveTopLeft( rect.topLeft() + basketPoint + QPoint(frameWidth(), frameWidth()) ); + + // Now, rect contain the global note rectangle on the screen. + // We have to clip it by the basket widget : + if (rect.bottom() > basketPoint.y() + visibleHeight() + 1) { // Bottom too... bottom + rect.setBottom( basketPoint.y() + visibleHeight() + 1); + if (rect.height() <= 0) // Have at least one visible pixel of height + rect.setTop(rect.bottom()); + } + if (rect.top() < basketPoint.y() + frameWidth()) { // Top too... top + rect.setTop( basketPoint.y() + frameWidth()); + if (rect.height() <= 0) + rect.setBottom(rect.top()); + } + if (rect.right() > basketPoint.x() + visibleWidth() + 1) { // Right too... right + rect.setRight( basketPoint.x() + visibleWidth() + 1); + if (rect.width() <= 0) // Have at least one visible pixel of width + rect.setLeft(rect.right()); + } + if (rect.left() < basketPoint.x() + frameWidth()) { // Left too... left + rect.setLeft( basketPoint.x() + frameWidth()); + if (rect.width() <= 0) + rect.setRight(rect.left()); + } + + return rect; +} + +void Basket::disableNextClick() +{ + m_lastDisableClick = QTime::currentTime(); +} + +void Basket::recomputeAllStyles() +{ + FOR_EACH_NOTE (note) + note->recomputeAllStyles(); +} + +void Basket::removedStates(const QValueList<State*> &deletedStates) +{ + bool modifiedBasket = false; + + FOR_EACH_NOTE (note) + if (note->removedStates(deletedStates)) + modifiedBasket = true; + + if (modifiedBasket) + save(); +} + +void Basket::insertNote(Note *note, Note *clicked, int zone, const QPoint &pos, bool animateNewPosition) +{ + if (!note) { + std::cout << "Wanted to insert NO note" << std::endl; + return; + } + + if (clicked && zone == Note::BottomColumn) { + // When inserting at the bottom of a column, it's obvious the new note SHOULD inherit tags. + // We ensure that by changing the insertion point after the last note of the column: + Note *last = clicked->lastChild(); + if (last) { + clicked = last; + zone = Note::BottomInsert; + } + } + + /// Insertion at the bottom of a column: + if (clicked && zone == Note::BottomColumn) { + note->setWidth(clicked->rightLimit() - clicked->x()); + Note *lastChild = clicked->lastChild(); + if (!animateNewPosition || !Settings::playAnimations()) + for (Note *n = note; n; n = n->next()) { + n->setXRecursivly(clicked->x()); + n->setYRecursivly((lastChild ? lastChild : clicked)->bottom() + 1); + } + appendNoteIn(note, clicked); + + /// Insertion relative to a note (top/bottom, insert/group): + } else if (clicked) { + note->setWidth(clicked->width()); + if (!animateNewPosition || !Settings::playAnimations()) + for (Note *n = note; n; n = n->next()) { + if (zone == Note::TopGroup || zone == Note::BottomGroup) + n->setXRecursivly(clicked->x() + Note::GROUP_WIDTH); + else + n->setXRecursivly(clicked->x()); + if (zone == Note::TopInsert || zone == Note::TopGroup) + n->setYRecursivly(clicked->y()); + else + n->setYRecursivly(clicked->bottom() + 1); + } + + if (zone == Note::TopInsert) { appendNoteBefore(note, clicked); } + else if (zone == Note::BottomInsert) { appendNoteAfter(note, clicked); } + else if (zone == Note::TopGroup) { groupNoteBefore(note, clicked); } + else if (zone == Note::BottomGroup) { groupNoteAfter(note, clicked); } + + /// Free insertion: + } else if (isFreeLayout()) { + // Group if note have siblings: + if (note->next()) { + Note *group = new Note(this); + for (Note *n = note; n; n = n->next()) + n->setParentNote(group); + group->setFirstChild(note); + note = group; + } + // Insert at cursor position: + const int initialWidth = 250; + note->setWidth(note->isGroup() ? Note::GROUP_WIDTH : initialWidth); + if (note->isGroup() && note->firstChild()) + note->setInitialHeight(note->firstChild()->height()); + //note->setGroupWidth(initialWidth); + if (animateNewPosition && Settings::playAnimations()) + note->setFinalPosition(pos.x(), pos.y()); + else { + note->setXRecursivly(pos.x()); + note->setYRecursivly(pos.y()); + } + appendNoteAfter(note, lastNote()); + } + + relayoutNotes(true); +} + +void Basket::clickedToInsert(QMouseEvent *event, Note *clicked, /*Note::Zone*/int zone) +{ + Note *note; + if (event->button() == Qt::MidButton) + note = NoteFactory::dropNote(KApplication::clipboard()->data(QClipboard::Selection), this); + else + note = NoteFactory::createNoteText("", this); + + if (!note) + return; + + insertNote(note, clicked, zone, event->pos(), /*animateNewPosition=*/false); + +// ensureNoteVisible(lastInsertedNote()); // TODO: in insertNote() + + if (event->button() != Qt::MidButton) { + removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. + closeEditor(); + noteEdit(note, /*justAdded=*/true); + } +} + +void Basket::contentsDragEnterEvent(QDragEnterEvent *event) +{ + m_isDuringDrag = true; + Global::bnpView->updateStatusBarHint(); + if (NoteDrag::basketOf(event) == this) + m_draggedNotes = NoteDrag::notesOf(event); +} + +void Basket::contentsDragMoveEvent(QDragMoveEvent *event) +{ +// m_isDuringDrag = true; + +// if (isLocked()) +// return; + +// FIXME: viewportToContents does NOT work !!! +// QPoint pos = viewportToContents(event->pos()); +// QPoint pos( event->pos().x() + contentsX(), event->pos().y() + contentsY() ); + +// if (insertAtCursorPos()) +// computeInsertPlace(pos); + doHoverEffects(event->pos()); + +// showFrameInsertTo(); + if (isFreeLayout() || noteAt(event->pos().x(), event->pos().y())) // Cursor before rightLimit() or hovering the dragged source notes + acceptDropEvent(event); + else { + event->acceptAction(false); + event->accept(false); + } + +/* Note *hoveredNote = noteAt(event->pos().x(), event->pos().y()); + if ( (isColumnsLayout() && !hoveredNote) || (draggedNotes().contains(hoveredNote)) ) { + event->acceptAction(false); + event->accept(false); + } else + acceptDropEvent(event);*/ + + // A workarround since QScrollView::dragAutoScroll seem to have no effect : +// ensureVisible(event->pos().x() + contentsX(), event->pos().y() + contentsY(), 30, 30); +// QScrollView::dragMoveEvent(event); +} + +void Basket::contentsDragLeaveEvent(QDragLeaveEvent*) +{ +// resetInsertTo(); + m_isDuringDrag = false; + m_draggedNotes.clear(); + m_noActionOnMouseRelease = true; + emit resetStatusBarText(); + doHoverEffects(); +} + +void Basket::contentsDropEvent(QDropEvent *event) +{ + QPoint pos = event->pos(); + std::cout << "Contents Drop Event at position " << pos.x() << ":" << pos.y() << std::endl; + + m_isDuringDrag = false; + emit resetStatusBarText(); + +// if (isLocked()) +// return; + + // Do NOT check the bottom&right borders. + // Because imagine someone drag&drop a big note from the top to the bottom of a big basket (with big vertical scrollbars), + // the note is first removed, and relayoutNotes() compute the new height that is smaller + // Then noteAt() is called for the mouse pointer position, because the basket is now smaller, the cursor is out of boundaries!!! + // Should, of course, not return 0: + Note *clicked = noteAt(event->pos().x(), event->pos().y()); + + if (NoteFactory::movingNotesInTheSameBasket(event, this, event->action()) && event->action() == QDropEvent::Move) { + m_doNotCloseEditor = true; + } + + Note *note = NoteFactory::dropNote( event, this, true, event->action(), dynamic_cast<Note*>(event->source()) ); + + if (note) { + Note::Zone zone = (clicked ? clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()), /*toAdd=*/true ) : Note::None); + bool animateNewPosition = NoteFactory::movingNotesInTheSameBasket(event, this, event->action()); + if (animateNewPosition) { + FOR_EACH_NOTE (n) + n->setOnTop(false); + // FOR_EACH_NOTE_IN_CHUNK(note) + for (Note *n = note; n; n = n->next()) + n->setOnTop(true); + } + + insertNote(note, clicked, zone, event->pos(), animateNewPosition); + + // If moved a note on bottom, contentsHeight has been disminished, then view scrolled up, and we should re-scroll the view down: + ensureNoteVisible(note); + +// if (event->button() != Qt::MidButton) { +// removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. +// } + +// resetInsertTo(); +// doHoverEffects(); called by insertNote() + save(); + } + + m_draggedNotes.clear(); + + m_doNotCloseEditor = false; + // When starting the drag, we saved where we were editing. + // This is because during a drag, the mouse can fly over the text edit and move the cursor position, and even HIDE the cursor. + // So we re-show the cursor, and re-position it at the right place: + if (m_editor && m_editor->textEdit()) { + QTextEdit *editor = m_editor->textEdit(); + editor->setCursorPosition(m_editParagraph, m_editIndex); + } +} + +// handles dropping of a note to basket that is not shown +// (usually through its entry in the basket list) +void Basket::blindDrop(QDropEvent* event) +{ + if (!m_isInsertPopupMenu && redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->paste(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->paste(); + } else { + if (!isLoaded()) { + Global::bnpView->showPassiveLoading(this); + load(); + } + closeEditor(); + unselectAll(); + Note *note = NoteFactory::dropNote( event, this, true, event->action(), + dynamic_cast<Note*>(event->source()) ); + if (note) { + insertCreatedNote(note); + //unselectAllBut(note); + if (Settings::usePassivePopup()) + Global::bnpView->showPassiveDropped(i18n("Dropped to basket <i>%1</i>")); + } + } + save(); +} + +void Basket::insertEmptyNote(int type) +{ + if (!isLoaded()) + load(); + if (isDuringEdit()) + closeEditor(); + Note *note = NoteFactory::createEmptyNote((NoteType::Id)type, this); + insertCreatedNote(note/*, / *edit=* /true*/); + noteEdit(note, /*justAdded=*/true); +} + +void Basket::insertWizard(int type) +{ + saveInsertionData(); + Note *note = 0; + switch (type) { + default: + case 1: note = NoteFactory::importKMenuLauncher(this); break; + case 2: note = NoteFactory::importIcon(this); break; + case 3: note = NoteFactory::importFileContent(this); break; + } + if (!note) + return; + restoreInsertionData(); + insertCreatedNote(note); + unselectAllBut(note); + resetInsertionData(); +} + +void Basket::insertColor(const QColor &color) +{ + Note *note = NoteFactory::createNoteColor(color, this); + restoreInsertionData(); + insertCreatedNote(note); + unselectAllBut(note); + resetInsertionData(); +} + +void Basket::insertImage(const QPixmap &image) +{ + Note *note = NoteFactory::createNoteImage(image, this); + restoreInsertionData(); + insertCreatedNote(note); + unselectAllBut(note); + resetInsertionData(); +} + +void Basket::pasteNote(QClipboard::Mode mode) +{ + if (!m_isInsertPopupMenu && redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->paste(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->paste(); + } else { + if (!isLoaded()) { + Global::bnpView->showPassiveLoading(this); + load(); + } + closeEditor(); + unselectAll(); + Note *note = NoteFactory::dropNote(KApplication::clipboard()->data(mode), this); + if (note) { + insertCreatedNote(note); + //unselectAllBut(note); + } + } +} + +void Basket::insertCreatedNote(Note *note) +{ + // Get the insertion data if the user clicked inside the basket: + Note *clicked = m_clickedToInsert; + int zone = m_zoneToInsert; + QPoint pos = m_posToInsert; + + // If it isn't the case, use the default position: + if (!clicked && (pos.x() < 0 || pos.y() < 0)) { + // Insert right after the focused note: + focusANote(); + if (m_focusedNote) { + clicked = m_focusedNote; + zone = (m_focusedNote->isFree() ? Note::BottomGroup : Note::BottomInsert); + pos = QPoint(m_focusedNote->x(), m_focusedNote->finalBottom()); + // Insert at the end of the last column: + } else if (isColumnsLayout()) { + Note *column = /*(Settings::newNotesPlace == 0 ?*/ firstNote() /*: lastNote())*/; + /*if (Settings::newNotesPlace == 0 && column->firstChild()) { // On Top, if at least one child in the column + clicked = column->firstChild(); + zone = Note::TopInsert; + } else { // On Bottom*/ + clicked = column; + zone = Note::BottomColumn; + /*}*/ + // Insert at free position: + } else { + pos = QPoint(0, 0); + } + } + + insertNote(note, clicked, zone, pos); +// ensureNoteVisible(lastInsertedNote()); + removeInserter(); // Case: user clicked below a column to insert, the note is inserted and doHoverEffects() put a new inserter below. We don't want it. +// resetInsertTo(); + save(); +} + +void Basket::saveInsertionData() +{ + m_savedClickedToInsert = m_clickedToInsert; + m_savedZoneToInsert = m_zoneToInsert; + m_savedPosToInsert = m_posToInsert; +} + +void Basket::restoreInsertionData() +{ + m_clickedToInsert = m_savedClickedToInsert; + m_zoneToInsert = m_savedZoneToInsert; + m_posToInsert = m_savedPosToInsert; +} + +void Basket::resetInsertionData() +{ + m_clickedToInsert = 0; + m_zoneToInsert = 0; + m_posToInsert = QPoint(-1, -1); +} + +void Basket::hideInsertPopupMenu() +{ + QTimer::singleShot( 50/*ms*/, this, SLOT(timeoutHideInsertPopupMenu()) ); +} + +void Basket::timeoutHideInsertPopupMenu() +{ + resetInsertionData(); +} + +void Basket::acceptDropEvent(QDropEvent *event, bool preCond) +{ + // FIXME: Should not accept all actions! Or not all actions (link not supported?!) + event->acceptAction(preCond && 1); + event->accept(preCond); +} + +void Basket::contentsMouseReleaseEvent(QMouseEvent *event) +{ + // Now disallow drag: + m_canDrag = false; + + // Cancel Resizer move: + if (m_resizingNote) { + m_resizingNote = 0; + m_pickedResizer = 0; + m_lockedHovering = false; + doHoverEffects(); + save(); + } + + // Cancel Note move: +/* if (m_movingNote) { + m_movingNote = 0; + m_pickedHandle = QPoint(0, 0); + m_lockedHovering = false; + //doHoverEffects(); + save(); + } +*/ + + // Cancel Selection rectangle: + if (m_isSelecting) { + m_isSelecting = false; + stopAutoScrollSelection(); + resetWasInLastSelectionRect(); + doHoverEffects(); + updateContents(m_selectionRect); + } + m_selectionStarted = false; + + Note *clicked = noteAt(event->pos().x(), event->pos().y()); + Note::Zone zone = (clicked ? clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ) : Note::None); + if ((zone == Note::Handle || zone == Note::Group) && editedNote() && editedNote() == clicked) { + if (m_ignoreCloseEditorOnNextMouseRelease) + m_ignoreCloseEditorOnNextMouseRelease = false; + else { + bool editedNoteStillThere = closeEditor(); + if (editedNoteStillThere) + //clicked->setSelected(true); + unselectAllBut(clicked); + } + } + + + if (event->stateAfter() == 0 && (zone == Note::Group || zone == Note::Handle)) { + closeEditor(); + unselectAllBut(clicked); + } + + + // Do nothing if an action has already been made during mousePressEvent, + // or if user made a selection and canceled it by regressing to a very small rectangle. + if (m_noActionOnMouseRelease) + return; + // We immediatly set it to true, to avoid actions set on mouseRelease if NO mousePress event has been triggered. + // This is the case when a popup menu is shown, and user click to the basket area to close it: + // the menu then receive the mousePress event and the basket area ONLY receive the mouseRelease event. + // Obviously, nothing should be done in this case: + m_noActionOnMouseRelease = true; + + + + if (event->button() == Qt::MidButton && zone != Note::Resizer && (!isDuringEdit() || clicked != editedNote())) { + if ((Settings::middleAction() != 0) && (event->stateAfter() == Qt::ShiftButton)) { + m_clickedToInsert = clicked; + m_zoneToInsert = zone; + m_posToInsert = event->pos(); + closeEditor(); + removeInserter(); // If clicked at an insertion line and the new note shows a dialog for editing, + NoteType::Id type = (NoteType::Id)0; // hide that inserter before the note edition instead of after the dialog is closed + switch (Settings::middleAction()) { + case 5: type = NoteType::Color; break; + case 6: + Global::bnpView->grabScreenshot(); + return; + case 7: + Global::bnpView->slotColorFromScreen(); + return; + case 8: + Global::bnpView->insertWizard(3); // loadFromFile + return; + case 9: + Global::bnpView->insertWizard(1); // importKMenuLauncher + return; + case 10: + Global::bnpView->insertWizard(2); // importIcon + return; + } + if (type != 0) { + m_ignoreCloseEditorOnNextMouseRelease = true; + Global::bnpView->insertEmpty(type); + return; + } + } + } + +// Note *clicked = noteAt(event->pos().x(), event->pos().y()); + if ( ! clicked ) { + if (isFreeLayout() && event->button() == Qt::LeftButton) { + clickedToInsert(event); + save(); + } + return; + } +// Note::Zone zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ); + + // Convenient variables: + bool controlPressed = event->stateAfter() & Qt::ControlButton; + bool shiftPressed = event->stateAfter() & Qt::ShiftButton; + + if (clicked && zone != Note::None && zone != Note::BottomColumn && zone != Note::Resizer && (controlPressed || shiftPressed)) { + if (controlPressed && shiftPressed) + selectRange(m_startOfShiftSelectionNote, clicked, /*unselectOthers=*/false); + else if (shiftPressed) + selectRange(m_startOfShiftSelectionNote, clicked); + else if (controlPressed) + clicked->setSelectedRecursivly(!clicked->allSelected()); + setFocusedNote(clicked); /// /// /// + m_startOfShiftSelectionNote = (clicked->isGroup() ? clicked->firstRealChild() : clicked); + m_noActionOnMouseRelease = true; + return; + } + + // Switch tag states: + if (zone >= Note::Emblem0) { + if (event->button() == Qt::LeftButton) { + int icons = -1; + for (State::List::iterator it = clicked->states().begin(); it != clicked->states().end(); ++it) { + if ( ! (*it)->emblem().isEmpty() ) + icons++; + if (icons == zone - Note::Emblem0) { + State *state = (*it)->nextState(); + if (!state) + return; + it = clicked->states().insert(it, state); + ++it; + clicked->states().remove(it); + clicked->recomputeStyle(); + clicked->unbufferize(); + updateNote(clicked); + updateEditorAppearance(); + filterAgain(); + save(); + break; + } + } + return; + }/* else if (event->button() == Qt::RightButton) { + popupEmblemMenu(clicked, zone - Note::Emblem0); + return; + }*/ + } + + // Insert note or past clipboard: + QString text; +// Note *note; + QString link; + //int zone = zone; + if (event->button() == Qt::MidButton && zone == Note::Resizer) + return; //zone = clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()), true ); + if (event->button() == Qt::RightButton && (clicked->isColumn() || zone == Note::Resizer)) + return; + if (clicked->isGroup() && zone == Note::None) + return; + switch (zone) { + case Note::Handle: + case Note::Group: + // We select note on mousePress if it was unselected or Ctrl is pressed. + // But the user can want to drag select_s_ notes, so it the note is selected, we only select it alone on mouseRelease: + if (event->stateAfter() == 0) { + std::cout << "EXEC" << std::endl; + if ( !(event->stateAfter() & Qt::ControlButton) && clicked->allSelected()) + unselectAllBut(clicked); + if (zone == Note::Handle && isDuringEdit() && editedNote() == clicked) { + closeEditor(); + clicked->setSelected(true); + } + } + break; + + case Note::Custom0: + //unselectAllBut(clicked); + setFocusedNote(clicked); + noteOpen(clicked); + break; + + case Note::GroupExpander: + case Note::TagsArrow: + break; + + case Note::Link: + link = clicked->linkAt(event->pos() - QPoint(clicked->x(), clicked->y())); + if ( ! link.isEmpty() ) { + if (link == "basket-internal-remove-basket") { + // TODO: ask confirmation: "Do you really want to delete the welcome baskets?\n You can re-add them at any time in the Help menu." + Global::bnpView->doBasketDeletion(this); + } else if (link == "basket-internal-import") { + QPopupMenu *menu = Global::bnpView->popupMenu("fileimport"); + menu->exec(event->globalPos()); + } else { + KRun *run = new KRun(link); // open the URL. + run->setAutoDelete(true); + } + break; + } // If there is no link, edit note content + case Note::Content: + closeEditor(); + unselectAllBut(clicked); + noteEdit(clicked, /*justAdded=*/false, event->pos()); + break; + + case Note::TopInsert: + case Note::TopGroup: + case Note::BottomInsert: + case Note::BottomGroup: + case Note::BottomColumn: + clickedToInsert(event, clicked, zone); + save(); + break; + + case Note::None: + default: + KMessageBox::information(viewport(), + i18n("This message should never appear. If it does, this program is buggy! " + "Please report the bug to the developer.")); + break; + } +} + +void Basket::contentsMouseDoubleClickEvent(QMouseEvent *event) +{ + Note *clicked = noteAt(event->pos().x(), event->pos().y()); + Note::Zone zone = (clicked ? clicked->zoneAt( event->pos() - QPoint(clicked->x(), clicked->y()) ) : Note::None); + + if (event->button() == Qt::LeftButton && (zone == Note::Group || zone == Note::Handle)) { + doCopy(CopyToSelection); + m_noActionOnMouseRelease = true; + } else + contentsMousePressEvent(event); +} + +void Basket::contentsMouseMoveEvent(QMouseEvent *event) +{ + // Drag the notes: + if (m_canDrag && (m_pressPos - event->pos()).manhattanLength() > KApplication::startDragDistance()) { + m_canDrag = false; + m_isSelecting = false; // Don't draw selection rectangle ater drag! + m_selectionStarted = false; + NoteSelection *selection = selectedNotes(); + if (selection->firstStacked()) { + QDragObject *d = NoteDrag::dragObject(selection, /*cutting=*/false, /*source=*/this); // d will be deleted by QT + /*bool shouldRemove = */d->drag(); +// delete selection; + + // Never delete because URL is dragged and the file must be available for the extern appliation +// if (shouldRemove && d->target() == 0) // If target is another application that request to remove the note +// emit wantDelete(this); + } + return; + } + + // Moving a Resizer: + if (m_resizingNote) { + int groupWidth = event->pos().x() - m_resizingNote->x() - m_pickedResizer; + int minRight = m_resizingNote->minRight(); + int maxRight = 100 * contentsWidth(); // A big enough value (+infinity) for free layouts. + Note *nextColumn = m_resizingNote->next(); + if (m_resizingNote->isColumn()) { + if (nextColumn) + maxRight = nextColumn->x() + nextColumn->rightLimit() - nextColumn->minRight() - Note::RESIZER_WIDTH; + else + maxRight = contentsWidth(); + } + if (groupWidth > maxRight - m_resizingNote->x()) + groupWidth = maxRight - m_resizingNote->x(); + if (groupWidth < minRight - m_resizingNote->x()) + groupWidth = minRight - m_resizingNote->x(); + int delta = groupWidth - m_resizingNote->groupWidth(); + m_resizingNote->setGroupWidth(groupWidth); + // If resizing columns: + if (m_resizingNote->isColumn()) { + Note *column = m_resizingNote; + if ( (column = column->next()) ) { + // Next columns should not have them X coordinate animated, because it would flicker: + column->setXRecursivly(column->x() + delta); + // And the resizer should resize the TWO sibling columns, and not push the other columns on th right: + column->setGroupWidth(column->groupWidth() - delta); + } + } + relayoutNotes(true); + } + + // Moving a Note: +/* if (m_movingNote) { + int x = event->pos().x() - m_pickedHandle.x(); + int y = event->pos().y() - m_pickedHandle.y(); + if (x < 0) x = 0; + if (y < 0) y = 0; + m_movingNote->setX(x); + m_movingNote->setY(y); + m_movingNote->relayoutAt(x, y, / *animate=* /false); + relayoutNotes(true); + } +*/ + + // Dragging the selection rectangle: + if (m_selectionStarted) + doAutoScrollSelection(); + + doHoverEffects(event->pos()); +} + +void Basket::doAutoScrollSelection() +{ + static const int AUTO_SCROLL_MARGIN = 50; // pixels + static const int AUTO_SCROLL_DELAY = 100; // milliseconds + + QPoint pos = viewport()->mapFromGlobal(QCursor::pos()); + + // Do the selection: + + if (m_isSelecting) + updateContents(m_selectionRect); + + m_selectionEndPoint = viewportToContents(pos); + m_selectionRect = QRect(m_selectionBeginPoint, m_selectionEndPoint).normalize(); + if (m_selectionRect.left() < 0) m_selectionRect.setLeft(0); + if (m_selectionRect.top() < 0) m_selectionRect.setTop(0); + if (m_selectionRect.right() >= contentsWidth()) m_selectionRect.setRight(contentsWidth() - 1); + if (m_selectionRect.bottom() >= contentsHeight()) m_selectionRect.setBottom(contentsHeight() - 1); + + if ( (m_selectionBeginPoint - m_selectionEndPoint).manhattanLength() > QApplication::startDragDistance() ) { + m_isSelecting = true; + selectNotesIn(m_selectionRect, m_selectionInvert); + updateContents(m_selectionRect); + m_noActionOnMouseRelease = true; + } else { + // If the user was selecting but cancel by making the rectangle too small, cancel it really!!! + if (m_isSelecting) { + if (m_selectionInvert) + selectNotesIn(QRect(), m_selectionInvert); + else + unselectAllBut(0); // TODO: unselectAll(); + } + if (m_isSelecting) + resetWasInLastSelectionRect(); + m_isSelecting = false; + stopAutoScrollSelection(); + return; + } + + // Do the auto-scrolling: + // FIXME: It's still flickering + + QRect insideRect(AUTO_SCROLL_MARGIN, AUTO_SCROLL_MARGIN, visibleWidth() - 2*AUTO_SCROLL_MARGIN, visibleHeight() - 2*AUTO_SCROLL_MARGIN); + + int dx = 0; + int dy = 0; + + if (pos.y() < AUTO_SCROLL_MARGIN) + dy = pos.y() - AUTO_SCROLL_MARGIN; + else if (pos.y() > visibleHeight() - AUTO_SCROLL_MARGIN) + dy = pos.y() - visibleHeight() + AUTO_SCROLL_MARGIN; + + if (pos.x() < AUTO_SCROLL_MARGIN) + dx = pos.x() - AUTO_SCROLL_MARGIN; + else if (pos.x() > visibleWidth() - AUTO_SCROLL_MARGIN) + dx = pos.x() - visibleWidth() + AUTO_SCROLL_MARGIN; + + if (dx || dy) { + kapp->sendPostedEvents(); // Do the repaints, because the scrolling will make the area to repaint to be wrong + scrollBy(dx, dy); + if (!m_autoScrollSelectionTimer.isActive()) + m_autoScrollSelectionTimer.start(AUTO_SCROLL_DELAY); + } else + stopAutoScrollSelection(); +} + +void Basket::stopAutoScrollSelection() +{ + m_autoScrollSelectionTimer.stop(); +} + +void Basket::resetWasInLastSelectionRect() +{ + Note *note = m_firstNote; + while (note) { + note->resetWasInLastSelectionRect(); + note = note->next(); + } +} + +void Basket::selectAll() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->selectAll(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->selectAll(); + } else { + // First select all in the group, then in the parent group... + Note *child = m_focusedNote; + Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : 0); + while (parent) { + if (!parent->allSelected()) { + parent->setSelectedRecursivly(true); + return; + } + child = parent; + parent = parent->parentNote(); + } + // Then, select all: + FOR_EACH_NOTE (note) + note->setSelectedRecursivly(true); + } +} + +void Basket::unselectAll() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) { + m_editor->textEdit()->removeSelection(); + selectionChangedInEditor(); // THIS IS NOT EMITED BY Qt!!! + } else if (m_editor->lineEdit()) + m_editor->lineEdit()->deselect(); + } else { + if (countSelecteds() > 0) // Optimisation + FOR_EACH_NOTE (note) + note->setSelectedRecursivly(false); + } +} + +void Basket::invertSelection() +{ + FOR_EACH_NOTE (note) + note->invertSelectionRecursivly(); +} + +void Basket::unselectAllBut(Note *toSelect) +{ + FOR_EACH_NOTE (note) + note->unselectAllBut(toSelect); +} + +void Basket::invertSelectionOf(Note *toSelect) +{ + FOR_EACH_NOTE (note) + note->invertSelectionOf(toSelect); +} + +void Basket::selectNotesIn(const QRect &rect, bool invertSelection, bool unselectOthers /*= true*/) +{ + FOR_EACH_NOTE (note) + note->selectIn(rect, invertSelection, unselectOthers); +} + +void Basket::doHoverEffects() +{ + doHoverEffects( viewportToContents( viewport()->mapFromGlobal(QCursor::pos()) ) ); +} + +void Basket::doHoverEffects(Note *note, Note::Zone zone, const QPoint &pos) +{ + // Inform the old and new hovered note (if any): + Note *oldHoveredNote = m_hoveredNote; + if (note != m_hoveredNote) { + if (m_hoveredNote) { + m_hoveredNote->setHovered(false); + m_hoveredNote->setHoveredZone(Note::None); + updateNote(m_hoveredNote); + } + m_hoveredNote = note; + if (note) + note->setHovered(true); + } + + // If we are hovering a note, compute which zone is hovered and inform the note: + if (m_hoveredNote) { + if (zone != m_hoveredZone || oldHoveredNote != m_hoveredNote) { + m_hoveredZone = zone; + m_hoveredNote->setCursor(zone); + updateNote(m_hoveredNote); + } + m_hoveredNote->setHoveredZone(zone); + // If we are hovering an insert line zone, update this thing: + if (zone == Note::TopInsert || zone == Note::TopGroup || zone == Note::BottomInsert || zone == Note::BottomGroup || zone == Note::BottomColumn) + placeInserter(m_hoveredNote, zone); + else + removeInserter(); + // If we are hovering an embedded link in a rich text element, show the destination in the statusbar: + if (zone == Note::Link) + emit setStatusBarText(m_hoveredNote->linkAt( pos - QPoint(m_hoveredNote->x(), m_hoveredNote->y()) )); + else if (m_hoveredNote->content()) + emit setStatusBarText(m_hoveredNote->content()->statusBarMessage(m_hoveredZone));//resetStatusBarText(); + // If we aren't hovering a note, reset all: + } else { + if (isFreeLayout() && !isSelecting()) + viewport()->setCursor(Qt::CrossCursor); + else + viewport()->unsetCursor(); + m_hoveredZone = Note::None; + removeInserter(); + emit resetStatusBarText(); + } +} + +void Basket::doHoverEffects(const QPoint &pos) +{ +// if (isDuringEdit()) +// viewport()->unsetCursor(); + + // Do we have the right to do hover effects? + if ( ! m_loaded || m_lockedHovering) + return; + + // enterEvent() (mouse enter in the widget) set m_underMouse to true, and leaveEvent() make it false. + // But some times the enterEvent() is not trigerred: eg. when dragging the scrollbar: + // Ending the drag INSIDE the basket area will make NO hoverEffects() because m_underMouse is false. + // User need to leave the area and re-enter it to get effects. + // This hack solve that by dismissing the m_underMouse variable: + bool underMouse = Global::bnpView->currentBasket() == this && QRect(contentsX(), contentsY(), visibleWidth(), visibleHeight()).contains(pos); + + // Don't do hover effects when a popup menu is opened. + // Primarily because the basket area will only receive mouseEnterEvent and mouveLeaveEvent. + // It willn't be noticed of mouseMoveEvent, which would result in a apparently broken application state: + if (kapp->activePopupWidget()) + underMouse = false; + + // Compute which note is hovered: + Note *note = (m_isSelecting || !underMouse ? 0 : noteAt(pos.x(), pos.y())); + Note::Zone zone = (note ? note->zoneAt( pos - QPoint(note->x(), note->y()), isDuringDrag() ) : Note::None); + + // Inform the old and new hovered note (if any) and update the areas: + doHoverEffects(note, zone, pos); +} + +void Basket::mouseEnteredEditorWidget() +{ + if (!m_lockedHovering && !kapp->activePopupWidget()) + doHoverEffects(editedNote(), Note::Content, QPoint()); +} + +void Basket::removeInserter() +{ + if (m_inserterShown) { // Do not hide (and then update/repaint the view) if it is already hidden! + m_inserterShown = false; + updateContents(m_inserterRect); + } +} + +void Basket::placeInserter(Note *note, int zone) +{ + // Remove the inserter: + if (!note) { + removeInserter(); + return; + } + + // Update the old position: + if (inserterShown()) + updateContents(m_inserterRect); + // Some comodities: + m_inserterShown = true; + m_inserterTop = (zone == Note::TopGroup || zone == Note::TopInsert); + m_inserterGroup = (zone == Note::TopGroup || zone == Note::BottomGroup); + // X and width: + int groupIndent = (note->isGroup() ? note->width() : Note::HANDLE_WIDTH); + int x = note->x(); + int width = (note->isGroup() ? note->rightLimit() - note->x() : note->width()); + if (m_inserterGroup) { + x += groupIndent; + width -= groupIndent; + } + m_inserterSplit = (Settings::groupOnInsertionLine() && note && !note->isGroup() && !note->isFree() && !note->isColumn()); +// if (note->isGroup()) +// width = note->rightLimit() - note->x() - (m_inserterGroup ? groupIndent : 0); + // Y: + int y = note->y() - (m_inserterGroup && m_inserterTop ? 1 : 3); + if (!m_inserterTop) + y += (note->isColumn() ? note->finalHeight() : note->height()); + // Assigning result: + m_inserterRect = QRect(x, y, width, 6 - (m_inserterGroup ? 2 : 0)); + // Update the new position: + updateContents(m_inserterRect); +} + +inline void drawLineByRect(QPainter &painter, int x, int y, int width, int height) +{ + painter.drawLine(x, y, x + width - 1, y + height - 1); +} + +void Basket::drawInserter(QPainter &painter, int xPainter, int yPainter) +{ + if (!m_inserterShown) + return; + + QRect rect = m_inserterRect; // For shorter code-lines when drawing! + rect.moveBy(-xPainter, -yPainter); + int lineY = (m_inserterGroup && m_inserterTop ? 0 : 2); + int roundY = (m_inserterGroup && m_inserterTop ? 0 : 1); + + QColor dark = KApplication::palette().active().dark(); + QColor light = dark.light().light(); + if (m_inserterGroup && Settings::groupOnInsertionLine()) + light = Tools::mixColor(light, KGlobalSettings::highlightColor()); + painter.setPen(dark); + // The horizontal line: + //painter.drawRect( rect.x(), rect.y() + lineY, rect.width(), 2); + int width = rect.width() - 4; + drawGradient(&painter, dark, light, rect.x() + 2, rect.y() + lineY, width/2, 2, /*sunken=*/false, /*horz=*/false, /*flat=*/false); + drawGradient(&painter, light, dark, rect.x() + 2 + width/2, rect.y() + lineY, width - width/2, 2, /*sunken=*/false, /*horz=*/false, /*flat=*/false); + // The left-most and right-most edges (biggest vertical lines): + drawLineByRect(painter, rect.x(), rect.y(), 1, (m_inserterGroup ? 4 : 6)); + drawLineByRect(painter, rect.x() + rect.width() - 1, rect.y(), 1, (m_inserterGroup ? 4 : 6)); + // The left and right mid vertical lines: + drawLineByRect(painter, rect.x() + 1, rect.y() + roundY, 1, (m_inserterGroup ? 3 : 4)); + drawLineByRect(painter, rect.x() + rect.width() - 2, rect.y() + roundY, 1, (m_inserterGroup ? 3 : 4)); + // Draw the split as a feedback to know where is the limit between insert and group: + if (m_inserterSplit) { + int noteWidth = rect.width() + (m_inserterGroup ? Note::HANDLE_WIDTH : 0); + int xSplit = rect.x() - (m_inserterGroup ? Note::HANDLE_WIDTH : 0) + noteWidth / 2; + painter.setPen(Tools::mixColor(dark, light)); + painter.drawRect(xSplit - 2, rect.y() + lineY, 4, 2); + painter.setPen(dark); + painter.drawRect(xSplit - 1, rect.y() + lineY, 2, 2); + } +} + +void Basket::maybeTip(const QPoint &pos) +{ + if ( !m_loaded || !Settings::showNotesToolTip() ) + return; + + QString message; + QRect rect; + + QPoint contentPos = viewportToContents(pos); + Note *note = noteAt(contentPos.x(), contentPos.y()); + + if (!note && isFreeLayout()) { + message = i18n("Insert note here\nRight click for more options"); + QRect itRect; + for (QValueList<QRect>::iterator it = m_blankAreas.begin(); it != m_blankAreas.end(); ++it) { + itRect = QRect(0, 0, visibleWidth(), visibleHeight()).intersect(*it); + if (itRect.contains(contentPos)) { + rect = itRect; + rect.moveLeft(rect.left() - contentsX()); + rect.moveTop( rect.top() - contentsY()); + break; + } + } + } else { + if (!note) + return; + + Note::Zone zone = note->zoneAt( contentPos - QPoint(note->x(), note->y()) ); + switch (zone) { + case Note::Resizer: message = (note->isColumn() ? + i18n("Resize those columns") : + (note->isGroup() ? + i18n("Resize this group") : + i18n("Resize this note"))); break; + case Note::Handle: message = i18n("Select or move this note"); break; + case Note::Group: message = i18n("Select or move this group"); break; + case Note::TagsArrow: message = i18n("Assign or remove tags from this note"); + if (note->states().count() > 0) { + message = "<qt><nobr>" + message + "</nobr><br>" + i18n("<b>Assigned Tags</b>: %1"); + QString tagsString = ""; + for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) { + QString tagName = "<nobr>" + Tools::textToHTMLWithoutP((*it)->fullName()) + "</nobr>"; + if (tagsString.isEmpty()) + tagsString = tagName; + else + tagsString = i18n("%1, %2").arg(tagsString, tagName); + } + message = message.arg(tagsString); + } + break; + case Note::Custom0: message = note->content()->zoneTip(zone); break; //"Open this link/Open this file/Open this sound file/Launch this application" + case Note::GroupExpander: message = (note->isFolded() ? + i18n("Expand this group") : + i18n("Collapse this group")); break; + case Note::Link: + case Note::Content: message = note->content()->editToolTipText(); break; + case Note::TopInsert: + case Note::BottomInsert: message = i18n("Insert note here\nRight click for more options"); break; + case Note::TopGroup: message = i18n("Group note with the one below\nRight click for more options"); break; + case Note::BottomGroup: message = i18n("Group note with the one above\nRight click for more options"); break; + case Note::BottomColumn: message = i18n("Insert note here\nRight click for more options"); break; + case Note::None: message = "** Zone NONE: internal error **"; break; + default: + if (zone >= Note::Emblem0) + message = note->stateForEmblemNumber(zone - Note::Emblem0)->fullName(); + else + message = ""; + break; + } + + if (zone == Note::Content || zone == Note::Link || zone == Note::Custom0) { + QStringList keys; + QStringList values; + + note->content()->toolTipInfos(&keys, &values); + keys.append(i18n("Added")); + keys.append(i18n("Last Modification")); + values.append(note->addedStringDate()); + values.append(note->lastModificationStringDate()); + + message = "<qt><nobr>" + message; + QStringList::iterator key; + QStringList::iterator value; + for (key = keys.begin(), value = values.begin(); key != keys.end() && value != values.end(); ++key, ++value) + message += "<br>" + i18n("of the form 'key: value'", "<b>%1</b>: %2").arg(*key, *value); + message += "</nobr></qt>"; + } else if (m_inserterSplit && (zone == Note::TopInsert || zone == Note::BottomInsert)) + message += "\n" + i18n("Click on the right to group instead of insert"); + else if (m_inserterSplit && (zone == Note::TopGroup || zone == Note::BottomGroup)) + message += "\n" + i18n("Click on the left to insert instead of group"); + + rect = note->zoneRect( zone, contentPos - QPoint(note->x(), note->y()) ); + + rect.moveLeft(rect.left() - contentsX()); + rect.moveTop( rect.top() - contentsY()); + + rect.moveLeft(rect.left() + note->x()); + rect.moveTop( rect.top() + note->y()); + } + + tip(rect, message); +} + +Note* Basket::lastNote() +{ + Note *note = firstNote(); + while (note && note->next()) + note = note->next(); + return note; +} + +void Basket::deleteNotes() +{ + Note *note = m_firstNote; + + while (note){ + Note *tmp = note->next(); + delete note; + note = tmp; + } + m_firstNote = 0; + m_resizingNote = 0; + m_movingNote = 0; + m_focusedNote = 0; + m_startOfShiftSelectionNote = 0; + m_tagPopupNote = 0; + m_clickedToInsert = 0; + m_savedClickedToInsert = 0; + m_hoveredNote = 0; + m_count = 0; + m_countFounds = 0; + m_countSelecteds = 0; + + emit resetStatusBarText(); + emit countsChanged(this); +} + +Note* Basket::noteAt(int x, int y) +{ +//NO: +// // Do NOT check the bottom&right borders. +// // Because imagine someone drag&drop a big note from the top to the bottom of a big basket (with big vertical scrollbars), +// // the note is first removed, and relayoutNotes() compute the new height that is smaller +// // Then noteAt() is called for the mouse pointer position, because the basket is now smaller, the cursor is out of boundaries!!! +// // Should, of course, not return 0: + if (x < 0 || x > contentsWidth() || y < 0 || y > contentsHeight()) + return 0; + + // When resizing a note/group, keep it highlighted: + if (m_resizingNote) + return m_resizingNote; + + // Search and return the hovered note: + Note *note = m_firstNote; + Note *possibleNote; + while (note) { + possibleNote = note->noteAt(x, y); + if (possibleNote) { + if (draggedNotes().contains(possibleNote)) + return 0; + else + return possibleNote; + } + note = note->next(); + } + + // If the basket is layouted in columns, return one of the columns to be able to add notes in them: + if (isColumnsLayout()) { + Note *column = m_firstNote; + while (column) { + if (x >= column->x() && x < column->rightLimit()) + return column; + column = column->next(); + } + } + + // Nothing found, no note is hovered: + return NULL; +} + +Basket::~Basket() +{ + //delete m_action; + if(m_decryptBox) + delete m_decryptBox; +#ifdef HAVE_LIBGPGME + delete m_gpg; +#endif + deleteNotes(); +} + +void Basket::viewportResizeEvent(QResizeEvent *event) +{ + relayoutNotes(true); + //cornerWidget()->setShown(horizontalScrollBar()->isShown() && verticalScrollBar()->isShown()); + if (horizontalScrollBar()->isShown() && verticalScrollBar()->isShown()) { + if (!cornerWidget()) + setCornerWidget(m_cornerWidget); + } else { + if (cornerWidget()) + setCornerWidget(0); + } +// if (isDuringEdit()) +// ensureNoteVisible(editedNote()); + QScrollView::viewportResizeEvent(event); +} + +void Basket::animateLoad() +{ + const int viewHeight = contentsY() + visibleHeight(); + + QTime t = QTime::currentTime(); // Set random seed + srand(t.hour()*12 + t.minute()*60 + t.second()*60); + + Note *note = firstNote(); + while (note) { + if ((note->finalY() < viewHeight) && note->matching()) + note->initAnimationLoad(); + note = note->next(); + } + + m_loaded = true; +} + +QColor Basket::selectionRectInsideColor() +{ + return Tools::mixColor(Tools::mixColor(backgroundColor(), KGlobalSettings::highlightColor()), backgroundColor()); +} + +QColor alphaBlendColors(const QColor &bgColor, const QColor &fgColor, const int a) +{ + // normal button... + QRgb rgb = bgColor.rgb(); + QRgb rgb_b = fgColor.rgb(); + int alpha = a; + if (alpha>255) alpha = 255; + if (alpha<0) alpha = 0; + int inv_alpha = 255 - alpha; + QColor result = QColor( qRgb(qRed(rgb_b)*inv_alpha/255 + qRed(rgb)*alpha/255, + qGreen(rgb_b)*inv_alpha/255 + qGreen(rgb)*alpha/255, + qBlue(rgb_b)*inv_alpha/255 + qBlue(rgb)*alpha/255) ); + + return result; +} + +void Basket::unlock() +{ + QTimer::singleShot( 0, this, SLOT(load()) ); +} + +void Basket::inactivityAutoLockTimeout() +{ + lock(); +} + +void Basket::drawContents(QPainter *painter, int clipX, int clipY, int clipWidth, int clipHeight) +{ + // Start the load the first time the basket is shown: + if (!m_loadingLaunched) + { + if(!m_locked) + QTimer::singleShot( 0, this, SLOT(load()) ); + else { + Global::bnpView->notesStateChanged(); // Show "Locked" instead of "Loading..." in the statusbar + } + } + + QBrush brush(backgroundColor()); // FIXME: share it for all the basket? + QRect clipRect(clipX, clipY, clipWidth, clipHeight); + + if(m_locked) + { + if(!m_decryptBox) + { + m_decryptBox = new QFrame( this, "m_decryptBox" ); + m_decryptBox->setFrameShape( QFrame::StyledPanel ); + m_decryptBox->setFrameShadow( QFrame::Plain ); + m_decryptBox->setLineWidth( 1 ); + + QGridLayout* layout = new QGridLayout( m_decryptBox, 1, 1, 11, 6, "decryptBoxLayout"); + +#ifdef HAVE_LIBGPGME + m_button = new QPushButton( m_decryptBox, "button" ); + m_button->setText( i18n( "&Unlock" ) ); + layout->addWidget( m_button, 1, 2 ); + connect( m_button, SIGNAL( clicked() ), this, SLOT( unlock() ) ); +#endif + QLabel* label = new QLabel( m_decryptBox, "label" ); + QString text = "<b>" + i18n("Password protected basket.") + "</b><br/>"; +#ifdef HAVE_LIBGPGME + label->setText( text + i18n("Press Unlock to access it.") ); +#else + label->setText( text + i18n("Encryption is not supported by<br/>this version of %1.").arg(kapp->aboutData()->programName()) ); +#endif + label->setAlignment( int( QLabel::AlignTop ) ); + layout->addMultiCellWidget( label, 0, 0, 1, 2 ); + QLabel* pixmap = new QLabel( m_decryptBox, "pixmap" ); + pixmap->setPixmap( KGlobal::iconLoader()->loadIcon("encrypted", KIcon::NoGroup, KIcon::SizeHuge) ); + layout->addMultiCellWidget( pixmap, 0, 1, 0, 0 ); + + QSpacerItem* spacer = new QSpacerItem( 40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum ); + layout->addItem( spacer, 1, 1 ); + + label = new QLabel("<small>" + + i18n("To make baskets stay unlocked, change the automatic<br>" + "locking duration in the application settings.") + "</small>", + m_decryptBox); + //label->setFixedWidth(label->sizeHint().width() / 2); + label->setAlignment( int( QLabel::AlignTop ) ); + layout->addMultiCellWidget( label, 2,2,0,2 ); + + m_decryptBox->resize(layout->sizeHint()); + } + if(m_decryptBox->isHidden()) + { + m_decryptBox->show(); + } +#ifdef HAVE_LIBGPGME + m_button->setFocus(); +#endif + m_decryptBox->move((visibleWidth() - m_decryptBox->width()) / 2, + (visibleHeight() - m_decryptBox->height()) / 2); + } + else + { + if(m_decryptBox && m_decryptBox->isShown()) + m_decryptBox->hide(); + } + + // Draw notes (but not when it's not loaded or locked yet): + Note *note = ((m_loaded || m_locked) ? m_firstNote : 0); + while (note) { + note->draw(painter, clipRect); + note = note->next(); + } + enableActions(); + + // Draw loading message: + if (!m_loaded) { + QPixmap pixmap(visibleWidth(), visibleHeight()); // TODO: Clip it to asked size only! + QPainter painter2(&pixmap); + QSimpleRichText rt(QString("<center>%1</center>").arg(i18n("Loading...")), QScrollView::font()); + rt.setWidth(visibleWidth()); + int hrt = rt.height(); + painter2.fillRect(0, 0, visibleWidth(), visibleHeight(), brush); + blendBackground(painter2, QRect(0, 0, visibleWidth(), visibleHeight()), -1, -1, /*opaque=*/true); + QColorGroup cg = colorGroup(); + cg.setColor(QColorGroup::Text, textColor()); + rt.draw(&painter2, 0, (visibleHeight() - hrt) / 2, QRect(), cg); + painter2.end(); + painter->drawPixmap(0, 0, pixmap); + return; // TODO: Clip to the wanted rectangle + } + + // We will draw blank areas below. + // For each rectangle to be draw there is three ways to do so: + // - The rectangle is full of the background COLOR => we fill a rect directly on screen + // - The rectangle is full of the background PIXMAP => we draw it directly on screen (we draw m_opaqueBackgroundPixmap that is not transparent) + // - The rectangle contains the resizer => We draw it on an offscreen buffer and then paint the buffer on screen + // If the background image is not tiled, we know that recomputeBlankRects() broken rects so that they are full of either background pixmap or color, but not a mix. + + // Draw blank areas (see the last preparation above): + QPixmap pixmap; + QPainter painter2; + QRect rect; + for (QValueList<QRect>::iterator it = m_blankAreas.begin(); it != m_blankAreas.end(); ++it) { + rect = clipRect.intersect(*it); + if (rect.width() > 0 && rect.height() > 0) { + // If there is an inserter to draw, draw the image off screen, + // apply the inserter and then draw the image on screen: + if ( (inserterShown() && rect.intersects(inserterRect())) || (m_isSelecting && rect.intersects(m_selectionRect)) ) { + pixmap.resize(rect.width(), rect.height()); + painter2.begin(&pixmap); + painter2.fillRect(0, 0, rect.width(), rect.height(), backgroundColor()); + blendBackground(painter2, rect, -1, -1, /*opaque=*/true); + // Draw inserter: + if (inserterShown() && rect.intersects(inserterRect())) + drawInserter(painter2, rect.x(), rect.y()); + // Draw selection rect: + if (m_isSelecting && rect.intersects(m_selectionRect)) { + QRect selectionRect = m_selectionRect; + selectionRect.moveBy(-rect.x(), -rect.y()); + QRect selectionRectInside(selectionRect.x() + 1, selectionRect.y() + 1, selectionRect.width() - 2, selectionRect.height() - 2); + if (selectionRectInside.width() > 0 && selectionRectInside.height() > 0) { + QColor insideColor = selectionRectInsideColor(); + painter2.fillRect(selectionRectInside, insideColor); + selectionRectInside.moveBy(rect.x(), rect.y()); + blendBackground(painter2, selectionRectInside, rect.x(), rect.y(), true, /*&*/m_selectedBackgroundPixmap); + } + painter2.setPen(KGlobalSettings::highlightColor().dark()); + painter2.drawRect(selectionRect); + painter2.setPen(Tools::mixColor(KGlobalSettings::highlightColor().dark(), backgroundColor())); + painter2.drawPoint(selectionRect.topLeft()); + painter2.drawPoint(selectionRect.topRight()); + painter2.drawPoint(selectionRect.bottomLeft()); + painter2.drawPoint(selectionRect.bottomRight()); + } + painter2.end(); + painter->drawPixmap(rect.x(), rect.y(), pixmap); + // If it's only a blank rectangle to draw, draw it directly on screen (faster!!!): + } else if ( ! hasBackgroundImage() ) { + painter->fillRect(rect, backgroundColor()); + // It's either a background pixmap to draw or a background color to fill: + } else { + if (isTiledBackground() || (rect.x() < backgroundPixmap()->width() && rect.y() < backgroundPixmap()->height())) + blendBackground(*painter, rect, 0, 0, /*opaque=*/true); + else + painter->fillRect(rect, backgroundColor()); + } + } + } +} + +/* rect(x,y,width,height)==(xBackgroundToDraw,yBackgroundToDraw,widthToDraw,heightToDraw) + */ +void Basket::blendBackground(QPainter &painter, const QRect &rect, int xPainter, int yPainter, bool opaque, QPixmap *bg) +{ + if (xPainter == -1 && yPainter == -1) { + xPainter = rect.x(); + yPainter = rect.y(); + } + + if (hasBackgroundImage()) { + const QPixmap *bgPixmap = (bg ? /* * */bg : (opaque ? m_opaqueBackgroundPixmap : m_backgroundPixmap)); + if (isTiledBackground()) + painter.drawTiledPixmap(rect.x() - xPainter, rect.y() - yPainter, rect.width(), rect.height(), *bgPixmap, rect.x(), rect.y()); + else + painter.drawPixmap(rect.x() - xPainter, rect.y() - yPainter, *bgPixmap, rect.x(), rect.y(), rect.width(), rect.height()); + } +} + +void Basket::recomputeBlankRects() +{ + m_blankAreas.clear(); + m_blankAreas.append( QRect(0, 0, contentsWidth(), contentsHeight()) ); + + FOR_EACH_NOTE (note) + note->recomputeBlankRects(m_blankAreas); + + // See the drawing of blank areas in Basket::drawContents() + if (hasBackgroundImage() && ! isTiledBackground()) + substractRectOnAreas( QRect(0, 0, backgroundPixmap()->width(), backgroundPixmap()->height()), m_blankAreas, false ); +} + +void Basket::addAnimatedNote(Note *note) +{ + if (m_animatedNotes.isEmpty()) { + m_animationTimer.start(FRAME_DELAY); + m_lastFrameTime = QTime::currentTime(); + } + + m_animatedNotes.append(note); +} + +void Basket::unsetNotesWidth() +{ + Note *note = m_firstNote; + while (note) { + note->unsetWidth(); + note = note->next(); + } +} + +void Basket::relayoutNotes(bool animate) +{ + if (Global::bnpView->currentBasket() != this) + return; // Optimize load time, and basket will be relaid out when activated, anyway + + if (!Settings::playAnimations()) + animate = false; + + if (!animate) { + m_animatedNotes.clear(); + m_animationTimer.stop(); + } + + int h = 0; + tmpWidth = 0; + tmpHeight = 0; + Note *note = m_firstNote; + while (note) { + if (note->matching()) { + note->relayoutAt(0, h, animate); + if (note->hasResizer()) { + int minGroupWidth = note->minRight() - note->finalX(); + if (note->groupWidth() < minGroupWidth) { + note->setGroupWidth(minGroupWidth); + relayoutNotes(animate); // Redo the thing, but this time it should not recurse + return; + } + } + h += note->finalHeight(); + } + note = note->next(); + } + + if (isFreeLayout()) + tmpHeight += 100; + else + tmpHeight += 15; + + resizeContents( QMAX(tmpWidth, visibleWidth()), QMAX(tmpHeight, visibleHeight()) ); + recomputeBlankRects(); + placeEditor(); + doHoverEffects(); + updateContents(); +} + +void Basket::updateNote(Note *note) +{ + updateContents(note->rect()); + if (note->hasResizer()) + updateContents(note->resizerRect()); +} + +void Basket::animateObjects() +{ + QValueList<Note*>::iterator it; + for (it = m_animatedNotes.begin(); it != m_animatedNotes.end(); ) +// if ((*it)->y() >= contentsY() && (*it)->bottom() <= contentsY() + contentsWidth()) +// updateNote(*it); + if ((*it)->advance()) + it = m_animatedNotes.remove(it); + else { +// if ((*it)->y() >= contentsY() && (*it)->bottom() <= contentsY() + contentsWidth()) +// updateNote(*it); + ++it; + } + + if (m_animatedNotes.isEmpty()) { + // Stop animation timer: + m_animationTimer.stop(); + // Reset all onTop notes: + Note* note = m_firstNote; + while (note) { + note->setOnTop(false); + note = note->next(); + } + } + + if (m_focusedNote) + ensureNoteVisible(m_focusedNote); + + // We refresh content if it was the last frame, + // or if the drawing of the last frame was not too long. + if (!m_animationTimer.isActive() || (m_lastFrameTime.msecsTo(QTime::currentTime()) < FRAME_DELAY*11/10)) { // *11/10 == *1.1 : We keep a 0.1 security margin + m_lastFrameTime = m_lastFrameTime.addMSecs(FRAME_DELAY); // because timers are not accurate and can trigger late + //m_lastFrameTime = QTime::currentTime(); +//std::cout << ">>" << m_lastFrameTime.toString("hh:mm:ss.zzz") << std::endl; + if (m_underMouse) + doHoverEffects(); + recomputeBlankRects(); + //relayoutNotes(true); // In case an animated note was to the contents view boundaries, resize the view! + updateContents(); + // If the drawing of the last frame was too long, we skip the drawing of the current and do the next one: + } else { + m_lastFrameTime = m_lastFrameTime.addMSecs(FRAME_DELAY); +//std::cout << "+=" << m_lastFrameTime.toString("hh:mm:ss.zzz") << std::endl; + animateObjects(); + } + + doHoverEffects(); + placeEditor(); + +/* int delta = m_deltaY / 3; + if (delta == 0 && m_deltaY != 0) + delta = (m_deltaY > 0 ? 1 : -1); + m_deltaY -= delta; + resizeContents(contentsWidth(), contentsHeight() + delta); //m_lastNote->y() + m_lastNote->height() +*/ +} + +void Basket::popupEmblemMenu(Note *note, int emblemNumber) +{ + m_tagPopupNote = note; + State *state = note->stateForEmblemNumber(emblemNumber); + State *nextState = state->nextState(/*cycle=*/false); + Tag *tag = state->parentTag(); + m_tagPopup = tag; + + QKeySequence sequence = tag->shortcut().operator QKeySequence(); + bool sequenceOnDelete = (nextState == 0 && !tag->shortcut().isNull()); + + KPopupMenu menu(this); + if (tag->countStates() == 1) { + menu.insertTitle(/*SmallIcon(state->icon()), */tag->name()); + menu.insertItem( SmallIconSet("editdelete"), i18n("&Remove"), 1 ); + menu.insertItem( SmallIconSet("configure"), i18n("&Customize..."), 2 ); + menu.insertSeparator(); + menu.insertItem( SmallIconSet("filter"), i18n("&Filter by this Tag"), 3 ); + } else { + menu.insertTitle(tag->name()); + QValueList<State*>::iterator it; + State *currentState; + + int i = 10; + for (it = tag->states().begin(); it != tag->states().end(); ++it) { + currentState = *it; + QKeySequence sequence; + if (currentState == nextState && !tag->shortcut().isNull()) + sequence = tag->shortcut().operator QKeySequence(); + menu.insertItem(StateMenuItem::radioButtonIconSet(state == currentState, menu.colorGroup()), new StateMenuItem(currentState, sequence, false), i ); + if (currentState == nextState && !tag->shortcut().isNull()) + menu.setAccel(sequence, i); + ++i; + } + menu.insertSeparator(); + menu.insertItem( new IndentedMenuItem(i18n("&Remove"), "editdelete", (sequenceOnDelete ? sequence : QKeySequence())), 1 ); + menu.insertItem( new IndentedMenuItem(i18n("&Customize..."), "configure"), 2 ); + menu.insertSeparator(); + menu.insertItem( new IndentedMenuItem(i18n("&Filter by this Tag"), "filter"), 3 ); + menu.insertItem( new IndentedMenuItem(i18n("Filter by this &State"), "filter"), 4 ); + } + if (sequenceOnDelete) + menu.setAccel(sequence, 1); + + connect( &menu, SIGNAL(activated(int)), this, SLOT(toggledStateInMenu(int)) ); + connect( &menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); + connect( &menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + + m_lockedHovering = true; + menu.exec(QCursor::pos()); +} + +void Basket::toggledStateInMenu(int id) +{ + if (id == 1) { + removeTagFromSelectedNotes(m_tagPopup); + //m_tagPopupNote->removeTag(m_tagPopup); + //m_tagPopupNote->setWidth(0); // To force a new layout computation + updateEditorAppearance(); + filterAgain(); + save(); + return; + } + if (id == 2) { // Customize this State: + TagsEditDialog dialog(this, m_tagPopupNote->stateOfTag(m_tagPopup)); + dialog.exec(); + return; + } + if (id == 3) { // Filter by this Tag + decoration()->filterBar()->filterTag(m_tagPopup); + return; + } + if (id == 4) { // Filter by this State + decoration()->filterBar()->filterState(m_tagPopupNote->stateOfTag(m_tagPopup)); + return; + } + + /*addStateToSelectedNotes*/changeStateOfSelectedNotes(m_tagPopup->states()[id - 10] /*, orReplace=true*/); + //m_tagPopupNote->addState(m_tagPopup->states()[id - 10], true); + filterAgain(); + save(); +} + +State* Basket::stateForTagFromSelectedNotes(Tag *tag) +{ + State *state = 0; + + FOR_EACH_NOTE (note) + if (note->stateForTagFromSelectedNotes(tag, &state) && state == 0) + return 0; + return state; +} + +void Basket::activatedTagShortcut(Tag *tag) +{ + // Compute the next state to set: + State *state = stateForTagFromSelectedNotes(tag); + if (state) + state = state->nextState(/*cycle=*/false); + else + state = tag->states().first(); + + // Set or unset it: + if (state) { + FOR_EACH_NOTE (note) + note->addStateToSelectedNotes(state, /*orReplace=*/true); + updateEditorAppearance(); + } else + removeTagFromSelectedNotes(tag); + + filterAgain(); + save(); +} + +void Basket::popupTagsMenu(Note *note) +{ + m_tagPopupNote = note; + + KPopupMenu menu(this); + menu.insertTitle(i18n("Tags")); +// QValueList<Tag*>::iterator it; +// Tag *currentTag; +// State *currentState; +// int i = 10; +// for (it = Tag::all.begin(); it != Tag::all.end(); ++it) { +// // Current tag and first state of it: +// currentTag = *it; +// currentState = currentTag->states().first(); +// QKeySequence sequence; +// if (!currentTag->shortcut().isNull()) +// sequence = currentTag->shortcut().operator QKeySequence(); +// menu.insertItem(StateMenuItem::checkBoxIconSet(note->hasTag(currentTag), menu.colorGroup()), new StateMenuItem(currentState, sequence, true), i ); +// if (!currentTag->shortcut().isNull()) +// menu.setAccel(sequence, i); +// ++i; +// } +// +// menu.insertSeparator(); +// // menu.insertItem( /*SmallIconSet("editdelete"),*/ "&Assign New Tag...", 1 ); +// //id = menu.insertItem( SmallIconSet("editdelete"), "&Remove All", -2 ); +// //if (note->states().isEmpty()) +// // menu.setItemEnabled(id, false); +// // menu.insertItem( SmallIconSet("configure"), "&Customize...", 3 ); +// menu.insertItem( new IndentedMenuItem(i18n("&Assign New Tag...")), 1 ); +// menu.insertItem( new IndentedMenuItem(i18n("&Remove All"), "editdelete"), 2 ); +// menu.insertItem( new IndentedMenuItem(i18n("&Customize..."), "configure"), 3 ); +// +// if (!selectedNotesHaveTags())//note->states().isEmpty()) +// menu.setItemEnabled(2, false); +// +// connect( &menu, SIGNAL(activated(int)), this, SLOT(toggledTagInMenu(int)) ); +// connect( &menu, SIGNAL(aboutToHide()), this, SLOT(unlockHovering()) ); +// connect( &menu, SIGNAL(aboutToHide()), this, SLOT(disableNextClick()) ); + + Global::bnpView->populateTagsMenu(menu, note); + + m_lockedHovering = true; + menu.exec(QCursor::pos()); +} + +void Basket::unlockHovering() +{ + m_lockedHovering = false; + doHoverEffects(); +} + +void Basket::toggledTagInMenu(int id) +{ + if (id == 1) { // Assign new Tag... + TagsEditDialog dialog(this, /*stateToEdit=*/0, /*addNewTag=*/true); + dialog.exec(); + if (!dialog.addedStates().isEmpty()) { + State::List states = dialog.addedStates(); + for (State::List::iterator itState = states.begin(); itState != states.end(); ++itState) + FOR_EACH_NOTE (note) + note->addStateToSelectedNotes(*itState); + updateEditorAppearance(); + filterAgain(); + save(); + } + return; + } + if (id == 2) { // Remove All + removeAllTagsFromSelectedNotes(); + filterAgain(); + save(); + return; + } + if (id == 3) { // Customize... + TagsEditDialog dialog(this); + dialog.exec(); + return; + } + + Tag *tag = Tag::all[id - 10]; + if (!tag) + return; + + if (m_tagPopupNote->hasTag(tag)) + removeTagFromSelectedNotes(tag); + else + addTagToSelectedNotes(tag); + m_tagPopupNote->setWidth(0); // To force a new layout computation + filterAgain(); + save(); +} + +void Basket::addTagToSelectedNotes(Tag *tag) +{ + FOR_EACH_NOTE (note) + note->addTagToSelectedNotes(tag); + updateEditorAppearance(); +} + +void Basket::removeTagFromSelectedNotes(Tag *tag) +{ + FOR_EACH_NOTE (note) + note->removeTagFromSelectedNotes(tag); + updateEditorAppearance(); +} + +void Basket::addStateToSelectedNotes(State *state) +{ + FOR_EACH_NOTE (note) + note->addStateToSelectedNotes(state); + updateEditorAppearance(); +} + +void Basket::updateEditorAppearance() +{ + if (isDuringEdit() && m_editor->widget()) { + m_editor->widget()->setFont(m_editor->note()->font()); + m_editor->widget()->setPaletteBackgroundColor(m_editor->note()->backgroundColor()); + m_editor->widget()->setPaletteForegroundColor(m_editor->note()->textColor()); + + // Uggly Hack arround Qt bugs: placeCursor() don't call any signal: + HtmlEditor *htmlEditor = dynamic_cast<HtmlEditor*>(m_editor); + if (htmlEditor) { + int para, index; + m_editor->textEdit()->getCursorPosition(¶, &index); + if (para == 0 && index == 0) { + m_editor->textEdit()->moveCursor(QTextEdit::MoveForward, /*select=*/false); + m_editor->textEdit()->moveCursor(QTextEdit::MoveBackward, /*select=*/false); + } else { + m_editor->textEdit()->moveCursor(QTextEdit::MoveBackward, /*select=*/false); + m_editor->textEdit()->moveCursor(QTextEdit::MoveForward, /*select=*/false); + } + htmlEditor->cursorPositionChanged(); // Does not work anyway :-( (when clicking on a red bold text, the toolbar still show black normal text) + } + } +} + +void Basket::editorPropertiesChanged() +{ + if (isDuringEdit() && m_editor->note()->content()->type() == NoteType::Html) { + m_editor->textEdit()->setAutoFormatting(Settings::autoBullet() ? QTextEdit::AutoAll : QTextEdit::AutoNone); + } +} + +void Basket::changeStateOfSelectedNotes(State *state) +{ + FOR_EACH_NOTE (note) + note->changeStateOfSelectedNotes(state); + updateEditorAppearance(); +} + +void Basket::removeAllTagsFromSelectedNotes() +{ + FOR_EACH_NOTE (note) + note->removeAllTagsFromSelectedNotes(); + updateEditorAppearance(); +} + +bool Basket::selectedNotesHaveTags() +{ + FOR_EACH_NOTE (note) + if (note->selectedNotesHaveTags()) + return true; + return false; +} + +QColor Basket::backgroundColor() +{ + if (m_backgroundColorSetting.isValid()) + return m_backgroundColorSetting; + else + return KGlobalSettings::baseColor(); +} + +QColor Basket::textColor() +{ + if (m_textColorSetting.isValid()) + return m_textColorSetting; + else + return KGlobalSettings::textColor(); +} + +void Basket::unbufferizeAll() +{ + FOR_EACH_NOTE (note) + note->unbufferizeAll(); +} + +Note* Basket::editedNote() +{ + if (m_editor) + return m_editor->note(); + else + return 0; +} + +bool Basket::hasTextInEditor() +{ + if (!isDuringEdit() || !redirectEditActions()) + return false; + + if (m_editor->textEdit()) + return ! m_editor->textEdit()->text().isEmpty(); + else if (m_editor->lineEdit()) + return ! m_editor->lineEdit()->text().isEmpty(); + else + return false; +} + +bool Basket::hasSelectedTextInEditor() +{ + if (!isDuringEdit() || !redirectEditActions()) + return false; + + if (m_editor->textEdit()) { + // The following line does NOT work if one letter is selected and the user press Shift+Left or Shift+Right to unselect than letter: + // Qt misteriously tell us there is an invisible selection!! + //return m_editor->textEdit()->hasSelectedText(); + return !m_editor->textEdit()->selectedText().isEmpty(); + } else if (m_editor->lineEdit()) + return m_editor->lineEdit()->hasSelectedText(); + else + return false; +} + +bool Basket::selectedAllTextInEditor() +{ + if (!isDuringEdit() || !redirectEditActions()) + return false; + + if (m_editor->textEdit()) + return m_editor->textEdit()->text().isEmpty() || m_editor->textEdit()->text() == m_editor->textEdit()->selectedText(); + else if (m_editor->lineEdit()) + return m_editor->lineEdit()->text().isEmpty() || m_editor->lineEdit()->text() == m_editor->lineEdit()->selectedText(); + else + return false; +} + +void Basket::selectionChangedInEditor() +{ + Global::bnpView->notesStateChanged(); +} + +void Basket::contentChangedInEditor() +{ + // Do not wait 3 seconds, because we need the note to expand as needed (if a line is too wider... the note should grow wider): + if (m_editor->textEdit()) + m_editor->autoSave(/*toFileToo=*/false); +// else { + if (m_inactivityAutoSaveTimer.isActive()) + m_inactivityAutoSaveTimer.stop(); + m_inactivityAutoSaveTimer.start(3 * 1000, /*singleShot=*/true); + Global::bnpView->setUnsavedStatus(true); +// } +} + +void Basket::inactivityAutoSaveTimeout() +{ + if (m_editor) + m_editor->autoSave(/*toFileToo=*/true); +} + +void Basket::placeEditorAndEnsureVisible() +{ + placeEditor(/*andEnsureVisible=*/true); +} + +void Basket::placeEditor(bool /*andEnsureVisible*/ /*= false*/) +{ + if (!isDuringEdit()) + return; + + QFrame *editorQFrame = dynamic_cast<QFrame*>(m_editor->widget()); + KTextEdit *textEdit = m_editor->textEdit(); +// QLineEdit *lineEdit = m_editor->lineEdit(); + Note *note = m_editor->note(); + + int frameWidth = (editorQFrame ? editorQFrame->frameWidth() : 0); + int x = note->x() + note->contentX() + note->content()->xEditorIndent() - frameWidth; + int y; + int maxHeight = QMAX(visibleHeight(), contentsHeight()); + int height, width; + + if (textEdit) { + x -= 4; + // Need to do it 2 times, because it's wrong overwise + // (sometimes, width depends on height, and sometimes, height depends on with): + for (int i = 0; i < 2; i++) { + // FIXME: CRASH: Select all text, press Del or [<--] and editor->removeSelectedText() is called: + // editor->sync() CRASH!! + // editor->sync(); + y = note->y() + Note::NOTE_MARGIN - frameWidth; + height = textEdit->contentsHeight() + 2*frameWidth; +// height = /*QMAX(*/height/*, note->height())*/; +// height = QMIN(height, visibleHeight()); + width = note->x() + note->width() - x + 1;// /*note->x() + note->width()*/note->rightLimit() - x + 2*frameWidth + 1; +//width=QMAX(width,textEdit->contentsWidth()+2*frameWidth); + if (y + height > maxHeight) + y = maxHeight - height; + textEdit->setFixedSize(width, height); + } + } else { + height = note->height() - 2*Note::NOTE_MARGIN + 2*frameWidth; + width = note->x() + note->width() - x;//note->rightLimit() - x + 2*frameWidth; + m_editor->widget()->setFixedSize(width, height); + x -= 1; + y = note->y() + Note::NOTE_MARGIN - frameWidth; + } + if ((m_editorWidth > 0 && m_editorWidth != width) || (m_editorHeight > 0 && m_editorHeight != height)) { + m_editorWidth = width; // Avoid infinite recursion!!! + m_editorHeight = height; + m_editor->autoSave(/*toFileToo=*/true); + } + m_editorWidth = width; + m_editorHeight = height; + addChild(m_editor->widget(), x, y); + m_editorX = x; + m_editorY = y; + + m_leftEditorBorder->setFixedSize( (m_editor->textEdit() ? 3 : 0), height); +// m_leftEditorBorder->raise(); + addChild(m_leftEditorBorder, x, y ); + m_leftEditorBorder->setPosition( x, y ); + + m_rightEditorBorder->setFixedSize(3, height); +// m_rightEditorBorder->raise(); +// addChild(m_rightEditorBorder, note->rightLimit() - Note::NOTE_MARGIN, note->y() + Note::NOTE_MARGIN ); +// m_rightEditorBorder->setPosition( note->rightLimit() - Note::NOTE_MARGIN, note->y() + Note::NOTE_MARGIN ); + addChild(m_rightEditorBorder, note->x() + note->width() - Note::NOTE_MARGIN, note->y() + Note::NOTE_MARGIN ); + m_rightEditorBorder->setPosition( note->x() + note->width() - Note::NOTE_MARGIN, note->y() + Note::NOTE_MARGIN ); + +// if (andEnsureVisible) +// ensureNoteVisible(note); +} + +#include <iostream> +#include <private/qrichtext_p.h> +void Basket::editorCursorPositionChanged() +{ + if (!isDuringEdit()) + return; + + FocusedTextEdit *textEdit = (FocusedTextEdit*) m_editor->textEdit(); + const QTextCursor *cursor = textEdit->textCursor(); +// std::cout << cursor->x() << ";" << cursor->y() << " " +// << cursor->globalX() << ";" << cursor->globalY() << " " +// << cursor->offsetX() << ";" << cursor->offsetY() << ";" << std::endl; + + ensureVisible(m_editorX + cursor->globalX(), m_editorY + cursor->globalY(), 50, 50); +} + +void Basket::closeEditorDelayed() +{ + setFocus(); + QTimer::singleShot( 0, this, SLOT(closeEditor()) ); +} + +bool Basket::closeEditor() +{ + if (!isDuringEdit()) + return true; + + if (m_doNotCloseEditor) + return true; + + if (m_redirectEditActions) { + disconnect( m_editor->widget(), SIGNAL(selectionChanged()), this, SLOT(selectionChangedInEditor()) ); + if (m_editor->textEdit()) { + disconnect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(selectionChangedInEditor()) ); + disconnect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(contentChangedInEditor()) ); + } else if (m_editor->lineEdit()) { + disconnect( m_editor->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(selectionChangedInEditor()) ); + disconnect( m_editor->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(contentChangedInEditor()) ); + } + } + m_editor->widget()->disconnect(); + m_editor->widget()->hide(); + m_editor->validate(); + + delete m_leftEditorBorder; + delete m_rightEditorBorder; + m_leftEditorBorder = 0; + m_rightEditorBorder = 0; + + Note *note = m_editor->note(); + note->setWidth(0); // For relayoutNotes() to succeed to take care of the change + + // Delete the editor BEFORE unselecting the note because unselecting the note would trigger closeEditor() recursivly: + bool isEmpty = m_editor->isEmpty(); + delete m_editor; + m_editor = 0; + m_redirectEditActions = false; + m_editorWidth = -1; + m_editorHeight = -1; + m_inactivityAutoSaveTimer.stop(); + + // Delete the note if it is now empty: + if (isEmpty) { + focusANonSelectedNoteAboveOrThenBelow(); + note->setSelected(true); + note->deleteSelectedNotes(); + save(); + note = 0; + } + + unlockHovering(); + filterAgain(/*andEnsureVisible=*/false); + +// Does not work: +// if (Settings::playAnimations()) +// note->setOnTop(true); // So if it grew, do not obscure it temporarily while the notes below it are moving + + if (note) + note->setSelected(false);//unselectAll(); + doHoverEffects(); +// save(); + + Global::bnpView->m_actEditNote->setEnabled( !isLocked() && countSelecteds() == 1 /*&& !isDuringEdit()*/ ); + + emit resetStatusBarText(); // Remove the "Editing. ... to validate." text. + + //if (kapp->activeWindow() == Global::mainContainer) + + // Set focus to the basket, unless the user pressed a letter key in the filter bar and the currently edited note came hidden, then editing closed: + if (!decoration()->filterBar()->lineEdit()->hasFocus()) + setFocus(); + + // Return true if the note is still there: + return (note != 0); +} + +void Basket::closeBasket() +{ + closeEditor(); + unbufferizeAll(); // Keep the memory footprint low + if (isEncrypted()) { + if (Settings::enableReLockTimeout()) { + int seconds = Settings::reLockTimeoutMinutes() * 60; + m_inactivityAutoLockTimer.start(seconds * 1000, /*singleShot=*/true); + } + } +} + +void Basket::openBasket() +{ + if (m_inactivityAutoLockTimer.isActive()) + m_inactivityAutoLockTimer.stop(); +} + +Note* Basket::theSelectedNote() +{ + if (countSelecteds() != 1) { + std::cout << "NO SELECTED NOTE !!!!" << std::endl; + return 0; + } + + Note *selectedOne; + FOR_EACH_NOTE (note) { + selectedOne = note->theSelectedNote(); + if (selectedOne) + return selectedOne; + } + + std::cout << "One selected note, BUT NOT FOUND !!!!" << std::endl; + + return 0; +} + +void debugSel(NoteSelection* sel, int n = 0) +{ + for (NoteSelection *node = sel; node; node = node->next) { + for (int i = 0; i < n; i++) + std::cout << "-"; + std::cout << (node->firstChild ? "Group" : node->note->content()->toText("")) << std::endl; + if (node->firstChild) + debugSel(node->firstChild, n+1); + } +} + +NoteSelection* Basket::selectedNotes() +{ + NoteSelection selection; + + FOR_EACH_NOTE (note) + selection.append(note->selectedNotes()); + + if (!selection.firstChild) + return 0; + + for (NoteSelection *node = selection.firstChild; node; node = node->next) + node->parent = 0; + + // If the top-most groups are columns, export only childs of those groups + // (because user is not consciencious that columns are groups, and don't care: it's not what she want): + if (selection.firstChild->note->isColumn()) { + NoteSelection tmpSelection; + NoteSelection *nextNode; + NoteSelection *nextSubNode; + for (NoteSelection *node = selection.firstChild; node; node = nextNode) { + nextNode = node->next; + if (node->note->isColumn()) { + for (NoteSelection *subNode = node->firstChild; subNode; subNode = nextSubNode) { + nextSubNode = subNode->next; + tmpSelection.append(subNode); + subNode->parent = 0; + subNode->next = 0; + } + } else { + tmpSelection.append(node); + node->parent = 0; + node->next = 0; + } + } +// debugSel(tmpSelection.firstChild); + return tmpSelection.firstChild; + } else { +// debugSel(selection.firstChild); + return selection.firstChild; + } +} + +void Basket::showEditedNoteWhileFiltering() +{ + if (m_editor) { + Note *note = m_editor->note(); + filterAgain(); + note->setSelected(true); + relayoutNotes(false); + note->setX(note->finalX()); + note->setY(note->finalY()); + filterAgainDelayed(); + } +} + +void Basket::noteEdit(Note *note, bool justAdded, const QPoint &clickedPoint) // TODO: Remove the first parameter!!! +{ + if (!note) + note = theSelectedNote(); // TODO: Or pick the focused note! + if (!note) + return; + + if (isDuringEdit()) { + closeEditor(); // Validate the noteeditors in KLineEdit that does not intercept Enter key press (and edit is triggered with Enter too... Can conflict) + return; + } + + if (note != m_focusedNote) { + setFocusedNote(note); + m_startOfShiftSelectionNote = note; + } + + if (justAdded && isFiltering()) { + QTimer::singleShot( 0, this, SLOT(showEditedNoteWhileFiltering()) ); + } + + doHoverEffects(note, Note::Content); // Be sure (in the case Edit was triggered by menu or Enter key...): better feedback! + //m_lockedHovering = true; + + //m_editorWidget = note->content()->launchEdit(this); + NoteEditor *editor = NoteEditor::editNoteContent(note->content(), this); + + if (editor->widget()) { + m_editor = editor; + m_leftEditorBorder = new TransparentWidget(this); + m_rightEditorBorder = new TransparentWidget(this); + m_editor->widget()->reparent(viewport(), QPoint(0,0), true); + m_leftEditorBorder->reparent(viewport(), QPoint(0,0), true); + m_rightEditorBorder->reparent(viewport(), QPoint(0,0), true); + addChild(m_editor->widget(), 0, 0); + placeEditorAndEnsureVisible(); // placeEditor(); // FIXME: After? + m_redirectEditActions = m_editor->lineEdit() || m_editor->textEdit(); + if (m_redirectEditActions) { + connect( m_editor->widget(), SIGNAL(selectionChanged()), this, SLOT(selectionChangedInEditor()) ); + // In case there is NO text, "Select All" is disabled. But if the user press a key the there is now a text: + // selection has not changed but "Select All" should be re-enabled: + if (m_editor->textEdit()) { + connect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(selectionChangedInEditor()) ); + connect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(contentChangedInEditor()) ); + } else if (m_editor->lineEdit()) { + connect( m_editor->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(selectionChangedInEditor()) ); + connect( m_editor->lineEdit(), SIGNAL(textChanged(const QString&)), this, SLOT(contentChangedInEditor()) ); + } + } + m_editor->widget()->show(); + //m_editor->widget()->raise(); + m_editor->widget()->setFocus(); + connect( m_editor, SIGNAL(askValidation()), this, SLOT(closeEditorDelayed()) ); + connect( m_editor, SIGNAL(mouseEnteredEditorWidget()), this, SLOT(mouseEnteredEditorWidget()) ); + if (m_editor->textEdit()) { + connect( m_editor->textEdit(), SIGNAL(textChanged()), this, SLOT(placeEditorAndEnsureVisible()) ); + if (clickedPoint != QPoint()) { + QPoint pos(clickedPoint.x() - note->x() - note->contentX() + m_editor->textEdit()->frameWidth() + 4 - m_editor->textEdit()->frameWidth(), + clickedPoint.y() - note->y() - m_editor->textEdit()->frameWidth()); + // Do it right before the kapp->processEvents() to not have the cursor to quickly flicker at end (and sometimes stay at end AND where clicked): + m_editor->textEdit()->moveCursor(KTextEdit::MoveHome, false); + m_editor->textEdit()->ensureCursorVisible(); + m_editor->textEdit()->placeCursor(pos); + updateEditorAppearance(); + } + } +// kapp->processEvents(); // Show the editor toolbar before ensuring the note is visible + ensureNoteVisible(note); // because toolbar can create a new line and then partially hide the note + m_editor->widget()->setFocus(); // When clicking in the basket, a QTimer::singleShot(0, ...) focus the basket! So we focus the the widget after kapp->processEvents() + emit resetStatusBarText(); // Display "Editing. ... to validate." + } else { + // Delete the note user have canceled the addition: + if ((justAdded && editor->canceled()) || editor->isEmpty() /*) && editor->note()->states().count() <= 0*/) { + focusANonSelectedNoteAboveOrThenBelow(); + editor->note()->setSelected(true); + editor->note()->deleteSelectedNotes(); + save(); + } + delete editor; + unlockHovering(); + filterAgain(); + unselectAll(); + } + Global::bnpView->m_actEditNote->setEnabled(false); +} + +void Basket::noteDelete() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->del(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->del(); + return; + } + + if (countSelecteds() <= 0) + return; + int really = KMessageBox::Yes; + if (Settings::confirmNoteDeletion()) + really = KMessageBox::questionYesNo( this, + i18n("<qt>Do you really want to delete this note?</qt>", + "<qt>Do you really want to delete those <b>%n</b> notes?</qt>", + countSelecteds()), + i18n("Delete Note", "Delete Notes", countSelecteds()) +#if KDE_IS_VERSION( 3, 2, 90 ) // KDE 3.3.x + , KStdGuiItem::del(), KStdGuiItem::cancel()); +#else + ); +#endif + if (really == KMessageBox::No) + return; + + noteDeleteWithoutConfirmation(); +} + +void Basket::focusANonSelectedNoteBelow(bool inSameColumn) +{ + // First focus another unselected one below it...: + if (m_focusedNote && m_focusedNote->isSelected()) { + Note *next = m_focusedNote->nextShownInStack(); + while (next && next->isSelected()) + next = next->nextShownInStack(); + if (next) { + if (inSameColumn && isColumnsLayout() && m_focusedNote->parentPrimaryNote() == next->parentPrimaryNote()) { + setFocusedNote(next); + m_startOfShiftSelectionNote = next; + } + } + } +} + +void Basket::focusANonSelectedNoteAbove(bool inSameColumn) +{ + // ... Or above it: + if (m_focusedNote && m_focusedNote->isSelected()) { + Note *prev = m_focusedNote->prevShownInStack(); + while (prev && prev->isSelected()) + prev = prev->prevShownInStack(); + if (prev) { + if (inSameColumn && isColumnsLayout() && m_focusedNote->parentPrimaryNote() == prev->parentPrimaryNote()) { + setFocusedNote(prev); + m_startOfShiftSelectionNote = prev; + } + } + } +} + +void Basket::focusANonSelectedNoteBelowOrThenAbove() +{ + focusANonSelectedNoteBelow(/*inSameColumn=*/true); + focusANonSelectedNoteAbove(/*inSameColumn=*/true); + focusANonSelectedNoteBelow(/*inSameColumn=*/false); + focusANonSelectedNoteAbove(/*inSameColumn=*/false); +} + +void Basket::focusANonSelectedNoteAboveOrThenBelow() +{ + focusANonSelectedNoteAbove(/*inSameColumn=*/true); + focusANonSelectedNoteBelow(/*inSameColumn=*/true); + focusANonSelectedNoteAbove(/*inSameColumn=*/false); + focusANonSelectedNoteBelow(/*inSameColumn=*/false); +} + +void Basket::noteDeleteWithoutConfirmation(bool deleteFilesToo) +{ + // If the currently focused note is selected, it will be deleted. + focusANonSelectedNoteBelowOrThenAbove(); + + // Do the deletion: + Note *note = firstNote(); + Note *next; + while (note) { + next = note->next(); // If we delete 'note' on the next line, note->next() will be 0! + note->deleteSelectedNotes(deleteFilesToo); + note = next; + } + + relayoutNotes(true); // FIXME: filterAgain()? + save(); +} + +void Basket::doCopy(CopyMode copyMode) +{ + QClipboard *cb = KApplication::clipboard(); + QClipboard::Mode mode = (copyMode == CopyToSelection ? QClipboard::Selection : QClipboard::Clipboard); + + NoteSelection *selection = selectedNotes(); + int countCopied = countSelecteds(); + if (selection->firstStacked()) { + QDragObject *d = NoteDrag::dragObject(selection, copyMode == CutToClipboard, /*source=*/0); // d will be deleted by QT +// /*bool shouldRemove = */d->drag(); +// delete selection; + cb->setData(d, mode); // NoteMultipleDrag will be deleted by QT +// if (copyMode == CutToClipboard && !note->useFile()) // If useFile(), NoteDrag::dragObject() will delete it TODO +// note->slotDelete(); + + if (copyMode == CutToClipboard) + noteDeleteWithoutConfirmation(/*deleteFilesToo=*/false); + + switch (copyMode) { + default: + case CopyToClipboard: emit postMessage(i18n("Copied note to clipboard.", "Copied notes to clipboard.", countCopied)); break; + case CutToClipboard: emit postMessage(i18n("Cut note to clipboard.", "Cut notes to clipboard.", countCopied)); break; + case CopyToSelection: emit postMessage(i18n("Copied note to selection.", "Copied notes to selection.", countCopied)); break; + } + } +} + +void Basket::noteCopy() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->copy(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->copy(); + } else + doCopy(CopyToClipboard); +} + +void Basket::noteCut() +{ + if (redirectEditActions()) { + if (m_editor->textEdit()) + m_editor->textEdit()->cut(); + else if (m_editor->lineEdit()) + m_editor->lineEdit()->cut(); + } else + doCopy(CutToClipboard); +} + +void Basket::noteOpen(Note *note) +{ + /* + GetSelectedNotes + NoSelectedNote || Count == 0 ? return + AllTheSameType ? + Get { url, message(count) } + */ + + // TODO: Open ALL selected notes! + if (!note) + note = theSelectedNote(); + if (!note) + return; + + KURL url = note->content()->urlToOpen(/*with=*/false); + QString message = note->content()->messageWhenOpenning(NoteContent::OpenOne /*NoteContent::OpenSeveral*/); + if (url.isEmpty()) { + if (message.isEmpty()) + emit postMessage(i18n("Unable to open this note.") /*"Unable to open those notes."*/); + else { + int result = KMessageBox::warningContinueCancel(this, message, /*caption=*/QString::null, KGuiItem(i18n("&Edit"), "edit")); + if (result == KMessageBox::Continue) + noteEdit(note); + } + } else { + emit postMessage(message); // "Openning link target..." / "Launching application..." / "Openning note file..." + // Finally do the opening job: + QString customCommand = note->content()->customOpenCommand(); + if (customCommand.isEmpty()) { + KRun *run = new KRun(url); + run->setAutoDelete(true); + } else + KRun::run(customCommand, url); + } +} + +/** Code from bool KRun::displayOpenWithDialog(const KURL::List& lst, bool tempFiles) + * It does not allow to set a text, so I ripped it to do that: + */ +bool KRun__displayOpenWithDialog(const KURL::List& lst, bool tempFiles, const QString &text) +{ + if (kapp && !kapp->authorizeKAction("openwith")) { + KMessageBox::sorry(0L, i18n("You are not authorized to open this file.")); // TODO: Better message, i18n freeze :-( + return false; + } + KOpenWithDlg l(lst, text, QString::null, 0L); + if (l.exec()) { + KService::Ptr service = l.service(); + if (!!service) + return KRun::run(*service, lst, tempFiles); + //kdDebug(250) << "No service set, running " << l.text() << endl; + return KRun::run(l.text(), lst); // TODO handle tempFiles + } + return false; +} + +void Basket::noteOpenWith(Note *note) +{ + if (!note) + note = theSelectedNote(); + if (!note) + return; + + KURL url = note->content()->urlToOpen(/*with=*/true); + QString message = note->content()->messageWhenOpenning(NoteContent::OpenOneWith /*NoteContent::OpenSeveralWith*/); + QString text = note->content()->messageWhenOpenning(NoteContent::OpenOneWithDialog /*NoteContent::OpenSeveralWithDialog*/); + if (url.isEmpty()) + emit postMessage(i18n("Unable to open this note.") /*"Unable to open those notes."*/); + else if (KRun__displayOpenWithDialog(url, false, text)) + emit postMessage(message); // "Openning link target with..." / "Openning note file with..." +} + +void Basket::noteSaveAs() +{ +// if (!note) +// note = theSelectedNote(); + Note *note = theSelectedNote(); + if (!note) + return; + + KURL url = note->content()->urlToOpen(/*with=*/false); + if (url.isEmpty()) + return; + + QString fileName = KFileDialog::getSaveFileName(url.fileName(), note->content()->saveAsFilters(), this, i18n("Save to File")); + // TODO: Ask to overwrite ! + if (fileName.isEmpty()) + return; + + // TODO: Convert format, etc. (use NoteContent::saveAs(fileName)) + KIO::copy(url, KURL(fileName)); +} + +Note* Basket::selectedGroup() +{ + FOR_EACH_NOTE (note) { + Note *selectedGroup = note->selectedGroup(); + if (selectedGroup) { + // If the selected group is one group in a column, then return that group, and not the column, + // because the column is not ungrouppage, and the Ungroup action would be disabled. + if (selectedGroup->isColumn() && selectedGroup->firstChild() && !selectedGroup->firstChild()->next()) { + return selectedGroup->firstChild(); + } + return selectedGroup; + } + } + return 0; +} + +bool Basket::selectionIsOneGroup() +{ + return (selectedGroup() != 0); +} + +Note* Basket::firstSelected() +{ + Note *first = 0; + FOR_EACH_NOTE (note) { + first = note->firstSelected(); + if (first) + return first; + } + return 0; +} + +Note* Basket::lastSelected() +{ + Note *last = 0, *tmp = 0; + FOR_EACH_NOTE (note) { + tmp = note->lastSelected(); + if (tmp) + last = tmp; + } + return last; +} + +bool Basket::convertTexts() +{ + m_watcher->stopScan(); + bool convertedNotes = false; + + if (!isLoaded()) + load(); + + FOR_EACH_NOTE (note) + if (note->convertTexts()) + convertedNotes = true; + + if (convertedNotes) + save(); + m_watcher->startScan(); + return convertedNotes; +} + +void Basket::noteGroup() +{ +/* // Nothing to do? + if (isLocked() || countSelecteds() <= 1) + return; + + // If every selected notes are ALREADY in one group, then don't touch anything: + Note *selectedGroup = this->selectedGroup(); + if (selectedGroup && !selectedGroup->isColumn()) + return; +*/ + + // Copied from BNPView::updateNotesActions() + bool severalSelected = countSelecteds() >= 2; + Note *selectedGroup = (severalSelected ? this->selectedGroup() : 0); + bool enabled = !isLocked() && severalSelected && (!selectedGroup || selectedGroup->isColumn()); + if (!enabled) + return; + + // Get the first selected note: we will group selected items just before: + Note *first = firstSelected(); +// if (selectedGroup != 0 || first == 0) +// return; + + m_loaded = false; // Hack to avoid notes to be unselected and new notes to be selected: + + // Create and insert the receiving group: + Note *group = new Note(this); + if (first->isFree()) { + insertNote(group, 0L, Note::BottomColumn, QPoint(first->finalX(), first->finalY()), /*animateNewPosition=*/false); + } else { + insertNote(group, first, Note::TopInsert, QPoint(), /*animateNewPosition=*/false); + } + + // Put a FAKE UNSELECTED note in the new group, so if the new group is inside an allSelected() group, the parent group is not moved inside the new group! + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); + insertNote(fakeNote, group, Note::BottomColumn, QPoint(), /*animateNewPosition=*/false); + + // Group the notes: + Note *nextNote; + Note *note = firstNote(); + while (note) { + nextNote = note->next(); + note->groupIn(group); + note = nextNote; + } + + m_loaded = true; // Part 2 / 2 of the workarround! + + // Do cleanup: + unplugNote(fakeNote); + unselectAll(); + group->setSelectedRecursivly(true); // Notes were unselected by unplugging + + relayoutNotes(true); + save(); +} + +void Basket::noteUngroup() +{ + Note *group = selectedGroup(); + if (group && !group->isColumn()) + ungroupNote(group); + save(); +} + +void Basket::unplugSelection(NoteSelection *selection) +{ + for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) + unplugNote(toUnplug->note); +} + +void Basket::insertSelection(NoteSelection *selection, Note *after) +{ + for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { + if (toUnplug->note->isGroup()) { + Note *group = new Note(this); + insertNote(group, after, Note::BottomInsert, QPoint(), /*animateNewPosition=*/false); + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); + insertNote(fakeNote, group, Note::BottomColumn, QPoint(), /*animateNewPosition=*/false); + insertSelection(toUnplug->firstChild, fakeNote); + unplugNote(fakeNote); + after = group; + } else { + Note *note = toUnplug->note; + note->setPrev(0); + note->setNext(0); + insertNote(note, after, Note::BottomInsert, QPoint(), /*animateNewPosition=*/true); + after = note; + } + } +} + +void Basket::selectSelection(NoteSelection *selection) +{ + for (NoteSelection *toUnplug = selection->firstStacked(); toUnplug; toUnplug = toUnplug->nextStacked()) { + if (toUnplug->note->isGroup()) + selectSelection(toUnplug); + else + toUnplug->note->setSelected(true); + } +} + +void Basket::noteMoveOnTop() +{ + // TODO: Get the group containing the selected notes and first move inside the group, then inside parent group, then in the basket + // TODO: Move on top/bottom... of the column or basjet + + NoteSelection *selection = selectedNotes(); + unplugSelection(selection); + // Replug the notes: + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); + if (isColumnsLayout()) { + if (firstNote()->firstChild()) + insertNote(fakeNote, firstNote()->firstChild(), Note::TopInsert, QPoint(), /*animateNewPosition=*/false); + else + insertNote(fakeNote, firstNote(), Note::BottomColumn, QPoint(), /*animateNewPosition=*/false); + } else { + // TODO: Also allow to move notes on top of a group!!!!!!! + insertNote(fakeNote, 0, Note::BottomInsert, QPoint(0, 0), /*animateNewPosition=*/false); + } + insertSelection(selection, fakeNote); + unplugNote(fakeNote); + selectSelection(selection); + relayoutNotes(true); + save(); +} + +void Basket::noteMoveOnBottom() +{ + + // TODO: Duplicate code: void noteMoveOn(); + + // TODO: Get the group containing the selected notes and first move inside the group, then inside parent group, then in the basket + // TODO: Move on top/bottom... of the column or basjet + + NoteSelection *selection = selectedNotes(); + unplugSelection(selection); + // Replug the notes: + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); + if (isColumnsLayout()) + insertNote(fakeNote, firstNote(), Note::BottomColumn, QPoint(), /*animateNewPosition=*/false); + else { + // TODO: Also allow to move notes on top of a group!!!!!!! + insertNote(fakeNote, 0, Note::BottomInsert, QPoint(0, 0), /*animateNewPosition=*/false); + } + insertSelection(selection, fakeNote); + unplugNote(fakeNote); + selectSelection(selection); + relayoutNotes(true); + save(); +} + +void Basket::moveSelectionTo(Note *here, bool below/* = true*/) +{ + NoteSelection *selection = selectedNotes(); + unplugSelection(selection); + // Replug the notes: + Note *fakeNote = NoteFactory::createNoteColor(Qt::red, this); +// if (isColumnsLayout()) + insertNote(fakeNote, here, (below ? Note::BottomInsert : Note::TopInsert), QPoint(), /*animateNewPosition=*/false); +// else { +// // TODO: Also allow to move notes on top of a group!!!!!!! +// insertNote(fakeNote, 0, Note::BottomInsert, QPoint(0, 0), /*animateNewPosition=*/false); +// } + insertSelection(selection, fakeNote); + unplugNote(fakeNote); + selectSelection(selection); + relayoutNotes(true); + save(); +} + +void Basket::noteMoveNoteUp() +{ + + // TODO: Move between columns, even if they are empty !!!!!!! + + // TODO: if first note of a group, move just above the group! And let that even if there is no note before that group!!! + + Note *first = firstSelected(); + Note *previous = first->prevShownInStack(); + if (previous) + moveSelectionTo(previous, /*below=*/false); +} + +void Basket::noteMoveNoteDown() +{ + Note *first = lastSelected(); + Note *next = first->nextShownInStack(); + if (next) + moveSelectionTo(next, /*below=*/true); +} + +void Basket::wheelEvent(QWheelEvent *event) +{ + QScrollView::wheelEvent(event); +} + +void Basket::linkLookChanged() +{ + Note *note = m_firstNote; + while (note) { + note->linkLookChanged(); + note = note->next(); + } + relayoutNotes(true); +} + +void Basket::slotCopyingDone2(KIO::Job *job) +{ + if (job->error()) { + DEBUG_WIN << "Copy finished, ERROR"; + return; + } + KIO::FileCopyJob *fileCopyJob = (KIO::FileCopyJob*)job; + Note *note = noteForFullPath(fileCopyJob->destURL().path()); + DEBUG_WIN << "Copy finished, load note: " + fileCopyJob->destURL().path() + (note ? "" : " --- NO CORRESPONDING NOTE"); + if (note != 0L) { + note->content()->loadFromFile(/*lazyLoad=*/false); + if(isEncrypted()) + note->content()->saveToFile(); + if (m_focusedNote == note) // When inserting a new note we ensure it visble + ensureNoteVisible(note); // But after loading it has certainly grown and if it was + } // on bottom of the basket it's not visible entirly anymore +} + +Note* Basket::noteForFullPath(const QString &path) +{ + Note *note = firstNote(); + Note *found; + while (note) { + found = note->noteForFullPath(path); + if (found) + return found; + note = note->next(); + } + return 0; +} + +void Basket::deleteFiles() +{ + m_watcher->stopScan(); + Tools::deleteRecursively(fullPath()); +} + +QValueList<State*> Basket::usedStates() +{ + QValueList<State*> states; + FOR_EACH_NOTE (note) + note->usedStates(states); + return states; +} + +QString Basket::saveGradientBackground(const QColor &color, const QFont &font, const QString &folder) +{ + // Construct file name and return if the file already exists: + QString fileName = "note_background_" + color.name().lower().mid(1) + ".png"; + QString fullPath = folder + fileName; + if (QFile::exists(fullPath)) + return fileName; + + // Get the gradient top and bottom colors: + QColor topBgColor; + QColor bottomBgColor; + Note::getGradientColors(color, &topBgColor, &bottomBgColor); + + // Draw and save the gradient image: + int sampleTextHeight = QFontMetrics(font) + .boundingRect(0, 0, /*width=*/10000, /*height=*/0, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, "Test text") + .height(); + QPixmap noteGradient(100, sampleTextHeight + Note::NOTE_MARGIN); + QPainter painter(¬eGradient); + drawGradient(&painter, topBgColor, bottomBgColor, 0, 0, noteGradient.width(), noteGradient.height(), /*sunken=*/false, /*horz=*/true, /*flat=*/false); + painter.end(); + noteGradient.save(fullPath, "PNG"); + + // Return the name of the created file: + return fileName; +} + +void Basket::listUsedTags(QValueList<Tag*> &list) +{ + if (!isLoaded()) { + load(); + } + + FOR_EACH_NOTE (child) + child->listUsedTags(list); +} + + +/** Unfocus the previously focused note (unless it was null) + * and focus the new @param note (unless it is null) if hasFocus() + * Update m_focusedNote to the new one + */ +void Basket::setFocusedNote(Note *note) // void Basket::changeFocusTo(Note *note) +{ + // Don't focus an hidden note: + if (note != 0L && !note->isShown()) + return; + // When clicking a group, this group gets focused. But only content-based notes should be focused: + if (note && note->isGroup()) + note = note->firstRealChild(); + // The first time a note is focused, it becomes the start of the Shift selection: + if (m_startOfShiftSelectionNote == 0) + m_startOfShiftSelectionNote = note; + // Unfocus the old focused note: + if (m_focusedNote != 0L) + m_focusedNote->setFocused(false); + // Notify the new one to draw a focus rectangle... only if the basket is focused: + if (hasFocus() && note != 0L) + note->setFocused(true); + // Save the new focused note: + m_focusedNote = note; +} + +/** If no shown note is currently focused, try to find a shown note and focus it + * Also update m_focusedNote to the new one (or null if there isn't) + */ +void Basket::focusANote() +{ + if (countFounds() == 0) { // No note to focus + setFocusedNote(0L); +// m_startOfShiftSelectionNote = 0; + return; + } + + if (m_focusedNote == 0L) { // No focused note yet : focus the first shown + Note *toFocus = (isFreeLayout() ? noteOnHome() : firstNoteShownInStack()); + setFocusedNote(toFocus); +// m_startOfShiftSelectionNote = m_focusedNote; + return; + } + + // Search a visible note to focus if the focused one isn't shown : + Note *toFocus = m_focusedNote; + if (toFocus && !toFocus->isShown()) + toFocus = toFocus->nextShownInStack(); + if (!toFocus && m_focusedNote) + toFocus = m_focusedNote->prevShownInStack(); + setFocusedNote(toFocus); +// m_startOfShiftSelectionNote = toFocus; +} + +Note* Basket::firstNoteInStack() +{ + if (!firstNote()) + return 0; + + if (firstNote()->content()) + return firstNote(); + else + return firstNote()->nextInStack(); +} + +Note* Basket::lastNoteInStack() +{ + Note *note = lastNote(); + while (note) { + if (note->content()) + return note; + Note *possibleNote = note->lastRealChild(); + if (possibleNote && possibleNote->content()) + return possibleNote; + note = note->prev(); + } + return 0; +} + +Note* Basket::firstNoteShownInStack() +{ + Note *first = firstNoteInStack(); + while (first && !first->isShown()) + first = first->nextInStack(); + return first; +} + +Note* Basket::lastNoteShownInStack() +{ + Note *last = lastNoteInStack(); + while (last && !last->isShown()) + last = last->prevInStack(); + return last; +} + +inline int abs(int n) +{ + return (n < 0 ? -n : n); +} + +Note* Basket::noteOn(NoteOn side) +{ + Note *bestNote = 0; + int distance = -1; + int bestDistance = contentsWidth() * contentsHeight() * 10; + + Note *note = firstNoteShownInStack(); + Note *primary = m_focusedNote->parentPrimaryNote(); + while (note) { + switch (side) { + case LEFT_SIDE: distance = m_focusedNote->distanceOnLeftRight(note, LEFT_SIDE); break; + case RIGHT_SIDE: distance = m_focusedNote->distanceOnLeftRight(note, RIGHT_SIDE); break; + case TOP_SIDE: distance = m_focusedNote->distanceOnTopBottom(note, TOP_SIDE); break; + case BOTTOM_SIDE: distance = m_focusedNote->distanceOnTopBottom(note, BOTTOM_SIDE); break; + } + if ((side == TOP_SIDE || side == BOTTOM_SIDE || primary != note->parentPrimaryNote()) && note != m_focusedNote && distance > 0 && distance < bestDistance) { + bestNote = note; + bestDistance = distance; + } + note = note ->nextShownInStack(); + } + + return bestNote; +} + +Note* Basket::firstNoteInGroup() +{ + Note *child = m_focusedNote; + Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : 0); + while (parent) { + if (parent->firstChild() != child && !parent->isColumn()) + return parent->firstRealChild(); + child = parent; + parent = parent->parentNote(); + } + return 0; +} + +Note* Basket::noteOnHome() +{ + // First try to find the first note of the group containing the focused note: + Note *child = m_focusedNote; + Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : 0); + while (parent) { + if (parent->nextShownInStack() != m_focusedNote) + return parent->nextShownInStack(); + child = parent; + parent = parent->parentNote(); + } + + // If it was not found, then focus the very first note in the basket: + if (isFreeLayout()) { + Note *first = firstNoteShownInStack(); // The effective first note found + Note *note = first; // The current note, to conpare with the previous first note, if this new note is more on top + if (note) + note = note->nextShownInStack(); + while (note) { + if (note->finalY() < first->finalY() || (note->finalY() == first->finalY() && note->finalX() < first->finalX())) + first = note; + note = note->nextShownInStack(); + } + return first; + } else + return firstNoteShownInStack(); +} + +Note* Basket::noteOnEnd() +{ + Note *child = m_focusedNote; + Note *parent = (m_focusedNote ? m_focusedNote->parentNote() : 0); + Note *lastChild; + while (parent) { + lastChild = parent->lastRealChild(); + if (lastChild && lastChild != m_focusedNote) { + if (lastChild->isShown()) + return lastChild; + lastChild = lastChild->prevShownInStack(); + if (lastChild && lastChild->isShown() && lastChild != m_focusedNote) + return lastChild; + } + child = parent; + parent = parent->parentNote(); + } + if (isFreeLayout()) { + Note *last; + Note *note; + last = note = firstNoteShownInStack(); + note = note->nextShownInStack(); + while (note) { + if (note->finalBottom() > last->finalBottom() || (note->finalBottom() == last->finalBottom() && note->finalX() > last->finalX())) + last = note; + note = note->nextShownInStack(); + } + return last; + } else + return lastNoteShownInStack(); +} + + +void Basket::keyPressEvent(QKeyEvent *event) +{ + if (isDuringEdit() && event->key() == Qt::Key_Return) { + //if (m_editor->lineEdit()) + // closeEditor(); + //else + m_editor->widget()->setFocus(); + } else if (event->key() == Qt::Key_Escape) { + if (isDuringEdit()) + closeEditor(); + else if (decoration()->filterData().isFiltering) + cancelFilter(); + else + unselectAll(); + } + + if (countFounds() == 0) + return; + + if (!m_focusedNote) + return; + + Note *toFocus = 0L; + + switch (event->key()) { + case Qt::Key_Down: + toFocus = (isFreeLayout() ? noteOn(BOTTOM_SIDE) : m_focusedNote->nextShownInStack()); + if (toFocus) + break; + scrollBy(0, 30); // This cases do not move focus to another note... + return; + case Qt::Key_Up: + toFocus = (isFreeLayout() ? noteOn(TOP_SIDE) : m_focusedNote->prevShownInStack()); + if (toFocus) + break; + scrollBy(0, -30); // This cases do not move focus to another note... + return; + case Qt::Key_PageDown: + if (isFreeLayout()) { + Note *lastFocused = m_focusedNote; + for (int i = 0; i < 10 && m_focusedNote; ++i) + m_focusedNote = noteOn(BOTTOM_SIDE); + toFocus = m_focusedNote; + m_focusedNote = lastFocused; + } else { + toFocus = m_focusedNote; + for (int i = 0; i < 10 && toFocus; ++i) + toFocus = toFocus->nextShownInStack(); + } + if (toFocus == 0L) + toFocus = (isFreeLayout() ? noteOnEnd() : lastNoteShownInStack()); + if (toFocus && toFocus != m_focusedNote) + break; + scrollBy(0, visibleHeight() / 2); // This cases do not move focus to another note... + return; + case Qt::Key_PageUp: + if (isFreeLayout()) { + Note *lastFocused = m_focusedNote; + for (int i = 0; i < 10 && m_focusedNote; ++i) + m_focusedNote = noteOn(TOP_SIDE); + toFocus = m_focusedNote; + m_focusedNote = lastFocused; + } else { + toFocus = m_focusedNote; + for (int i = 0; i < 10 && toFocus; ++i) + toFocus = toFocus->prevShownInStack(); + } + if (toFocus == 0L) + toFocus = (isFreeLayout() ? noteOnHome() : firstNoteShownInStack()); + if (toFocus && toFocus != m_focusedNote) + break; + scrollBy(0, - visibleHeight() / 2); // This cases do not move focus to another note... + return; + case Qt::Key_Home: + toFocus = noteOnHome(); + break; + case Qt::Key_End: + toFocus = noteOnEnd(); + break; + case Qt::Key_Left: + if (m_focusedNote->tryFoldParent()) + return; + if ( (toFocus = noteOn(LEFT_SIDE)) ) + break; + if ( (toFocus = firstNoteInGroup()) ) + break; + scrollBy(-30, 0); // This cases do not move focus to another note... + return; + case Qt::Key_Right: + if (m_focusedNote->tryExpandParent()) + return; + if ( (toFocus = noteOn(RIGHT_SIDE)) ) + break; + scrollBy(30, 0); // This cases do not move focus to another note... + return; + case Qt::Key_Space: // This case do not move focus to another note... + if (m_focusedNote) { + m_focusedNote->setSelected( ! m_focusedNote->isSelected() ); + event->accept(); + } else + event->ignore(); + return; // ... so we return after the process + default: + return; + } + + if (toFocus == 0L) { // If no direction keys have been pressed OR reached out the begin or end + event->ignore(); // Important !! + return; + } + + if (event->state() & Qt::ShiftButton) { // Shift+arrowKeys selection + if (m_startOfShiftSelectionNote == 0L) + m_startOfShiftSelectionNote = toFocus; + ensureNoteVisible(toFocus); // Important: this line should be before the other ones because else repaint would be done on the wrong part! + selectRange(m_startOfShiftSelectionNote, toFocus); + setFocusedNote(toFocus); + event->accept(); + return; + } else /*if (toFocus != m_focusedNote)*/ { // Move focus to ANOTHER note... + ensureNoteVisible(toFocus); // Important: this line should be before the other ones because else repaint would be done on the wrong part! + setFocusedNote(toFocus); + m_startOfShiftSelectionNote = toFocus; + if ( ! (event->state() & Qt::ControlButton) ) // ... select only current note if Control + unselectAllBut(m_focusedNote); + event->accept(); + return; + } + + event->ignore(); // Important !! +} + +/** Select a range of notes and deselect the others. + * The order between start and end has no importance (end could be before start) + */ +void Basket::selectRange(Note *start, Note *end, bool unselectOthers /*= true*/) +{ + Note *cur; + Note *realEnd = 0L; + + // Avoid crash when start (or end) is null + if (start == 0L) + start = end; + else if (end == 0L) + end = start; + // And if *both* are null + if (start == 0L) { + if (unselectOthers) + unselectAll(); + return; + } + // In case there is only one note to select + if (start == end) { + if (unselectOthers) + unselectAllBut(start); + else + start->setSelected(true); + return; + } + + // Free layout baskets should select range as if we were drawing a rectangle between start and end: + if (isFreeLayout()) { + QRect startRect( start->finalX(), start->finalY(), start->width(), start->finalHeight() ); + QRect endRect( end->finalX(), end->finalY(), end->width(), end->finalHeight() ); + QRect toSelect = startRect.unite(endRect); + selectNotesIn(toSelect, /*invertSelection=*/false, unselectOthers); + return; + } + + // Search the REAL first (and deselect the others before it) : + for (cur = firstNoteInStack(); cur != 0L; cur = cur->nextInStack()) { + if (cur == start || cur == end) + break; + if (unselectOthers) + cur->setSelected(false); + } + + // Select the notes after REAL start, until REAL end : + if (cur == start) + realEnd = end; + else if (cur == end) + realEnd = start; + + for (/*cur = cur*/; cur != 0L; cur = cur->nextInStack()) { + cur->setSelected(cur->isShown()); // Select all notes in the range, but only if they are shown + if (cur == realEnd) + break; + } + + if (!unselectOthers) + return; + + // Deselect the remaining notes : + if (cur) + cur = cur->nextInStack(); + for (/*cur = cur*/; cur != 0L; cur = cur->nextInStack()) + cur->setSelected(false); +} + +void Basket::focusInEvent(QFocusEvent*) +{ + // Focus cannot be get with Tab when locked, but a click can focus the basket! + if (isLocked()) { + if (m_button) + QTimer::singleShot( 0, m_button, SLOT(setFocus()) ); + } else + focusANote(); // hasFocus() is true at this stage, note will be focused +} + +void Basket::focusOutEvent(QFocusEvent*) +{ + if (m_focusedNote != 0L) + m_focusedNote->setFocused(false); +} + +void Basket::ensureNoteVisible(Note *note) +{ + if (!note->isShown()) // Logical! + return; + + if (note == editedNote()) // HACK: When filtering while editing big notes, etc... cause unwanted scrolls + return; + + int finalBottom = note->finalY() + QMIN(note->finalHeight(), visibleHeight()); + int finalRight = note->finalX() + QMIN(note->width() + (note->hasResizer() ? Note::RESIZER_WIDTH : 0), visibleWidth()); + ensureVisible( finalRight, finalBottom, 0,0 ); + ensureVisible( note->finalX(), note->finalY(), 0,0 ); +} + +void Basket::addWatchedFile(const QString &fullPath) +{ +// DEBUG_WIN << "Watcher>Add Monitoring Of : <font color=blue>" + fullPath + "</font>"; + m_watcher->addFile(fullPath); +} + +void Basket::removeWatchedFile(const QString &fullPath) +{ +// DEBUG_WIN << "Watcher>Remove Monitoring Of : <font color=blue>" + fullPath + "</font>"; + m_watcher->removeFile(fullPath); +} + +void Basket::watchedFileModified(const QString &fullPath) +{ + if (!m_modifiedFiles.contains(fullPath)) + m_modifiedFiles.append(fullPath); + // If a big file is saved by an application, notifications are send several times. + // We wait they are not sent anymore to considere the file complete! + m_watcherTimer.start(200/*ms*/, true); + DEBUG_WIN << "Watcher>Modified : <font color=blue>" + fullPath + "</font>"; +} + +void Basket::watchedFileDeleted(const QString &fullPath) +{ + Note *note = noteForFullPath(fullPath); + removeWatchedFile(fullPath); + if (note) { + NoteSelection *selection = selectedNotes(); + unselectAllBut(note); + noteDeleteWithoutConfirmation(); + while (selection) { + selection->note->setSelected(true); + selection = selection->nextStacked(); + } + } + DEBUG_WIN << "Watcher>Removed : <font color=blue>" + fullPath + "</font>"; +} + +void Basket::updateModifiedNotes() +{ + for (QValueList<QString>::iterator it = m_modifiedFiles.begin(); it != m_modifiedFiles.end(); ++it) { + Note *note = noteForFullPath(*it); + if (note) + note->content()->loadFromFile(/*lazyLoad=*/false); + } + m_modifiedFiles.clear(); +} + +bool Basket::setProtection(int type, QString key) +{ +#ifdef HAVE_LIBGPGME + if(type == PasswordEncryption || // Ask a new password + m_encryptionType != type || m_encryptionKey != key) + { + int savedType = m_encryptionType; + QString savedKey = m_encryptionKey; + + m_encryptionType = type; + m_encryptionKey = key; + m_gpg->clearCache(); + + if(saveAgain()) + { + emit propertiesChanged(this); + } + else + { + m_encryptionType = savedType; + m_encryptionKey = savedKey; + m_gpg->clearCache(); + return false; + } + } + return true; +#else + m_encryptionType = type; + m_encryptionKey = key; + return false; +#endif +} + +bool Basket::saveAgain() +{ + bool result = false; + + m_watcher->stopScan(); + // Re-encrypt basket file: + result = save(); + // Re-encrypt every note files recursively: + if(result) + { + FOR_EACH_NOTE (note) + { + result = note->saveAgain(); + if(!result) + break; + } + } + m_watcher->startScan(); + return result; +} + +bool Basket::loadFromFile(const QString &fullPath, QString *string, bool isLocalEncoding) +{ + QByteArray array; + + if(loadFromFile(fullPath, &array)){ + if (isLocalEncoding) + *string = QString::fromLocal8Bit(array.data(), array.size()); + else + *string = QString::fromUtf8(array.data(), array.size()); + return true; + } + else + return false; +} + +bool Basket::isEncrypted() +{ + return (m_encryptionType != NoEncryption); +} + +bool Basket::isFileEncrypted() +{ + QFile file(fullPath() + ".basket"); + + if (file.open(IO_ReadOnly)){ + QString line; + + file.readLine(line, 32); + if(line.startsWith("-----BEGIN PGP MESSAGE-----")) + return true; + } + return false; +} + +bool Basket::loadFromFile(const QString &fullPath, QByteArray *array) +{ + QFile file(fullPath); + bool encrypted = false; + + if (file.open(IO_ReadOnly)){ + *array = file.readAll(); + const char* magic = "-----BEGIN PGP MESSAGE-----"; + uint i = 0; + + if(array->size() > strlen(magic)) + for (i = 0; array->at(i) == magic[i]; ++i) + ; + if (i == strlen(magic)) + { + encrypted = true; + } + file.close(); +#ifdef HAVE_LIBGPGME + if(encrypted) + { + QByteArray tmp(*array); + + tmp.detach(); + // Only use gpg-agent for private key encryption since it doesn't + // cache password used in symmetric encryption. + m_gpg->setUseGnuPGAgent(Settings::useGnuPGAgent() && m_encryptionType == PrivateKeyEncryption); + if(m_encryptionType == PrivateKeyEncryption) + m_gpg->setText(i18n("Please enter the password for the following private key:"), false); + else + m_gpg->setText(i18n("Please enter the password for the basket <b>%1</b>:").arg(basketName()), false); // Used when decrypting + return m_gpg->decrypt(tmp, array); + } +#else + if(encrypted) + { + return false; + } +#endif + return true; + } else + return false; +} + +bool Basket::saveToFile(const QString& fullPath, const QString& string, bool isLocalEncoding) +{ + QCString bytes = (isLocalEncoding ? string.local8Bit() : string.utf8()); + return saveToFile(fullPath, bytes, bytes.length()); +} + +bool Basket::saveToFile(const QString& fullPath, const QByteArray& array) +{ + return saveToFile(fullPath, array, array.size()); +} + +bool Basket::saveToFile(const QString& fullPath, const QByteArray& array, Q_ULONG length) +{ + bool success = true; + QByteArray tmp; + +#ifdef HAVE_LIBGPGME + if(isEncrypted()) + { + QString key = QString::null; + + // We only use gpg-agent for private key encryption and saving without + // public key doesn't need one. + m_gpg->setUseGnuPGAgent(false); + if(m_encryptionType == PrivateKeyEncryption) + { + key = m_encryptionKey; + // public key doesn't need password + m_gpg->setText("", false); + } + else + m_gpg->setText(i18n("Please assign a password to the basket <b>%1</b>:").arg(basketName()), true); // Used when defining a new password + + success = m_gpg->encrypt(array, length, &tmp, key); + length = tmp.size(); + } + else + tmp = array; + +#else + success = !isEncrypted(); + if(success) + tmp = array; +#endif + /*if (success && (success = file.open(IO_WriteOnly))){ + success = (file.writeBlock(tmp) == (Q_LONG)tmp.size()); + file.close(); + }*/ + + if (success) + return safelySaveToFile(fullPath, tmp, length); + else + return false; +} + +/** Same as saveToFile(), but it is static, and does not crypt the data if needed. + * Basically, to save a file owned by a basket (a basket or a note file), use saveToFile(). + * But to save another file (eg. the basket hierarchy), use this safelySaveToFile() static method. + */ +/*static*/ bool Basket::safelySaveToFile(const QString& fullPath, const QByteArray& array, Q_ULONG length) +{ + // Here, we take a double protection: + // - We use KSaveFile to write atomically to the file (either it's a success or the file is untouched) + // - We show a modal dialog to the user when no disk space is left or access is denied and retry every couple of seconds + + // Static, because safelySaveToFile() can be called a second time while blocked. + // Example: + // User type something and press Enter: safelySaveToFile() is called and block. + // Three seconds later, a timer ask to save changes, and this second safelySaveToFile() block too. + // Do not show the dialog twice in this case! + static DiskErrorDialog *dialog = 0; + + //std::cout << "---------- Saving " << fullPath << ":" << std::endl; + bool openSuccess; + bool closeSuccess; + bool errorWhileWritting; + do { + KSaveFile saveFile(fullPath); + //std::cout << "==>>" << std::endl << "SAVE FILE CREATED: " << strerror(saveFile.status()) << std::endl; + openSuccess = (saveFile.status() == 0 && saveFile.file() != 0); + if (openSuccess) { + saveFile.file()->writeBlock(array, length); + //std::cout << "FILE WRITTEN: " << strerror(saveFile.status()) << std::endl; + closeSuccess = saveFile.close(); + //std::cout << "FILE CLOSED: " << (closeSuccess ? "well" : "erroneous") << std::endl; + } + errorWhileWritting = (!openSuccess || !closeSuccess || saveFile.status() != 0); + if (errorWhileWritting) { + //std::cout << "ERROR DETECTED" << std::endl; + if (dialog == 0) { + //std::cout << "Opening dialog for " << fullPath << std::endl; + dialog = new DiskErrorDialog( + (openSuccess + ? i18n("Insufficient Disk Space to Save Basket Data") + : i18n("Wrong Basket File Permissions") + ), + (openSuccess + ? i18n("Please remove files on the disk <b>%1</b> to let the application safely save your changes.") + .arg(KIO::findPathMountPoint(fullPath)) + : i18n("File permissions are bad for <b>%1</b>. Please check that you have write access to it and the parent folders.") + .arg(fullPath) + ), + kapp->activeWindow() + ); + } + if (!dialog->isShown()) + dialog->show(); + const int retryDelay = 1000/*ms*/; + const int sleepDelay = 50/*ms*/; + for (int i = 0; i < retryDelay / sleepDelay; ++i) { + kapp->processEvents(); + usleep(sleepDelay); + } + } + } while (errorWhileWritting); + if (dialog) { + delete dialog; + dialog = 0; + } + + return true; // Hum...?! +} + +/*static*/ bool Basket::safelySaveToFile(const QString& fullPath, const QString& string, bool isLocalEncoding) +{ + QCString bytes = (isLocalEncoding ? string.local8Bit() : string.utf8()); + return safelySaveToFile(fullPath, bytes, bytes.length() - 1); +} + +/*static*/ bool Basket::safelySaveToFile(const QString& fullPath, const QByteArray& array) +{ + return safelySaveToFile(fullPath, array, array.size()); +} + +DiskErrorDialog::DiskErrorDialog(const QString &titleMessage, const QString &message, QWidget *parent) + : KDialogBase(KDialogBase::Plain, i18n("Save Error"), + (KDialogBase::ButtonCode)0, (KDialogBase::ButtonCode)0, parent, /*name=*/"DiskError") +{ + //enableButtonCancel(false); + //enableButtonClose(false); + //enableButton(Close, false); + //enableButtonOK(false); + setModal(true); + QHBoxLayout *layout = new QHBoxLayout(plainPage(), /*margin=*/0, spacingHint()); + QPixmap icon = kapp->iconLoader()->loadIcon("hdd_unmount", KIcon::NoGroup, 64, KIcon::DefaultState, /*path_store=*/0L, /*canReturnNull=*/true); + QLabel *iconLabel = new QLabel(plainPage()); + iconLabel->setPixmap(icon); + iconLabel->setFixedSize(iconLabel->sizeHint()); + QLabel *label = new QLabel("<p><nobr><b><font size='+1'>" + titleMessage + "</font></b></nobr></p><p>" + message + "</p>", plainPage()); + if (!icon.isNull()) + layout->addWidget(iconLabel); + layout->addWidget(label); +} + +DiskErrorDialog::~DiskErrorDialog() +{ +} + +void DiskErrorDialog::closeEvent(QCloseEvent *event) +{ + event->ignore(); +} + +void DiskErrorDialog::keyPressEvent(QKeyEvent*) +{ + // Escape should not close the window... +} + +void Basket::lock() +{ +#ifdef HAVE_LIBGPGME + closeEditor(); + m_gpg->clearCache(); + m_locked = true; + enableActions(); + deleteNotes(); + m_loaded = false; + m_loadingLaunched = false; + updateContents(); +#endif +} + +#if 0 + +#include <qlayout.h> +#include <qvbox.h> +#include <qstring.h> +#include <qpixmap.h> +#include <qcolor.h> +#include <kpopupmenu.h> +#include <kurllabel.h> +#include <qcheckbox.h> +#include <qpalette.h> +#include <qcursor.h> +#include <qaction.h> +#include <kstdaccel.h> +#include <kglobalsettings.h> +#include <qevent.h> + +#include <kapplication.h> +#include <kaboutdata.h> +#include <qinputdialog.h> +#include <qdragobject.h> +#include <kurldrag.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kmimetype.h> +#include <kfiledialog.h> +#include <qdir.h> +#include <kiconloader.h> +#include <qregexp.h> +#include <qfileinfo.h> + +#include <qstringlist.h> +#include <qdir.h> +#include <kurl.h> +#include <krun.h> +#include <kmessagebox.h> +#include <kdeversion.h> + +#include "kdirwatch.h" +#include <qstringlist.h> +#include <klineedit.h> + +#include <config.h> +#include <qtextcodec.h> + +#include "basket.h" +#include "note.h" +#include "notefactory.h" +#include "variouswidgets.h" +#include "linklabel.h" +#include "global.h" +#include "container.h" +#include "xmlwork.h" +#include "settings.h" +#include "popupmenu.h" +#include "debugwindow.h" +#include "exporterdialog.h" + + +/** Basket */ + +const int Basket::c_updateTime = 200; + + + +// Remove the note from the basket and delete the associated file +// If the note mirror a file, it will ask before deleting or not the file +// But if askForMirroredFile is false, it willn't ask NOR delete the MIRRORED file +// (it will anyway delete the file if it is not a mirror) +void Basket::delNote(Note *note, bool askForMirroredFile) +{ +//... + if (hasFocus()) + focusANote(); // We need note->next() and note->previous() here [BUT deleted note should be hidden] + if (note->isSelected()) + note->setSelected(false); //removeSelectedNote(); + + relayoutNotes(); + recolorizeNotes(); + resetInsertTo(); // If we delete the first or the last, pointer to it is invalid + save(); + + if (note == m_startOfShiftSelectionNote) + m_startOfShiftSelectionNote = 0L; + + if (isDuringEdit() && m_editor->editedNote() == note) + closeEditor(false); +//... +} + + +// Calculate where to paste or drop +void Basket::computeInsertPlace(const QPoint &cursorPosition) +{ + int y = cursorPosition.y(); + + if (countShown() == 0) + return; + + // TODO: Memorize the last hovered note to avoid a new computation on dragMoveEvent !! + // If the mouse is not over the last note, compute which new is : + // TODO: Optimization : start from m_insertAtNote and compare y position to search before or after (or the same) + for (Note *it = firstNote(); it != 0L; it = it->next()) + if ( (it->isShown()) && (it->y() + it->height() >= y) && (it->y() < y) ) { + int center = it->y() + (it->height() / 2); + m_insertAtNote = it; + m_insertAfter = y > center; + return; + } + // Else, there is at least one shown note but cursor hover NO note, so we are after the last shown note + m_insertAtNote = lastShownNote(); + m_insertAfter = true; + + // Code for rectangular notes : + /*QRect globalRect = it->rect(); + globalRect.moveTopLeft(it->pos() + contentsY()); + if ( globalRect.contains(curPos) ) { + it->doInterestingThing(); + }*/ +} + +void Basket::dragMoveEvent(QDragMoveEvent* event) +{ +// m_isDuringDrag = true; + + if (isLocked()) + return; + +// FIXME: viewportToContents does NOT work !!! +// QPoint pos = viewportToContents(event->pos()); + QPoint pos( event->pos().x() + contentsX(), event->pos().y() + contentsY() ); + +// if (insertAtCursorPos()) + computeInsertPlace(pos); + + showFrameInsertTo(); + acceptDropEvent(event); + + // A workarround since QScrollView::dragAutoScroll seem to have no effect : + ensureVisible(event->pos().x() + contentsX(), event->pos().y() + contentsY(), 30, 30); +// QScrollView::dragMoveEvent(event); +} + +void Basket::dropEvent(QDropEvent *event) +{ + QPoint pos = event->pos(); + std::cout << "Drop Event at position " << pos.x() << ":" << pos.y() << std::endl; + m_isDuringDrag = false; + emit resetStatusBarText(); + + if (isLocked()) + return; + + NoteFactory::dropNote( event, this, true, event->action(), dynamic_cast<Note*>(event->source()) ); + // TODO: need to know if we really inserted an (or several!!!!) note !!! + ensureNoteVisible(lastInsertedNote()); + unselectAllBut(lastInsertedNote()); + setFocusedNote(lastInsertedNote()); + + resetInsertTo(); +} + +void Basket::moveOnTop() +{ + if (m_countSelecteds == 0) + return; + + Note *endOfBrowse = firstShownNote(); + Note *topNote = firstNote(); + Note *prev; + for (Note *it = lastShownNote(); it != 0L; ) { + prev = it->previous(); + if (it->isSelected()) { + m_insertAtNote = topNote; + m_insertAfter = false; + changeNotePlace(it); + topNote = it; + } + if (it == endOfBrowse) + break; + it = prev; + } + ensureNoteVisible(firstShownNote()); + ensureNoteVisible(m_focusedNote); +} + +void Basket::moveOnBottom() +{ + if (m_countSelecteds == 0) + return; + + Note *endOfBrowse = lastShownNote(); + Note *bottomNote = lastNote(); + Note *next; + for (Note *it = firstShownNote(); it != 0L; ) { + next = it->next(); + if (it->isSelected()) { + m_insertAtNote = bottomNote; + m_insertAfter = true; + changeNotePlace(it); + bottomNote = it; + } + if (it == endOfBrowse) + break; + it = next; + } + ensureNoteVisible(lastShownNote()); + ensureNoteVisible(m_focusedNote); +} + +void Basket::moveNoteUp() +{ + if (m_countSelecteds == 0) + return; + + // Begin from the top (important move all selected notes one note up + // AND to quit early if a selected note is the first shown one + for (Note *it = firstShownNote(); it != 0L; it = it->next()) { + if (it->isSelected() && it->isShown()) { // it->isShown() not necessary, but in case... + if (it == firstShownNote()) + return; // No way... + m_insertAtNote = nextShownNoteFrom(it, -1); // Previous shown note + if (m_insertAtNote == 0L) { // Should not appends, since it's not the first shown note, + resetInsertTo(); // there SHOULD be one before + return; + } + m_insertAfter = false; + changeNotePlace(it); + } + if (it == lastShownNote()) + break; + } + ensureNoteVisible(m_focusedNote); +} + +void Basket::moveNoteDown() +{ + if (m_countSelecteds == 0) + return; + + // Begin from the bottom (important move all selected notes one note down + // AND to quit early if a selected note is the last shown one + for (Note *it = lastShownNote(); it != 0L; it = it->previous()) { + if (it->isSelected() && it->isShown()) { // it->isShown() not necessary, but in case... + if (it == lastShownNote()) + return; // No way... + m_insertAtNote = nextShownNoteFrom(it, 1); // Next shown note + if (m_insertAtNote == 0L) { // Should not appends, since it's not the last shown note, + resetInsertTo(); // there SHOULD be one before + return; + } + m_insertAfter = true; + changeNotePlace(it); + } + if (it == firstShownNote()) + break; + } + ensureNoteVisible(m_focusedNote); +} + +#endif // #if 0 + +#include "basket.moc" diff --git a/src/basket.desktop b/src/basket.desktop new file mode 100644 index 0000000..5fcf3ef --- /dev/null +++ b/src/basket.desktop @@ -0,0 +1,34 @@ +[Desktop Entry] +Encoding=UTF-8 +Type=Application + +Exec=basket %f +MimeType=application/x-basket-archive;application/x-basket-template +Icon=basket + +Name=BasKet Note Pads +Name[de]=BasKet Notizblätter +Name[es]=Blocs de notas BasKet +Name[fr]=Blocs notes BasKet +Name[it]=Blocco appunti BasKet +Name[ja]=BasKet メモパッド +Name[nb]=Kurvnotat +Name[nn]=Korgnotat +Name[pt]=Bloco de Notas BasKet +Name[ru]=Альбом заметок BasKet +Name[xx]=xxBasKet Note Padsxx +Name[tr]=BasKet + +Comment=Taking care of your ideas. +Comment[de]=Bewahren Sie Ihre Ideen. +Comment[es]=Cuidando de sus ideas. +Comment[fr]=Prendre soin de vos idées. +Comment[it]=Ci prendiamo cura delle tue idee +Comment[nb]=Ta vare på tankene dine. +Comment[nn]=Ta vare på tankane dine. +Comment[ja]=あなたのアイデアのお世話をします。 +Comment[pt]=Cuidando das suas ideias. +Comment[ru]=Заботимся о ваших идеях. +Comment[xx]=xxTaking care of your ideas.xx +Comment[tr]=Not Tutma Aracı +Categories=KDE;Application;Utility;TextEditor; diff --git a/src/basket.h b/src/basket.h new file mode 100644 index 0000000..4e0331f --- /dev/null +++ b/src/basket.h @@ -0,0 +1,835 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BASKET_H +#define BASKET_H + +#include <qscrollview.h> +#include <qtooltip.h> +#include <qvaluelist.h> +#include <qtimer.h> +#include <qimage.h> +#include <qdatetime.h> +#include <qclipboard.h> +#include <kshortcut.h> +#include <kdirwatch.h> +#include <kaction.h> +#include <kio/job.h> +#include <kdialogbase.h> + +#include "filter.h" +#include "note.h" // For Note::Zone +#include "config.h" + +class QVBoxLayout; +class QDomDocument; +class QDomElement; + +class Basket; +class Note; +class NoteEditor; +class Tag; +#ifdef HAVE_LIBGPGME +class KGpgMe; +#endif + +/** Provide a dialog to avert the user the disk is full. + * This dialog is modal and is shown until the user has made space on the disk. + * @author S�astien Laot + */ +class DiskErrorDialog : public KDialogBase +{ + Q_OBJECT + public: + DiskErrorDialog(const QString &titleMessage, const QString &message, QWidget *parent = 0); + ~DiskErrorDialog(); + protected: + void closeEvent(QCloseEvent *event); + void keyPressEvent(QKeyEvent*); +}; + + +/** A list of flags to set how notes are inserted/plugged in the basket + * Declare a varible with the type PlugOptions::Flags and assign a value like PlugOptions::DoSelection... + * @author S�astien Laot + */ +namespace PlugOptions +{ + enum Flags { + SelectOnlyNewNotes = 0x01, /// << Unselect every notes in the basket and select the newly inserted ones + DoTagsInheriting = 0x02 /// << The new notes inherit the tags of the sibbling note + }; + // TODO: FocusLastInsertedNote (last visible!), EnsureVisibleAddedNotes, PopupFeebackBaloon (if not called by hand), AnimateNewPosition, FeedbackUnmatched + // TODO: moveNoteInTree(bool animate); +} + +/** This represent a hierarchy of the selected classes. + * If this is null, then there is no selected note. + */ +class NoteSelection +{ + public: + NoteSelection() : note(0), parent(0), firstChild(0), next(0), fullPath() {} + NoteSelection(Note *n) : note(n), parent(0), firstChild(0), next(0), fullPath() {} + + Note *note; + NoteSelection *parent; + NoteSelection *firstChild; + NoteSelection *next; + QString fullPath; // Needeed for 'Cut' code to store temporary path of the cutted note. + + NoteSelection* firstStacked(); + NoteSelection* nextStacked(); + void append(NoteSelection *node); + int count(); + + QValueList<Note*> parentGroups(); +}; + +/** This store all needed information when exporting to HTML + */ +class HtmlExportData +{ + public: + QString iconsFolderPath; + QString iconsFolderName; + QString imagesFolderPath; + QString imagesFolderName; + QString dataFolderPath; + QString dataFolderName; + bool formatForImpression; + bool embedLinkedFiles; + bool embedLinkedFolders; +}; + +/** This class handle Basket and add a FilterWidget on top of it. + * @author S�astien Laot + */ +class DecoratedBasket : public QWidget +{ + Q_OBJECT + public: + DecoratedBasket(QWidget *parent, const QString &folderName, const char *name = 0, WFlags fl = 0); + ~DecoratedBasket(); + void setFilterBarPosition(bool onTop); + void resetFilter(); + void setFilterBarShown(bool show, bool switchFocus = true); + bool isFilterBarShown() { return m_filter->isShown(); } + const FilterData& filterData() { return m_filter->filterData(); } + FilterBar* filterBar() { return m_filter; } + Basket* basket() { return m_basket; } + private: + QVBoxLayout *m_layout; + FilterBar *m_filter; + Basket *m_basket; +}; + +class TransparentWidget : public QWidget +{ + Q_OBJECT + public: + TransparentWidget(Basket *basket); + void setPosition(int x, int y); + //void reparent(QWidget *parent, WFlags f, const QPoint &p, bool showIt = FALSE); + protected: + void paintEvent(QPaintEvent*); + void mouseMoveEvent(QMouseEvent *event); + bool eventFilter(QObject *object, QEvent *event); + private: + Basket *m_basket; + int m_x; + int m_y; +}; + +/** + * @author S�astien Laot + */ +class Basket : public QScrollView, public QToolTip +{ +/// CONSTRUCTOR AND DESTRUCTOR: + Q_OBJECT + public: + enum EncryptionTypes { + NoEncryption = 0, + PasswordEncryption = 1, + PrivateKeyEncryption = 2 + }; + + public: + Basket(QWidget *parent, const QString &folderName); + ~Basket(); + +/// USER INTERACTION: + private: + bool m_noActionOnMouseRelease; + bool m_ignoreCloseEditorOnNextMouseRelease; + QPoint m_pressPos; + bool m_canDrag; + public: + void viewportResizeEvent(QResizeEvent *); + void drawContents(QPainter *painter, int clipX, int clipY, int clipWidth, int clipHeight); + void enterEvent(QEvent *); + void leaveEvent(QEvent *); + void contentsMouseMoveEvent(QMouseEvent *event); + void contentsMousePressEvent(QMouseEvent *event); + void contentsMouseReleaseEvent(QMouseEvent *event); + void contentsMouseDoubleClickEvent(QMouseEvent *event); + void contentsContextMenuEvent(QContextMenuEvent *event); + void updateNote(Note *note); + void clickedToInsert(QMouseEvent *event, Note *clicked = 0, int zone = 0); + private slots: + void setFocusIfNotInPopupMenu(); + +/// LAYOUT: + private: + Note *m_firstNote; + int m_columnsCount; + bool m_mindMap; + Note *m_resizingNote; + int m_pickedResizer; + Note *m_movingNote; + QPoint m_pickedHandle; + public: + int tmpWidth; + int tmpHeight; + public: + void unsetNotesWidth(); + void relayoutNotes(bool animate); + Note* noteAt(int x, int y); + inline Note* firstNote() { return m_firstNote; } + inline int columnsCount() { return m_columnsCount; } + inline bool isColumnsLayout() { return m_columnsCount > 0; } + inline bool isFreeLayout() { return m_columnsCount <= 0; } + inline bool isMindMap() { return isFreeLayout() && m_mindMap; } + Note* resizingNote() { return m_resizingNote; } + void deleteNotes(); + Note* lastNote(); + void setDisposition(int disposition, int columnCount); + void equalizeColumnSizes(); + +/// NOTES INSERTION AND REMOVAL: + public: + /// The following methods assume that the note(s) to insert already all have 'this' as the parent basket: + void prependNoteIn( Note *note, Note *in); /// << Add @p note (and the next linked notes) as the first note(s) of the group @p in. + void appendNoteIn( Note *note, Note *in); /// << Add @p note (and the next linked notes) as the last note(s) of the group @p in. + void appendNoteAfter( Note *note, Note *after); /// << Add @p note (and the next linked notes) just after (just below) the note @p after. + void appendNoteBefore(Note *note, Note *before); /// << Add @p note (and the next linked notes) just before (just above) the note @p before. + void groupNoteAfter( Note *note, Note *with); /// << Add a group at @p with place, move @p with in it, and add @p note (and the next linked notes) just after the group. + void groupNoteBefore( Note *note, Note *with); /// << Add a group at @p with place, move @p with in it, and add @p note (and the next linked notes) just before the group. + void unplugNote( Note *note); /// << Unplug @p note (and its child notes) from the basket (and also decrease counts...). + /// << After that, you should delete the notes yourself. Do not call prepend/append/group... functions two times: unplug and ok + void ungroupNote( Note *group); /// << Unplug @p group but put child notes at its place. + /// And this one do almost all the above methods depending on the context: + void insertNote(Note *note, Note *clicked, int zone, const QPoint &pos = QPoint(), bool animateNewPosition = false); + void insertCreatedNote(Note *note); + /// And working with selections: + void unplugSelection(NoteSelection *selection); + void insertSelection(NoteSelection *selection, Note *after); + void selectSelection(NoteSelection *selection); + private: + void preparePlug(Note *note); + private: + Note *m_clickedToInsert; + int m_zoneToInsert; + QPoint m_posToInsert; + Note *m_savedClickedToInsert; + int m_savedZoneToInsert; + QPoint m_savedPosToInsert; + bool m_isInsertPopupMenu; + public: + void saveInsertionData(); + void restoreInsertionData(); + void resetInsertionData(); + public slots: + void insertEmptyNote(int type); + void insertWizard(int type); + void insertColor(const QColor &color); + void insertImage(const QPixmap &image); + void pasteNote(QClipboard::Mode mode = QClipboard::Clipboard); + void delayedCancelInsertPopupMenu(); + void setInsertPopupMenu() { m_isInsertPopupMenu = true; } + void cancelInsertPopupMenu() { m_isInsertPopupMenu = false; } + private slots: + void hideInsertPopupMenu(); + void timeoutHideInsertPopupMenu(); + +/// TOOL TIPS: + protected: + void maybeTip(const QPoint &pos); + +/// ANIMATIONS: + private: + QValueList<Note*> m_animatedNotes; + QTimer m_animationTimer; + int m_deltaY; + QTime m_lastFrameTime; + static const int FRAME_DELAY; + private slots: + void animateObjects(); + public slots: + void animateLoad(); + public: + void addAnimatedNote(Note *note); + +/// LOAD AND SAVE: + private: + bool m_loaded; + bool m_loadingLaunched; + bool m_locked; + bool m_shouldConvertPlainTextNotes; + QFrame* m_decryptBox; + QPushButton* m_button; + int m_encryptionType; + QString m_encryptionKey; +#ifdef HAVE_LIBGPGME + KGpgMe* m_gpg; +#endif + QTimer m_inactivityAutoLockTimer; + void enableActions(); + + private slots: + void loadNotes(const QDomElement ¬es, Note *parent); + void saveNotes(QDomDocument &document, QDomElement &element, Note *parent); + void unlock(); +protected slots: + void inactivityAutoLockTimeout(); +public slots: + void load(); + void loadProperties(const QDomElement &properties); + void saveProperties(QDomDocument &document, QDomElement &properties); + bool save(); + public: + bool isEncrypted(); + bool isFileEncrypted(); + bool isLocked() { return m_locked; }; + void lock(); + bool isLoaded() { return m_loaded; }; + bool loadingLaunched() { return m_loadingLaunched; }; + bool loadFromFile(const QString &fullPath, QString* string, bool isLocalEncoding = false); + bool loadFromFile(const QString &fullPath, QByteArray* array); + bool saveToFile(const QString& fullPath, const QByteArray& array); + bool saveToFile(const QString& fullPath, const QByteArray& array, Q_ULONG length); + bool saveToFile(const QString& fullPath, const QString& string, bool isLocalEncoding = false); + static bool safelySaveToFile(const QString& fullPath, const QByteArray& array); + static bool safelySaveToFile(const QString& fullPath, const QByteArray& array, Q_ULONG length); + static bool safelySaveToFile(const QString& fullPath, const QString& string, bool isLocalEncoding = false); + bool setProtection(int type, QString key); + int encryptionType() { return m_encryptionType; }; + QString encryptionKey(){ return m_encryptionKey; }; + bool saveAgain(); + +/// BACKGROUND: + private: + QColor m_backgroundColorSetting; + QString m_backgroundImageName; + QPixmap *m_backgroundPixmap; + QPixmap *m_opaqueBackgroundPixmap; + QPixmap *m_selectedBackgroundPixmap; + bool m_backgroundTiled; + QColor m_textColorSetting; + public: + inline bool hasBackgroundImage() { return m_backgroundPixmap != 0; } + inline const QPixmap* backgroundPixmap() { return m_backgroundPixmap; } + inline bool isTiledBackground() { return m_backgroundTiled; } + inline QString backgroundImageName() { return m_backgroundImageName; } + inline QColor backgroundColorSetting() { return m_backgroundColorSetting; } + inline QColor textColorSetting() { return m_textColorSetting; } + QColor backgroundColor(); + QColor textColor(); + void setAppearance(const QString &icon, const QString &name, const QString &backgroundImage, const QColor &backgroundColor, const QColor &textColor); + void blendBackground(QPainter &painter, const QRect &rect, int xPainter = -1, int yPainter = -1, bool opaque = false, QPixmap *bg = 0); + void unbufferizeAll(); + void subscribeBackgroundImages(); + void unsubscribeBackgroundImages(); + +/// KEYBOARD SHORTCUT: + public: // Temporar: for deletion purpose + KAction *m_action; + private: + int m_shortcutAction; + private slots: + void activatedShortcut(); + public: + KShortcut shortcut() { return m_action->shortcut(); } + int shortcutAction() { return m_shortcutAction; } + void setShortcut(KShortcut shortcut, int action); + +/// USER INTERACTION: + private: + Note *m_hoveredNote; + int m_hoveredZone; + bool m_lockedHovering; + bool m_underMouse; + QRect m_inserterRect; + bool m_inserterShown; + bool m_inserterSplit; + bool m_inserterTop; + bool m_inserterGroup; + void placeInserter(Note *note, int zone); + void removeInserter(); + public: +// bool inserterShown() { return m_inserterShown; } + bool inserterSplit() { return m_inserterSplit; } + bool inserterGroup() { return m_inserterGroup; } + public slots: + void doHoverEffects(Note *note, Note::Zone zone, const QPoint &pos = QPoint(0, 0)); /// << @p pos is optionnal and only used to show the link target in the statusbar + void doHoverEffects(const QPoint &pos); + void doHoverEffects(); // The same, but using the current cursor position + void mouseEnteredEditorWidget(); + public: + void popupTagsMenu(Note *note); + void popupEmblemMenu(Note *note, int emblemNumber); + void addTagToSelectedNotes(Tag *tag); + void removeTagFromSelectedNotes(Tag *tag); + void removeAllTagsFromSelectedNotes(); + void addStateToSelectedNotes(State *state); + void changeStateOfSelectedNotes(State *state); + bool selectedNotesHaveTags(); + const QRect& inserterRect() { return m_inserterRect; } + bool inserterShown() { return m_inserterShown; } + void drawInserter(QPainter &painter, int xPainter, int yPainter); + DecoratedBasket* decoration(); + State *stateForTagFromSelectedNotes(Tag *tag); + public slots: + void activatedTagShortcut(Tag *tag); + void recomputeAllStyles(); + void removedStates(const QValueList<State*> &deletedStates); + private slots: + void toggledTagInMenu(int id); + void toggledStateInMenu(int id); + void unlockHovering(); + void disableNextClick(); + void contentsMoved(); + public: + Note *m_tagPopupNote; + private: + Tag *m_tagPopup; + QTime m_lastDisableClick; + +/// SELECTION: + private: + bool m_isSelecting; + bool m_selectionStarted; + bool m_selectionInvert; + QPoint m_selectionBeginPoint; + QPoint m_selectionEndPoint; + QRect m_selectionRect; + QTimer m_autoScrollSelectionTimer; + void stopAutoScrollSelection(); + private slots: + void doAutoScrollSelection(); + public: + inline bool isSelecting() { return m_isSelecting; } + inline const QRect& selectionRect() { return m_selectionRect; } + void selectNotesIn(const QRect &rect, bool invertSelection, bool unselectOthers = true); + void resetWasInLastSelectionRect(); + void selectAll(); + void unselectAll(); + void invertSelection(); + void unselectAllBut(Note *toSelect); + void invertSelectionOf(Note *toSelect); + QColor selectionRectInsideColor(); + Note* theSelectedNote(); + NoteSelection* selectedNotes(); + +/// BLANK SPACES DRAWING: + private: + QValueList<QRect> m_blankAreas; + void recomputeBlankRects(); + QWidget *m_cornerWidget; + +/// COMMUNICATION WITH ITS CONTAINER: + signals: + void postMessage(const QString &message); /// << Post a temporar message in the statusBar. + void setStatusBarText(const QString &message); /// << Set the permanent statusBar text or reset it if message isEmpty(). + void resetStatusBarText(); /// << Equivalent to setStatusBarText(""). + void propertiesChanged(Basket *basket); + void countsChanged(Basket *basket); + public slots: + void linkLookChanged(); + void signalCountsChanged(); + private: + QTimer m_timerCountsChanged; + private slots: + void countsChangedTimeOut(); + +/// NOTES COUNTING: + public: + void addSelectedNote() { ++m_countSelecteds; signalCountsChanged(); } + void removeSelectedNote() { --m_countSelecteds; signalCountsChanged(); } + void resetSelectedNote() { m_countSelecteds = 0; signalCountsChanged(); } // FIXME: Useful ??? + int count() { return m_count; } + int countFounds() { return m_countFounds; } + int countSelecteds() { return m_countSelecteds; } + private: + int m_count; + int m_countFounds; + int m_countSelecteds; + +/// PROPERTIES: + public: + QString basketName() { return m_basketName; } + QString icon() { return m_icon; } + QString folderName() { return m_folderName; } + QString fullPath(); + QString fullPathForFileName(const QString &fileName); // Full path of an [existing or not] note in this basket + static QString fullPathForFolderName(const QString &folderName); + private: + QString m_basketName; + QString m_icon; + QString m_folderName; + +/// ACTIONS ON SELECTED NOTES FROM THE INTERFACE: + public slots: + void noteEdit(Note *note = 0L, bool justAdded = false, const QPoint &clickedPoint = QPoint()); + void showEditedNoteWhileFiltering(); + void noteDelete(); + void noteDeleteWithoutConfirmation(bool deleteFilesToo = true); + void noteCopy(); + void noteCut(); + void noteOpen(Note *note = 0L); + void noteOpenWith(Note *note = 0L); + void noteSaveAs(); + void noteGroup(); + void noteUngroup(); + void noteMoveOnTop(); + void noteMoveOnBottom(); + void noteMoveNoteUp(); + void noteMoveNoteDown(); + void moveSelectionTo(Note *here, bool below); + public: + enum CopyMode { CopyToClipboard, CopyToSelection, CutToClipboard }; + void doCopy(CopyMode copyMode); + bool selectionIsOneGroup(); + Note* selectedGroup(); + Note* firstSelected(); + Note* lastSelected(); + +/// NOTES EDITION: + private: + NoteEditor *m_editor; + //QWidget *m_rightEditorBorder; + TransparentWidget *m_leftEditorBorder; + TransparentWidget *m_rightEditorBorder; + bool m_redirectEditActions; + int m_editorWidth; + int m_editorHeight; + QTimer m_inactivityAutoSaveTimer; + bool m_doNotCloseEditor; + int m_editParagraph; + int m_editIndex; + public: + bool isDuringEdit() { return m_editor; } + bool redirectEditActions() { return m_redirectEditActions; } + bool hasTextInEditor(); + bool hasSelectedTextInEditor(); + bool selectedAllTextInEditor(); + Note* editedNote(); + protected slots: + void selectionChangedInEditor(); + void contentChangedInEditor(); + void inactivityAutoSaveTimeout(); + public slots: + void editorCursorPositionChanged(); + private: + int m_editorX; + int m_editorY; + public slots: + void placeEditor(bool andEnsureVisible = false); + void placeEditorAndEnsureVisible(); + bool closeEditor(); + void closeEditorDelayed(); + void updateEditorAppearance(); + void editorPropertiesChanged(); + void openBasket(); + void closeBasket(); + +/// FILTERING: + public slots: + void newFilter(const FilterData &data, bool andEnsureVisible = true); + void cancelFilter(); + void validateFilter(); + void filterAgain(bool andEnsureVisible = true); + void filterAgainDelayed(); + bool isFiltering(); + +/// DRAG AND DROP: + private: + bool m_isDuringDrag; + QValueList<Note*> m_draggedNotes; + public: + static void acceptDropEvent(QDropEvent *event, bool preCond = true); + void contentsDropEvent(QDropEvent *event); + void blindDrop(QDropEvent* event); + bool isDuringDrag() { return m_isDuringDrag; } + QValueList<Note*> draggedNotes() { return m_draggedNotes; } + protected: + void contentsDragEnterEvent(QDragEnterEvent*); + void contentsDragMoveEvent(QDragMoveEvent *event); + void contentsDragLeaveEvent(QDragLeaveEvent*); + public slots: + void slotCopyingDone2(KIO::Job *job); + public: + Note* noteForFullPath(const QString &path); + +/// EXPORTATION: + public: + QValueList<State*> usedStates(); + static QString saveGradientBackground(const QColor &color, const QFont &font, const QString &folder); + + public: + void listUsedTags(QValueList<Tag*> &list); + +/// MANAGE FOCUS: + private: + Note *m_focusedNote; + public: + void setFocusedNote(Note *note); + void focusANote(); + void focusANonSelectedNoteAbove(bool inSameColumn); + void focusANonSelectedNoteBelow(bool inSameColumn); + void focusANonSelectedNoteBelowOrThenAbove(); + void focusANonSelectedNoteAboveOrThenBelow(); + Note* focusedNote() { return m_focusedNote; } + Note* firstNoteInStack(); + Note* lastNoteInStack(); + Note* firstNoteShownInStack(); + Note* lastNoteShownInStack(); + void selectRange(Note *start, Note *end, bool unselectOthers = true); /// FIXME: Not really a focus related method! + void ensureNoteVisible(Note *note); + virtual void keyPressEvent(QKeyEvent *event); + virtual void focusInEvent(QFocusEvent*); + virtual void focusOutEvent(QFocusEvent*); + QRect noteVisibleRect(Note *note); // clipped global (desktop as origin) rectangle + Note* firstNoteInGroup(); + Note *noteOnHome(); + Note *noteOnEnd(); + + enum NoteOn { LEFT_SIDE = 1, RIGHT_SIDE, TOP_SIDE, BOTTOM_SIDE }; + Note* noteOn(NoteOn side); + +/// REIMPLEMENTED: + public: + void deleteFiles(); + bool convertTexts(); + + + public: + void wheelEvent(QWheelEvent *event); + + + + public: + Note *m_startOfShiftSelectionNote; + + +/// THE NEW FILE WATCHER: + private: + KDirWatch *m_watcher; + QTimer m_watcherTimer; + QValueList<QString> m_modifiedFiles; + public: + void addWatchedFile(const QString &fullPath); + void removeWatchedFile(const QString &fullPath); + private slots: + void watchedFileModified(const QString &fullPath); + void watchedFileDeleted(const QString &fullPath); + void updateModifiedNotes(); + + +/// FROM OLD ARCHITECTURE ********************** + +public slots: + + void showFrameInsertTo() {} + void resetInsertTo() {} + + void computeInsertPlace(const QPoint &/*cursorPosition*/) { } + public: + + friend class SystemTray; + +/// SPEED OPTIMIZATION + private: + bool m_finishLoadOnFirstShow; + bool m_relayoutOnNextShow; + public: + void aboutToBeActivated(); +}; + + + + + + + + + +#if 0 + +#include <qwidget.h> +#include <qscrollview.h> +#include <qclipboard.h> +#include <qptrlist.h> +#include <qtimer.h> +#include <kio/job.h> +#include <qcolor.h> + +#include "filter.h" + +class QFrame; +class QVBoxLayout; +class QCheckBox; +class QString; +class QColor; +class QPixmap; +class QAction; +class QStringList; +class QRect; + +class QDomElement; + +class KDirWatch; + +class Basket; +class Note; +class NoteEditorBase; + +/** Used to enqueue a file path when the Basket receive a file modification / creation / deletion + * It associate the file name with an event. + * All this queue will be treated later. + * TODO: rename to class WatcherEvent ? + * @author S�astien Laot + */ +class FileEvent +{ + public: + enum Event { Modified = 1, Created, Deleted, Renamed }; + FileEvent(Event evt, const QString &path) + : event(evt), filePath(path) + { } + public: // Because it must be fast and theire is no need to be private + Event event; + QString filePath; +}; + +/** Basket that contain some Notes. + * @author S�astien Laot + */ +clas s Bas ket : public QScrollView +{ + Q_OBJECT + public: + /** Construtor and destructor */ + Bask et(QWidget *parent, const QString &folderName, const char *name = "", WFlags fl = 0); + public: + protected: + virtual void contentsContextMenuEvent(QContextMenuEvent *event); + virtual void contentsMousePressEvent(QMouseEvent *event); // For redirected event !! + virtual void showEvent(QShowEvent *); + /** Drag and drop functions */ + virtual void dragEnterEvent(QDragEnterEvent*); + virtual void dragMoveEvent(QDragMoveEvent* event); + virtual void dragLeaveEvent(QDragLeaveEvent*); + public: + virtual void dropEvent(QDropEvent *event); + static void acceptDropEvent(QDropEvent *event, bool preCond = true); + bool canDragNote() { return !isEmpty(); } + void computeInsertPlace(const QPoint &cursorPosition); + Note* noteAtPosition(const QPoint &pos); + Note* duplicatedOf(Note *note); + void checkClipboard(); + void processActionAsYouType(QKeyEvent *event); + void exportToHTML(); + signals: + void nameChanged(Basket *basket, const QString &name); + void iconChanged(Basket *basket, const QString &icon); + void notesNumberChanged(Basket *basket); + public slots: + void linkLookChanged(); + void showNotesToolTipChanged(); + /** Notes manipulation */ + void insertNote(Note *note); + void delNote(Note *note, bool askForMirroredFile = true); + void changeNotePlace(Note *note); + void pasteNote(QClipboard::Mode mode = QClipboard::Clipboard); + void recolorizeNotes(); + void reloadMirroredFolder(); + void showMirrorOnlyOnceInfo(); + /** Selection of note(s) */ + void selectAll(); + void unselectAll(); + void unselectAllBut(Note *toSelect); + void invertSelection(); + void selectRange(Note *start, Note *end); + void clicked(Note *note, bool controlPressed, bool shiftPressed); + void setFocusedNote(Note *note); + void focusANote(); + void ensureVisibleNote(Note *note); + QRect noteRect(Note *note); // clipped global (desktop as origin) rectangle + /** Travel the list to find the next shown note, or the previous if step == -1, or the next after 10 if step == 10... */ + Note* nextShownNoteFrom(Note *note, int step); + /** Actions on (selected) notes */ + void editNote(Note *note, bool editAnnotations = false); + void editNote(); + void delNote(); + void copyNote(); + void cutNote(); + void openNote(); + void openNoteWith(); + void saveNoteAs(); + void moveOnTop(); + void moveOnBottom(); + void moveNoteUp(); + void moveNoteDown(); + public: + void dontCareOfCreation(const QString &path); + QString copyIcon(const QString &iconName, int size, const QString &destFolder); + QString copyFile(const QString &srcPath, const QString &destFolder, bool createIt = false); + protected slots: + void slotModifiedFile(const QString &path); + void slotCreatedFile(const QString &path); + void slotDeletedFile(const QString &path); + void slotUpdateNotes(); + void placeEditor(); + void closeEditor(bool save = true); + void clipboardChanged(bool selectionMode = false); + void selectionChanged(); + private: + QTimer m_updateTimer; + QPtrList<FileEvent> m_updateQueue; + QStringList m_dontCare; + static const int c_updateTime; + private: + void load(); // Load is performed only once, during contructor + void loadNotes(const QDomElement ¬es); + bool importLauncher(const QString &type, const QDomElement &content, const QString &runCommand, + const QString &annotations/*, bool checked*/); + void computeShownNotes(); + + private: + KDirWatch *m_watcher; + NoteEditorBase *m_editor; + QKeyEvent *m_stackedKeyEvent; +}; + +#endif // #if 0 + +#endif // BASKET_H diff --git a/src/basket.lsm b/src/basket.lsm new file mode 100644 index 0000000..4ca358b --- /dev/null +++ b/src/basket.lsm @@ -0,0 +1,16 @@ +Begin3 +Title: BasKet Note Pads -- Taking care of your ideas. +Version: 1.0.3.1 +Entered-date: +Description: +Keywords: KDE Qt Basket Note Pad Item Drop Paste Checklist Stack +Author: Sébastien Laoût <[email protected]> +Maintained-by: Sébastien Laoût <[email protected]> +Home-page: http://basket.kde.org/ +Alternate-site: +Primary-site: ftp://ftp.kde.org/pub/kde/unstable/apps/utils + xxxxxx basket-1.0.3.1.tar.gz + xxx basket-1.0.3.1.lsm +Platform: Linux. Needs KDE +Copying-policy: GPL +End diff --git a/src/basket_config_apps.desktop b/src/basket_config_apps.desktop new file mode 100644 index 0000000..66b0955 --- /dev/null +++ b/src/basket_config_apps.desktop @@ -0,0 +1,37 @@ +[Desktop Entry] +Encoding=UTF-8 +Icon=run +Type=Service +ServiceTypes=KCModule + +X-KDE-ModuleType=Library +X-KDE-Library=basket +X-KDE-FactoryName=basket_config_apps +X-KDE-HasReadOnlyMode=false +X-KDE-ParentApp=basket +X-KDE-ParentComponents=basket,kontact_basketplugin +X-KDE-CfgDlgHierarchy=Basket +X-KDE-Weight=30 + +Name=Applications +Name[de]=Anwendungen +Name[es]=Aplicaciones +Name[fr]=Applications +Name[it]=Applicazioni +Name[ja]=アプリケーション +Name[nb]=Programmer +Name[nn]=Program +Name[pt]=Aplicações +Name[ru]=Приложения +Name[tr]=Uygulamalar +Comment=Applications +Comment[de]=Anwendungen +Comment[es]=Aplicaciones +Comment[fr]=Applications +Comment[it]=Applicazioni +Comment[ja]=アプリケーション +Comment[nb]=Programmer +Comment[nn]=Program +Comment[pt]=Aplicações +Comment[ru]=Приложения +Comment[tr]=Uygulamalar diff --git a/src/basket_config_baskets.desktop b/src/basket_config_baskets.desktop new file mode 100644 index 0000000..11f2242 --- /dev/null +++ b/src/basket_config_baskets.desktop @@ -0,0 +1,37 @@ +[Desktop Entry] +Encoding=UTF-8 +Icon=basket +Type=Service +ServiceTypes=KCModule + +X-KDE-ModuleType=Library +X-KDE-Library=basket +X-KDE-FactoryName=basket_config_baskets +X-KDE-HasReadOnlyMode=false +X-KDE-ParentApp=basket +X-KDE-ParentComponents=basket,kontact_basketplugin +X-KDE-CfgDlgHierarchy=Basket +X-KDE-Weight=10 + +Name=Baskets +Name[de]=Körbe +Name[es]=Cestas +Name[fr]=Paniers +Name[it]=Canestri +Name[ja]=バスケット +Name[nb]=Kurver +Name[nn]=Korger +Name[pt]=Cestos +Name[ru]=Корзины +Name[tr]=Sepetler +Comment=Baskets +Comment[de]=Körbe +Comment[es]=Cestas +Comment[fr]=Paniers +Comment[it]=Canestri +Comment[ja]=バスケット +Comment[nb]=Kurver +Comment[nn]=Korger +Comment[pt]=Cestos +Comment[ru]=Корзины +Comment[tr]=Sepetler
\ No newline at end of file diff --git a/src/basket_config_features.desktop b/src/basket_config_features.desktop new file mode 100644 index 0000000..dceae61 --- /dev/null +++ b/src/basket_config_features.desktop @@ -0,0 +1 @@ +# Was there in 0.6.0 Beta versions. Should not be used anymore. diff --git a/src/basket_config_general.desktop b/src/basket_config_general.desktop new file mode 100644 index 0000000..24d8534 --- /dev/null +++ b/src/basket_config_general.desktop @@ -0,0 +1,37 @@ +[Desktop Entry] +Encoding=UTF-8 +Icon=configure +Type=Service +ServiceTypes=KCModule + +X-KDE-ModuleType=Library +X-KDE-Library=basket +X-KDE-FactoryName=basket_config_general +X-KDE-HasReadOnlyMode=false +X-KDE-ParentApp=basket +X-KDE-ParentComponents=basket,kontact_basketplugin +X-KDE-CfgDlgHierarchy=Basket +X-KDE-Weight=10 + +Name=General +Name[de]=Allgemein +Name[es]=General +Name[fr]=Général +Name[it]=Generale +Name[ja]=全般 +Name[nb]=Generelt +Name[nn]=Generelt +Name[pt]=Geral +Name[ru]=Общие +Name[tr]=Genel +Comment=General +Comment[de]=Allgemein +Comment[es]=General +Comment[fr]=Général +Comment[it]=Generale +Comment[ja]=全般 +Comment[nb]=Generelt +Comment[nn]=Generelt +Comment[pt]=Geral +Comment[ru]=Общие +Comment[de]=Genel
\ No newline at end of file diff --git a/src/basket_config_new_notes.desktop b/src/basket_config_new_notes.desktop new file mode 100644 index 0000000..87474e9 --- /dev/null +++ b/src/basket_config_new_notes.desktop @@ -0,0 +1,37 @@ +[Desktop Entry] +Encoding=UTF-8 +Icon=filenew +Type=Service +ServiceTypes=KCModule + +X-KDE-ModuleType=Library +X-KDE-Library=basket +X-KDE-FactoryName=basket_config_new_notes +X-KDE-HasReadOnlyMode=false +X-KDE-ParentApp=basket +X-KDE-ParentComponents=basket,kontact_basketplugin +X-KDE-CfgDlgHierarchy=Basket +X-KDE-Weight=10 + +Name=New Notes +Name[de]=Neue Notizen +Name[es]=Notas nuevas +Name[fr]=Nouvelles notes +Name[it]=Nuove note +Name[ja]=新しいメモ +Name[nb]=Nye notat +Name[nn]=Nye notat +Name[pt]=Anotações novas +Name[ru]=Новые заметки +Name[tr]=Yeni Notlar +Comment=New Notes +Comment[de]=Neue Notizen +Comment[es]=Notas nuevas +Comment[fr]=Nouvelles notes +Comment[it]=Nuove note +Comment[ja]=新しいメモ +Comment[nb]=Nye notat +Comment[nn]=Nye notat +Comment[pt]=Anotações novas +Comment[ru]=Новые заметки +Comment[tr]=Yeni Notlar
\ No newline at end of file diff --git a/src/basket_config_notes.desktop b/src/basket_config_notes.desktop new file mode 100644 index 0000000..dceae61 --- /dev/null +++ b/src/basket_config_notes.desktop @@ -0,0 +1 @@ +# Was there in 0.6.0 Beta versions. Should not be used anymore. diff --git a/src/basket_config_notes_appearance.desktop b/src/basket_config_notes_appearance.desktop new file mode 100644 index 0000000..676cac4 --- /dev/null +++ b/src/basket_config_notes_appearance.desktop @@ -0,0 +1,35 @@ +[Desktop Entry] +Encoding=UTF-8 +Icon=colorize +Type=Service +ServiceTypes=KCModule + +X-KDE-ModuleType=Library +X-KDE-Library=basket +X-KDE-FactoryName=basket_config_notes_appearance +X-KDE-HasReadOnlyMode=false +X-KDE-ParentApp=basket +X-KDE-ParentComponents=basket,kontact_basketplugin +X-KDE-CfgDlgHierarchy=Basket +X-KDE-Weight=10 + +Name=Notes Appearance +Name[de]=Erscheinungsbild +Name[es]=Apariencia de las notas +Name[fr]=Apparence des notes +Name[it]=Aspetto delle note +Name[ja]=メモの外観 +Name[nb]=Notatutseende +Name[nn]=Notatutsjånad +Name[ru]=Вид +Name[tr]=Not Görünümleri +Comment=Notes Appearance +Comment[de]=Erscheinungsbild der Notizen +Comment[es]=Apariencia de las notas +Comment[fr]=Apparence des notes +Comment[it]=Aspetto delle note +Comment[ja]=メモの外観 +Comment[nb]=Notatutseende +Comment[nn]=Notatutsjånad +Comment[ru]=Вид +Comment[tr]=Not Görünümleri
\ No newline at end of file diff --git a/src/basket_options.h b/src/basket_options.h new file mode 100644 index 0000000..f8bbf82 --- /dev/null +++ b/src/basket_options.h @@ -0,0 +1,41 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BASKET_OPTIONS_H +#define BASKET_OPTIONS_H + +#include <kcmdlineargs.h> +#include <klocale.h> + +KCmdLineOptions basket_options[] = +{ + { "d", 0, 0 }, + { "debug", I18N_NOOP("Show the debug window"), 0 }, + { "f", 0, 0 }, + { "data-folder <folder>", I18N_NOOP("Custom folder where to load and save basket data and application data (useful for debugging purpose)"), 0 }, + { "h", 0, 0 }, + { "start-hidden", I18N_NOOP("Hide the main window in the system tray icon on startup"), 0 }, + { "k", 0, 0 }, + { "use-drkonquy", I18N_NOOP("When crashing, use the standard KDE report dialog instead of sending an email"), 0 }, + { "+[file]", I18N_NOOP("Open basket archive or template"), 0 }, + KCmdLineLastOption +}; + +#endif // BASKET_OPTIONS_H diff --git a/src/basket_part.cpp b/src/basket_part.cpp new file mode 100644 index 0000000..0ba1e50 --- /dev/null +++ b/src/basket_part.cpp @@ -0,0 +1,102 @@ +/*************************************************************************** + * Copyright (C) 2003 by Petri Damsten * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "basket_part.h" + +#include <kinstance.h> +#include <kaction.h> +#include <kstdaction.h> +#include <kfiledialog.h> +#include <kglobal.h> +#include <klocale.h> +#include <bnpview.h> +#include <aboutdata.h> +#include <kparts/genericfactory.h> +#include <kparts/statusbarextension.h> +#include "basketstatusbar.h" + +typedef KParts::GenericFactory< BasketPart > BasketFactory; +K_EXPORT_COMPONENT_FACTORY( libbasketpart, BasketFactory ) + +BasketPart::BasketPart( QWidget *parentWidget, const char *, + QObject *parent, const char *name, const QStringList & ) + : KParts::ReadWritePart(parent, name) +{ + // we need an instance + setInstance( BasketFactory::instance() ); + + BasketStatusBar* bar = new BasketStatusBar(new KParts::StatusBarExtension(this)); + // this should be your custom internal widget + m_view = new BNPView(parentWidget, "BNPViewPart", this, actionCollection(), bar); + connect(m_view, SIGNAL(setWindowCaption(const QString &)), this, SLOT(setCaption(const QString &))); + connect(m_view, SIGNAL(showPart()), this, SIGNAL(showPart())); + m_view->setFocusPolicy(QWidget::ClickFocus); + + // notify the part that this is our internal widget + setWidget(m_view); + + // set our XML-UI resource file + setXMLFile("basket_part.rc"); + + // we are read-write by default + setReadWrite(true); + + // we are not modified since we haven't done anything yet + setModified(false); +} + +BasketPart::~BasketPart() +{} + +void BasketPart::setReadWrite(bool rw) +{ + // TODO: notify your internal widget of the read-write state + ReadWritePart::setReadWrite(rw); +} + +void BasketPart::setModified(bool modified) +{ + // in any event, we want our parent to do it's thing + ReadWritePart::setModified(modified); +} + +bool BasketPart::openFile() +{ + // TODO + return false; +} + +bool BasketPart::saveFile() +{ + //TODO + return false; +} + +KAboutData *BasketPart::createAboutData() +{ + return new AboutData(); +} + +void BasketPart::setCaption(const QString &caption) +{ + emit setWindowCaption(caption); +} + +#include "basket_part.moc" diff --git a/src/basket_part.desktop b/src/basket_part.desktop new file mode 100644 index 0000000..789474d --- /dev/null +++ b/src/basket_part.desktop @@ -0,0 +1,21 @@ +[Desktop Entry] +Encoding=UTF-8 +Name=basketPart +Name[br]=Perzhbasket +Name[ca]=Part per a basket +Name[el]=Τμήμαbasket +Name[es]=Componente de Basket +Name[et]=basket komponent +Name[it]=Componente basket +Name[ja]=basket パート +Name[nb]=kurvDel +Name[nn]=korgDel +Name[pt_BR]=Parte basket +Name[sv]=basket-del +Name[ta]=basketபாகம் +Name[tr]=basket Bileşeni +Name[ru]=Модуль basket +MimeType=application/x-basket; +ServiceTypes=KParts/ReadOnlyPart,KParts/ReadWritePart +X-KDE-Library=libbasketpart +Type=Service diff --git a/src/basket_part.h b/src/basket_part.h new file mode 100644 index 0000000..6d9dc70 --- /dev/null +++ b/src/basket_part.h @@ -0,0 +1,91 @@ +/*************************************************************************** + * Copyright (C) 2003 by Petri Damsten * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef _BASKETPART_H_ +#define _BASKETPART_H_ + +#include <kparts/part.h> +#include <kparts/factory.h> + +class QWidget; +class QPainter; +class KURL; +class QMultiLineEdit; +class BNPView; + +/** + * This is a "Part". It that does all the real work in a KPart + * application. + * + * @short Main Part + * @author Petri Damsten <[email protected]> + * @version 0.1 + */ +class BasketPart : public KParts::ReadWritePart +{ + Q_OBJECT +public: + /** + * Default constructor + */ + BasketPart(QWidget *parentWidget, const char *widgetName, + QObject *parent, const char *name, const QStringList &); + + /** + * Destructor + */ + virtual ~BasketPart(); + + /** + * This is a virtual function inherited from KParts::ReadWritePart. + * A shell will use this to inform this Part if it should act + * read-only + */ + virtual void setReadWrite(bool rw); + + /** + * Reimplemented to disable and enable Save action + */ + virtual void setModified(bool modified); + + static KAboutData *createAboutData(); + + signals: + void showPart(); + + protected: + /** + * This must be implemented by each part + */ + virtual bool openFile(); + + /** + * This must be implemented by each read-write part + */ + virtual bool saveFile(); + + protected slots: + void setCaption(const QString &caption); + + private: + BNPView *m_view; +}; + +#endif // _BASKETPART_H_ diff --git a/src/basket_part.rc b/src/basket_part.rc new file mode 100644 index 0000000..a73a0fe --- /dev/null +++ b/src/basket_part.rc @@ -0,0 +1,229 @@ +<!DOCTYPE kpartgui SYSTEM "kpartgui.dtd"> +<kpartgui version="1.0.3.1" name="basket" > + <MenuBar noMerge="1" > + <Menu name="file" > + <text>&Basket</text> + <Action name="basket_new_menu" /> + <Separator/> + <Action name="basket_properties" /> + <Menu icon="fileexport" > + <text>&Export</text> + <Action name="basket_export_basket_archive" /> + <Action name="basket_export_html" /> + </Menu> + <Action name="basket_remove" /> + <Separator/> + <Action name="basket_password" /> + <Action name="basket_lock" /> + <Separator/> + <Menu name="fileimport" icon="fileimport" > + <text>&Import</text> + <Action name="basket_import_basket_archive" /> + <Separator/> + <Action name="basket_import_knotes" /> + <Action name="basket_import_kjots" /> + <Action name="basket_import_knowit" /> + <Action name="basket_import_tuxcards" /> + <Separator/> + <Action name="basket_import_sticky_notes" /> + <Action name="basket_import_tomboy" /> + <Separator/> + <Action name="basket_import_text_file" /> + </Menu> + <Action name="basket_backup_restore" /> + </Menu> + <Menu name="edit" > + <text>&Edit</text> + <Action name="edit_cut" /> + <Action name="edit_copy" /> + <Action name="edit_paste" /> + <Action name="edit_delete" /> + <Separator/> + <Action name="edit_select_all" /> + <Action name="edit_unselect_all" /> + <Action name="edit_invert_selection" /> + <Separator/> + <Action name="edit_filter" /> + <Action name="edit_filter_all_baskets" /> + <Action name="edit_filter_reset" /> + </Menu> + <Menu name="go" > + <text>&Go</text> + <Action name="go_basket_previous" /> + <Action name="go_basket_next" /> + <Action name="go_basket_fold" /> + <Action name="go_basket_expand" /> + </Menu> + <Menu name="note" > + <text>&Note</text> + <Action name="note_edit" /> + <Action name="note_open" /> + <Action name="note_open_with" /> + <Action name="note_save_to_file" /> + <Separator/> + <Action name="note_group" /> + <Action name="note_ungroup" /> + <Separator/> + <Action name="note_move_top" /> + <Action name="note_move_up" /> + <Action name="note_move_down" /> + <Action name="note_move_bottom" /> + </Menu> + <Menu name="tags" > + <text>&Tags</text> + </Menu> + <Menu name="insert" > + <text>&Insert</text> + <Action name="insert_html" /> + <Action name="insert_image" /> + <Action name="insert_link" /> + <Action name="insert_launcher" /> + <Action name="insert_color" /> + <Separator/> + <Action name="insert_screen_capture" /> + <Action name="insert_screen_color" /> + <Separator/> + <Action name="insert_from_file" /> + <Action name="insert_kmenu" /> + <Action name="insert_icon" /> + </Menu> + <Menu name="settings" > + <text>&Settings</text> + <Merge name="StandardToolBarMenuHandler" /> + <Action name="options_show_statusbar" /> + <Separator/> + <Action name="options_configure_keybinding" /> + <Action name="options_configure_global_keybinding" /> + <Action name="options_configure_toolbars" /> + <Action name="options_configure_notifications" /> + <Action name="options_configure" /> + </Menu> + <Menu name="help" > + <text>&Help</text> + <Action name="help_contents" /> + <Action name="help_plugins_contents" /> + <Action name="help_whats_this" /> + <Action name="help_show_tip" /> + <Separator/> + <Action name="help_welcome_baskets" /> +<!-- <Action name="help_report_bug" />--> + <Action name="likeback_send_a_comment" /> + <Separator/> + <Action name="help_about_app" /> + <Action name="help_about_kde" /> + </Menu> + </MenuBar> + <ToolBar newline="false" position="Top" noMerge="1" name="mainToolBar" fullWidth="true" > + <text>Main Toolbar</text> + <Action name="basket_properties" /> + <Separator lineSeparator="true" name="separator_0" /> + <Action name="edit_cut" /> + <Action name="edit_copy" /> + <Action name="edit_paste" /> + <Action name="edit_delete" /> + <Separator lineSeparator="true" name="separator_1" /> + <Action name="note_group" /> + </ToolBar> + <ToolBar newline="false" position="Top" noMerge="1" name="richTextEditToolBar" fullWidth="true" > + <text>Text Formating Toolbar</text> + <Action name="richtext_undo" /> + <Action name="richtext_redo" /> + <Separator lineSeparator="true" name="separator_1" /> + <Action name="richtext_font" /> + <Separator lineSeparator="false" name="separator_2" /> + <Action name="richtext_font_size" /> + <Separator lineSeparator="false" name="separator_3" /> + <Action name="richtext_color" /> + <Separator lineSeparator="true" name="separator_4" /> + <Action name="richtext_bold" /> + <Action name="richtext_italic" /> + <Action name="richtext_underline" /> +<!-- + <Separator lineSeparator="true" name="separator_2" /> + <Action name="richtext_super" /> + <Action name="richtext_sub" /> +--> + <Separator lineSeparator="true" name="separator_3" /> + <Action name="richtext_left" /> + <Action name="richtext_center" /> + <Action name="richtext_right" /> + <Action name="richtext_block" /> + </ToolBar> + <Menu name="basket_popup" > + <Action name="basket_new_menu" /> + <Separator/> + <Action name="basket_properties" /> + <Menu icon="fileexport" > + <text>&Export</text> + <Action name="basket_export_basket_archive" /> + <Action name="basket_export_html" /> + </Menu> + <Action name="basket_remove" /> + <Separator/> + <Action name="basket_password" /> + <Action name="basket_lock" /> + <Separator/> + <Menu icon="fileimport" > + <text>&Import</text> + <Action name="basket_import_basket_archive" /> + <Separator/> + <Action name="basket_import_knotes" /> + <Action name="basket_import_kjots" /> + <Action name="basket_import_knowit" /> + <Action name="basket_import_tuxcards" /> + <Separator/> + <Action name="basket_import_sticky_notes" /> + <Action name="basket_import_tomboy" /> + <Separator/> + <Action name="basket_import_text_file" /> + </Menu> + <Action name="basket_backup_restore" /> + </Menu> + <Menu name="tab_bar_popup" > + <Action name="basket_new" /> + <Menu icon="fileimport" > + <text>&Import</text> + <Action name="basket_import_basket_archive" /> + <Separator/> + <Action name="basket_import_knotes" /> + <Action name="basket_import_kjots" /> + <Action name="basket_import_knowit" /> + <Action name="basket_import_tuxcards" /> + <Separator/> + <Action name="basket_import_sticky_notes" /> + <Action name="basket_import_tomboy" /> + <Separator/> + <Action name="basket_import_text_file" /> + </Menu> + <Action name="basket_backup_restore" /> + </Menu> + <Menu noMerge="1" name="note_popup" > + <Action name="note_edit" /> + <Action name="note_open" /> + <Action name="note_open_with" /> + <Action name="note_save_to_file" /> + <Separator/> + <Action name="note_group" /> + <Action name="note_ungroup" /> + <Separator/> + <Action name="edit_cut" /> + <Action name="edit_copy" /> + <Action name="edit_delete" /> + </Menu> + <Menu noMerge="1" name="insert_popup" > + <Action name="insert_html" /> + <Action name="insert_image" /> + <Action name="insert_link" /> + <Action name="insert_launcher" /> + <Action name="insert_color" /> + <Separator/> + <Action name="insert_screen_capture" /> + <Action name="insert_screen_color" /> + <Separator/> + <Action name="insert_from_file" /> + <Action name="insert_kmenu" /> + <Action name="insert_icon" /> + <Separator/> + <Action name="edit_paste" /> + </Menu> +</kpartgui> diff --git a/src/basketdcopiface.h b/src/basketdcopiface.h new file mode 100644 index 0000000..718bcf9 --- /dev/null +++ b/src/basketdcopiface.h @@ -0,0 +1,37 @@ +/*************************************************************************** + * Copyright (C) 2003 by Petri Damsten * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BASKETDCOPINTERFACE_H +#define BASKETDCOPINTERFACE_H + +#include <dcopobject.h> + +/** + @author Petri Damsten <[email protected]> +*/ +class BasketDcopInterface : virtual public DCOPObject +{ + K_DCOP + k_dcop: + virtual ASYNC newBasket() = 0; + virtual void handleCommandLine() = 0; +}; + +#endif diff --git a/src/basketfactory.cpp b/src/basketfactory.cpp new file mode 100644 index 0000000..4006fbb --- /dev/null +++ b/src/basketfactory.cpp @@ -0,0 +1,159 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qdir.h> +#include <klocale.h> +#include <kmessagebox.h> + +#include "basketfactory.h" +#include "global.h" +#include "basket.h" +#include "xmlwork.h" +#include "note.h" // For balanced column width computation +#include "bnpview.h" + +/** BasketFactory */ + +// TODO: Don't create a basket with a name that already exists! + +QString BasketFactory::newFolderName() +{ + QString folderName; + QString fullPath; + QDir dir; + + for (int i = 1; ; ++i) { + folderName = "basket" + QString::number(i) + "/"; + fullPath = Global::basketsFolder() + folderName; + dir = QDir(fullPath); + if ( ! dir.exists() ) // OK : The folder do not yet exists : + break; // We've found one ! + } + + return folderName; +} + +QString BasketFactory::unpackTemplate(const QString &templateName) +{ + // Find a name for a new folder and create it: + QString folderName = newFolderName(); + QString fullPath = Global::basketsFolder() + folderName; + QDir dir; + if (!dir.mkdir(fullPath)) { + KMessageBox::error(/*parent=*/0, i18n("Sorry, but the folder creation for this new basket has failed."), i18n("Basket Creation Failed")); + return ""; + } + + // Unpack the template file to that folder: + // TODO: REALLY unpack (this hand-creation is temporary, or it could be used in case the template can't be found) + QFile file(fullPath + "/.basket"); + if (file.open(IO_WriteOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); + int nbColumns = (templateName == "mindmap" || templateName == "free" ? 0 : templateName.left(1).toInt()); + Basket *currentBasket = Global::bnpView->currentBasket(); + int columnWidth = (currentBasket && nbColumns > 0 ? (currentBasket->visibleWidth() - (nbColumns-1)*Note::RESIZER_WIDTH) / nbColumns : 0); + stream << QString( "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + "<!DOCTYPE basket>\n" + "<basket>\n" + " <properties>\n" + " <disposition mindMap=\"%1\" columnCount=\"%2\" free=\"%3\" />\n" + " </properties>\n" + " <notes>\n" ).arg( (templateName == "mindmap" ? "true" : "false"), + QString::number(nbColumns), + (templateName == "free" || templateName == "mindmap" ? "true" : "false") ); + if (nbColumns > 0) + for (int i = 0; i < nbColumns; ++i) + stream << QString(" <group width=\"%1\"></group>\n").arg(columnWidth); + stream << " </notes>\n" + "</basket>\n"; + file.close(); + return folderName; + } else { + KMessageBox::error(/*parent=*/0, i18n("Sorry, but the template copying for this new basket has failed."), i18n("Basket Creation Failed")); + return ""; + } +} + +void BasketFactory::newBasket(const QString &icon, + const QString &name, + const QString &backgroundImage, + const QColor &backgroundColor, + const QColor &textColor, + const QString &templateName, + Basket *parent) +{ + // Unpack the templateName file to a new basket folder: + QString folderName = unpackTemplate(templateName); + if (folderName.isEmpty()) + return; + + // Read the properties, change those that should be customized and save the result: + QDomDocument *document = XMLWork::openFile("basket", Global::basketsFolder() + folderName + "/.basket"); + if (!document) { + KMessageBox::error(/*parent=*/0, i18n("Sorry, but the template customization for this new basket has failed."), i18n("Basket Creation Failed")); + return; + } + QDomElement properties = XMLWork::getElement(document->documentElement(), "properties"); + + if (!icon.isEmpty()) { + QDomElement iconElement = XMLWork::getElement(properties, "icon"); + if (!iconElement.tagName().isEmpty()) // If there is already an icon, remove it since we will add our own value below + iconElement.removeChild(iconElement.firstChild()); + XMLWork::addElement(*document, properties, "icon", icon); + } + + if (!name.isEmpty()) { + QDomElement nameElement = XMLWork::getElement(properties, "name"); + if (!nameElement.tagName().isEmpty()) // If there is already a name, remove it since we will add our own value below + nameElement.removeChild(nameElement.firstChild()); + XMLWork::addElement(*document, properties, "name", name); + } + + if (backgroundColor.isValid()) { + QDomElement appearanceElement = XMLWork::getElement(properties, "appearance"); + if (appearanceElement.tagName().isEmpty()) { // If there is not already an appearance tag, add it since we will access it below + appearanceElement = document->createElement("appearance"); + properties.appendChild(appearanceElement); + } + appearanceElement.setAttribute("backgroundColor", backgroundColor.name()); + } + + if (!backgroundImage.isEmpty()) { + QDomElement appearanceElement = XMLWork::getElement(properties, "appearance"); + if (appearanceElement.tagName().isEmpty()) { // If there is not already an appearance tag, add it since we will access it below + appearanceElement = document->createElement("appearance"); + properties.appendChild(appearanceElement); + } + appearanceElement.setAttribute("backgroundImage", backgroundImage); + } + + if (textColor.isValid()) { + QDomElement appearanceElement = XMLWork::getElement(properties, "appearance"); + if (appearanceElement.tagName().isEmpty()) { // If there is not already an appearance tag, add it since we will access it below + appearanceElement = document->createElement("appearance"); + properties.appendChild(appearanceElement); + } + appearanceElement.setAttribute("textColor", textColor.name()); + } + + // Load it in the parent basket (it will save the tree and switch to this new basket): + Global::bnpView->loadNewBasket(folderName, properties, parent); +} diff --git a/src/basketfactory.h b/src/basketfactory.h new file mode 100644 index 0000000..26ae8ed --- /dev/null +++ b/src/basketfactory.h @@ -0,0 +1,46 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BASKETFACTORY_H +#define BASKETFACTORY_H + +class QString; + +class Basket; + +/** Methods to create various baskets (mkdir, init the properties and load it). + * @author S�bastien Lao�t + */ +namespace BasketFactory +{ + /** You should use this method to create a new basket: */ + void newBasket(const QString &icon, + const QString &name, + const QString &backgroundImage, + const QColor &backgroundColor, + const QColor &textColor, + const QString &templateName, + Basket *parent); + /** Internal tool methods to process the method above: */ + QString newFolderName(); + QString unpackTemplate(const QString &templateName); +} + +#endif // BASKETFACTORY_H diff --git a/src/basketlistview.cpp b/src/basketlistview.cpp new file mode 100644 index 0000000..51b9747 --- /dev/null +++ b/src/basketlistview.cpp @@ -0,0 +1,807 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "basketlistview.h" +#include <qregexp.h> +#include <kglobalsettings.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kstringhandler.h> +#include <qpainter.h> +#include <qbitmap.h> +#include <qpixmapcache.h> +#include <qtooltip.h> +#include <iostream> +#include <kdebug.h> +#include "global.h" +#include "bnpview.h" +#include "basket.h" +#include "tools.h" +#include "settings.h" +#include "notedrag.h" + +/** class BasketListViewItem: */ + +BasketListViewItem::BasketListViewItem(QListView *parent, Basket *basket) + : QListViewItem(parent), m_basket(basket) + , m_isUnderDrag(false) + , m_isAbbreviated(false) +{ + setDropEnabled(true); +} + +BasketListViewItem::BasketListViewItem(QListViewItem *parent, Basket *basket) + : QListViewItem(parent), m_basket(basket) + , m_isUnderDrag(false) + , m_isAbbreviated(false) +{ + setDropEnabled(true); +} + +BasketListViewItem::BasketListViewItem(QListView *parent, QListViewItem *after, Basket *basket) + : QListViewItem(parent, after), m_basket(basket) + , m_isUnderDrag(false) + , m_isAbbreviated(false) +{ + setDropEnabled(true); +} + +BasketListViewItem::BasketListViewItem(QListViewItem *parent, QListViewItem *after, Basket *basket) + : QListViewItem(parent, after), m_basket(basket) + , m_isUnderDrag(false) + , m_isAbbreviated(false) +{ + setDropEnabled(true); +} + +BasketListViewItem::~BasketListViewItem() +{ +} + +bool BasketListViewItem::acceptDrop(const QMimeSource *) const +{ + std::cout << "accept" << std::endl; + return true; +} + +void BasketListViewItem::dropped(QDropEvent *event) +{ + std::cout << "Dropping into basket " << m_basket->name() << std::endl; + m_basket->contentsDropEvent(event); + //Global::bnpView->currentBasket()->contentsDropEvent(event); // FIXME +} + +int BasketListViewItem::width(const QFontMetrics &/* fontMetrics */, const QListView */*listView*/, int /* column */) const +{ + return listView()->visibleWidth() + 100; +/* + int BASKET_ICON_SIZE = 16; + int MARGIN = 1; + + QRect textRect = fontMetrics.boundingRect(0, 0, / *width=* /1, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::ShowPrefix, text(column)); + + return MARGIN + BASKET_ICON_SIZE + MARGIN + textRect.width() + BASKET_ICON_SIZE/2 + MARGIN; +*/ +} + +QString BasketListViewItem::escapedName(const QString &string) +{ + // Underlining the Alt+Letter shortcut (and escape all other '&' characters), if any: + QString basketName = string; + basketName.replace('&', "&&"); // First escape all the amperstamp + QString letter; // Find the letter + QString altKey = /*i18n(*/"Alt"/*)*/; //i18n("The [Alt] key, as shown in shortcuts like Alt+C...", "Alt"); + QString shiftKey = /*i18n(*/"Shift"/*)*/; //i18n("The [Shift] key, as shown in shortcuts like Alt+Shift+1...", "Shift"); + QRegExp altLetterExp( QString("^%1\\+(.)$").arg(altKey) ); + QRegExp altShiftLetterExp( QString("^%1\\+%2\\+(.)$").arg(altKey, shiftKey) ); + if (altLetterExp.search(m_basket->shortcut().toStringInternal()) != -1) + letter = altLetterExp.cap(1); + if (letter.isEmpty() && altShiftLetterExp.search(m_basket->shortcut().toStringInternal()) != -1) + letter = altShiftLetterExp.cap(1); + if (!letter.isEmpty()) { + int index = basketName.find(letter, /*index=*/0, /*caseSensitive=*/false); + if (index != -1) + basketName.insert(index, '&'); + } + return basketName; +} + +void BasketListViewItem::setup() +{ + int BASKET_ICON_SIZE = 16; + int MARGIN = 1; + + setText(/*column=*/0, escapedName(m_basket->basketName())); + + widthChanged(); + QRect textRect = listView()->fontMetrics().boundingRect(0, 0, /*width=*/1, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::ShowPrefix, text(/*column=*/0)); + + int height = MARGIN + QMAX(BASKET_ICON_SIZE, textRect.height()) + MARGIN; + setHeight(height); + + QPixmap icon = kapp->iconLoader()->loadIcon(m_basket->icon(), KIcon::NoGroup, 16, KIcon::DefaultState, 0L, /*canReturnNull=*/false); + + setPixmap(/*column=*/0, icon); + + repaint(); +} + +BasketListViewItem* BasketListViewItem::lastChild() +{ + QListViewItem *child = firstChild(); + while (child) { + if (child->nextSibling()) + child = child->nextSibling(); + else + return (BasketListViewItem*)child; + } + return 0; +} + +BasketListViewItem* BasketListViewItem::prevSibling() +{ + BasketListViewItem *item = this; + while (item) { + if (item->nextSibling() == this) + return item; + item = (BasketListViewItem*)(item->itemAbove()); + } + return 0; +} + +BasketListViewItem* BasketListViewItem::shownItemAbove() +{ + BasketListViewItem *item = (BasketListViewItem*)itemAbove(); + while (item) { + if (item->isShown()) + return item; + item = (BasketListViewItem*)(item->itemAbove()); + } + return 0; +} + +BasketListViewItem* BasketListViewItem::shownItemBelow() +{ + BasketListViewItem *item = (BasketListViewItem*)itemBelow(); + while (item) { + if (item->isShown()) + return item; + item = (BasketListViewItem*)(item->itemBelow()); + } + return 0; +} + +QStringList BasketListViewItem::childNamesTree(int deep) +{ + QStringList result; + for (QListViewItem *child = firstChild(); child; child = child->nextSibling()) { + BasketListViewItem *item = (BasketListViewItem*)child; + // Compute indentation spaces: + QString spaces; + for (int i = 0; i < deep; ++i) + spaces += " "; + // Append the name: + result.append(spaces + item->basket()->basketName()); + // Append the childs: + if (child->firstChild()) { + QStringList childs = item->childNamesTree(deep + 1); + for (QStringList::iterator it = childs.begin(); it != childs.end(); ++it) + result.append(*it); + } + } + return result; +} + +void BasketListViewItem::moveChildsBaskets() +{ + QListViewItem *insertAfterThis = this; + QListViewItem *nextOne; + for (QListViewItem *child = firstChild(); child; child = nextOne) { + nextOne = child->nextSibling(); + // Re-insert the item with the good parent: + takeItem(child); + if (parent()) + parent()->insertItem(child); + else + listView()->insertItem(child); + // And move it at the good place: + child->moveItem(insertAfterThis); + insertAfterThis = child; + } +} + +void BasketListViewItem::ensureVisible() +{ + BasketListViewItem *item = this; + while (item->parent()) { + item = (BasketListViewItem*)(item->parent()); + item->setOpen(true); + } +} + +bool BasketListViewItem::isShown() +{ + QListViewItem *item = parent(); + while (item) { + if (!item->isOpen()) + return false; + item = item->parent(); + } + return true; +} + +bool BasketListViewItem::isCurrentBasket() +{ + return basket() == Global::bnpView->currentBasket(); +} + +// TODO: Move this function from item.cpp to class Tools: +extern void drawGradient( QPainter *p, const QColor &colorTop, const QColor & colorBottom, + int x, int y, int w, int h, + bool sunken, bool horz, bool flat ); /*const*/ + +QPixmap BasketListViewItem::circledTextPixmap(const QString &text, int height, const QFont &font, const QColor &color) +{ + QString key = QString("BLI-%1.%2.%3.%4") + .arg(text).arg(height).arg(font.toString()).arg(color.rgb()); + if (QPixmap* cached=QPixmapCache::find(key)) { + return *cached; + } + + // Compute the sizes of the image components: + QRect textRect = QFontMetrics(font).boundingRect(0, 0, /*width=*/1, height, Qt::AlignAuto | Qt::AlignTop, text); + int xMargin = height / 6; + int width = xMargin + textRect.width() + xMargin; + + // Create the gradient image: + QPixmap gradient(3 * width, 3 * height); // We double the size to be able to smooth scale down it (== antialiased curves) + QPainter gradientPainter(&gradient); +#if 1 // Enable the new look of the gradient: + QColor topColor = KGlobalSettings::highlightColor().light(130); //120 + QColor topMidColor = KGlobalSettings::highlightColor().light(105); //105 + QColor bottomMidColor = KGlobalSettings::highlightColor().dark(130); //120 + QColor bottomColor = KGlobalSettings::highlightColor(); + drawGradient(&gradientPainter, topColor, topMidColor, + 0, 0, gradient.width(), gradient.height() / 2, /*sunken=*/false, /*horz=*/true, /*flat=*/false); + drawGradient(&gradientPainter, bottomMidColor, bottomColor, + 0, gradient.height() / 2, gradient.width(), gradient.height() - gradient.height() / 2, /*sunken=*/false, /*horz=*/true, /*flat=*/false); + gradientPainter.fillRect(0, 0, gradient.width(), 3, KGlobalSettings::highlightColor()); +#else + drawGradient(&gradientPainter, KGlobalSettings::highlightColor(), KGlobalSettings::highlightColor().dark(), + 0, 0, gradient.width(), gradient.height(), /*sunken=*/false, /*horz=*/true, /*flat=*/false); +#endif + gradientPainter.end(); + + // Draw the curved rectangle: + QBitmap curvedRectangle(3 * width, 3 * height); + curvedRectangle.fill(Qt::color0); + QPainter curvePainter(&curvedRectangle); + curvePainter.setPen(Qt::color1); + curvePainter.setBrush(Qt::color1); + curvePainter.setClipRect(0, 0, 3*(height / 5), 3*(height)); // If the width is small, don't fill the right part of the pixmap + curvePainter.drawEllipse(0, 3*(-height / 4), 3*(height), 3*(height * 3 / 2)); // Don't forget we double the sizes + curvePainter.setClipRect(3*(width - height / 5), 0, 3*(height / 5), 3*(height)); + curvePainter.drawEllipse(3*(width - height), 3*(-height / 4), 3*(height), 3*(height * 3 / 2)); + curvePainter.setClipping(false); + curvePainter.fillRect(3*(height / 6), 0, 3*(width - 2 * height / 6), 3*(height), curvePainter.brush()); + curvePainter.end(); + + // Apply the curved rectangle as the mask of the gradient: + gradient.setMask(curvedRectangle); + QImage resultImage = gradient.convertToImage(); + resultImage.setAlphaBuffer(true); + + // Scale down the image smoothly to get anti-aliasing: + QPixmap pmScaled; + pmScaled.convertFromImage(resultImage.smoothScale(width, height)); + + // Draw the text, and return the result: + QPainter painter(&pmScaled); + painter.setPen(color); + painter.setFont(font); + painter.drawText(0+1, 0, width, height, Qt::AlignHCenter | Qt::AlignVCenter, text); + painter.end(); + + QPixmapCache::insert(key, pmScaled); + + return pmScaled; +} + +QPixmap BasketListViewItem::foundCountPixmap(bool isLoading, int countFound, bool childsAreLoading, int countChildsFound, const QFont &font, int height) +{ + if (isLoading) + return QPixmap(); + + QFont boldFont(font); + boldFont.setBold(true); + + QString text; + if (childsAreLoading) { + if (countChildsFound > 0) + text = i18n("%1+%2+").arg(QString::number(countFound), QString::number(countChildsFound)); + else + text = i18n("%1+").arg(QString::number(countFound)); + } else { + if (countChildsFound > 0) + text = i18n("%1+%2").arg(QString::number(countFound), QString::number(countChildsFound)); + else if (countFound > 0) + text = QString::number(countFound); + else + return QPixmap(); + } + + return circledTextPixmap(text, height, boldFont, KGlobalSettings::highlightedTextColor()); +} + +bool BasketListViewItem::haveChildsLoading() +{ + QListViewItem *child = firstChild(); + while (child) { + BasketListViewItem *childItem = (BasketListViewItem*)child; + if (!childItem->basket()->isLoaded() && !childItem->basket()->isLocked()) + return true; + if (childItem->haveChildsLoading()) + return true; + child = child->nextSibling(); + } + return false; +} + +bool BasketListViewItem::haveHiddenChildsLoading() +{ + if (isOpen()) + return false; + return haveChildsLoading(); +} + +bool BasketListViewItem::haveChildsLocked() +{ + QListViewItem *child = firstChild(); + while (child) { + BasketListViewItem *childItem = (BasketListViewItem*)child; + if (/*!*/childItem->basket()->isLocked()) + return true; + if (childItem->haveChildsLocked()) + return true; + child = child->nextSibling(); + } + return false; +} + +bool BasketListViewItem::haveHiddenChildsLocked() +{ + if (isOpen()) + return false; + return haveChildsLocked(); +} + +int BasketListViewItem::countChildsFound() +{ + int count = 0; + QListViewItem *child = firstChild(); + while (child) { + BasketListViewItem *childItem = (BasketListViewItem*)child; + count += childItem->basket()->countFounds(); + count += childItem->countChildsFound(); + child = child->nextSibling(); + } + return count; +} + +int BasketListViewItem::countHiddenChildsFound() +{ + if (isOpen()) + return 0; + return countChildsFound(); +} + +void BasketListViewItem::paintCell(QPainter *painter, const QColorGroup &/*colorGroup*/, int /*column*/, int width, int /*align*/) +{ + // Workaround a Qt bug: + // When the splitter is moved to hide the tree view and then the application is restarted, + // Qt try to draw items with a negative size! + if (width <= 0) { + std::cout << "width <= 0" << std::endl; + return; + } + + int BASKET_ICON_SIZE = 16; + int MARGIN = 1; + + + // If we are filtering all baskets, and are effectively filtering on something: + bool showLoadingIcon = false; + bool showEncryptedIcon = false; + QPixmap countPixmap; + bool showCountPixmap = Global::bnpView->isFilteringAllBaskets() && + Global::bnpView->currentBasket()->decoration()->filterBar()->filterData().isFiltering; + if (showCountPixmap) { + showLoadingIcon = (!m_basket->isLoaded() && !m_basket->isLocked()) || haveHiddenChildsLoading(); + showEncryptedIcon = m_basket->isLocked() || haveHiddenChildsLocked(); + countPixmap = foundCountPixmap(!m_basket->isLoaded(), m_basket->countFounds(), haveHiddenChildsLoading() || haveHiddenChildsLocked(), + countHiddenChildsFound(), listView()->font(), height() - 2 * MARGIN); + } + int effectiveWidth = width - (countPixmap.isNull() ? 0 : countPixmap.width() + MARGIN) + - (showLoadingIcon || showEncryptedIcon ? BASKET_ICON_SIZE + MARGIN : 0)/* + - (showEncryptedIcon ? BASKET_ICON_SIZE + MARGIN : 0)*/; + + + bool drawRoundRect = m_basket->backgroundColorSetting().isValid() || m_basket->textColorSetting().isValid(); + QColor textColor = (drawRoundRect ? m_basket->textColor() : (isCurrentBasket() ? KGlobalSettings::highlightedTextColor() : KGlobalSettings::textColor())); + + BasketListViewItem *shownAbove = shownItemAbove(); + BasketListViewItem *shownBelow = shownItemBelow(); + + // Don't forget to update the key computation if parameters + // affecting the rendering logic change + QString key = QString("BLVI::pC-%1.%2.%3.%4.%5.%6.%7.%8.%9.%10.%11.%12.%13.%14.%15") + .arg(effectiveWidth) + .arg(drawRoundRect) + .arg(textColor.rgb()) + .arg(m_basket->backgroundColor().rgb()) + .arg(isCurrentBasket()) + .arg(shownBelow && shownBelow->isCurrentBasket()) + .arg(shownAbove && shownAbove->isCurrentBasket()) + .arg(showLoadingIcon) + .arg(showEncryptedIcon) + .arg(showCountPixmap) + .arg(m_basket->countFounds()) + .arg(countHiddenChildsFound()) + .arg(m_isUnderDrag) + .arg(m_basket->basketName()) + .arg(m_basket->icon()); + if (QPixmap* cached = QPixmapCache::find(key)) { + // Qt's documentation recommends copying the pointer + // into a QPixmap immediately + QPixmap cachedBuffer = *cached; + painter->drawPixmap(0, 0, cachedBuffer); + return; + } + + // Bufferize the drawing of items (otherwize, resizing the splitter make the tree act like a Christmas Tree ;-D ): + QPixmap theBuffer(width, height()); + QPainter thePainter(&theBuffer); + + // Fill with the basket background color: + QColor background = (isCurrentBasket() ? KGlobalSettings::highlightColor() : listView()->paletteBackgroundColor()); + thePainter.fillRect(0, 0, width, height(), background); + + int textWidth = effectiveWidth - MARGIN - BASKET_ICON_SIZE - MARGIN - MARGIN; + + // Draw the rounded rectangle: + if (drawRoundRect) { + QRect textRect = listView()->fontMetrics().boundingRect(0, 0, /*width=*/1, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::ShowPrefix, text(/*column=*/0)); + int xRound = MARGIN; + int yRound = MARGIN; + int hRound = height() - 2 * MARGIN; + int wRound = QMIN(BASKET_ICON_SIZE + MARGIN + textRect.width() + hRound/2, effectiveWidth - MARGIN - MARGIN); + if (wRound > 0) { // Do not crash if there is no space anymore to draw the rounded rectangle: + QPixmap buffer(wRound * 2, hRound * 2); + buffer.fill(background); + QPainter pBuffer(&buffer); + QColor colorRound = m_basket->backgroundColor(); + pBuffer.setPen(colorRound); + pBuffer.setBrush(colorRound); + if (wRound > hRound) { // If the rectangle is smaller in width than in height, don't overlap ellipses... + pBuffer.drawEllipse(0, 0, hRound * 2, hRound * 2); + pBuffer.drawEllipse(wRound * 2 - hRound * 2, 0, hRound * 2, hRound * 2); + pBuffer.fillRect(hRound*2/2, 0, wRound * 2 - hRound * 2, hRound * 2, colorRound); + } else + pBuffer.drawEllipse(0, 0, wRound * 2, hRound * 2); + pBuffer.end(); + QImage imageToScale = buffer.convertToImage(); + QPixmap pmScaled; + pmScaled.convertFromImage(imageToScale.smoothScale(wRound, hRound)); + thePainter.drawPixmap(xRound, yRound, pmScaled); + textWidth -= hRound/2; + } + } + + QColor bgColor = listView()->paletteBackgroundColor(); + QColor selColor = KGlobalSettings::highlightColor(); + QColor midColor = Tools::mixColor(bgColor, selColor); + // Draw the left selection roundings: + if (isCurrentBasket()) { + thePainter.setPen(bgColor); + thePainter.drawPoint(0, 0); + thePainter.drawPoint(1, 0); + thePainter.drawPoint(0, 1); + thePainter.drawPoint(0, height() - 1); + thePainter.drawPoint(1, height() - 1); + thePainter.drawPoint(0, height() - 2); + thePainter.setPen(midColor); + thePainter.drawPoint(2, 0); + thePainter.drawPoint(0, 2); + thePainter.drawPoint(2, height() - 1); + thePainter.drawPoint(0, height() - 3); + } + // Draw the bottom-right selection roundings: + //BasketListViewItem *shownBelow = shownItemBelow(); + if (shownBelow && shownBelow->isCurrentBasket()) { + thePainter.setPen(selColor); + thePainter.drawPoint(width - 1, height() - 1); + thePainter.drawPoint(width - 2, height() - 1); + thePainter.drawPoint(width - 1, height() - 2); + thePainter.setPen(midColor); + thePainter.drawPoint(width - 3, height() - 1); + thePainter.drawPoint(width - 1, height() - 3); + } + // Draw the top-right selection roundings: + // BasketListViewItem *shownAbove = shownItemAbove(); + if (shownAbove && shownAbove->isCurrentBasket()) { + thePainter.setPen(selColor); + thePainter.drawPoint(width - 1, 0); + thePainter.drawPoint(width - 2, 0); + thePainter.drawPoint(width - 1, 1); + thePainter.setPen(midColor); + thePainter.drawPoint(width - 3, 0); + thePainter.drawPoint(width - 1, 2); + } + + // Draw the icon and text: + int yPixmap = (height() - BASKET_ICON_SIZE) / 2; + thePainter.drawPixmap(MARGIN, yPixmap, *pixmap(/*column=*/0)); + thePainter.setPen(textColor); + if (textWidth > 0) { // IF there is space left to draw the text: + int xText = MARGIN + BASKET_ICON_SIZE + MARGIN; + QString theText = m_basket->basketName(); + if (painter->fontMetrics().width(theText) > textWidth) { + theText = KStringHandler::rPixelSqueeze(theText, painter->fontMetrics(), textWidth); + m_isAbbreviated = true; + } + else { + m_isAbbreviated = false; + } + theText = escapedName(theText); + thePainter.drawText(xText, 0, textWidth, height(), Qt::AlignAuto | Qt::AlignVCenter | Qt::ShowPrefix, theText); + } + + // If we are filtering all baskets, and are effectively filtering on something: + if (!countPixmap.isNull()) + { + thePainter.drawPixmap(effectiveWidth, 1, countPixmap); + effectiveWidth += countPixmap.width() + MARGIN; + } + if (showLoadingIcon) { + QPixmap icon = kapp->iconLoader()->loadIcon("find", KIcon::NoGroup, 16, KIcon::DefaultState, 0L, /*canReturnNull=*/false); + thePainter.drawPixmap(effectiveWidth, 0, icon); + effectiveWidth += BASKET_ICON_SIZE + MARGIN; + } + if (showEncryptedIcon && !showLoadingIcon) { + QPixmap icon = kapp->iconLoader()->loadIcon("encrypted", KIcon::NoGroup, 16, KIcon::DefaultState, 0L, /*canReturnNull=*/false); + thePainter.drawPixmap(effectiveWidth, 0, icon); + } + + if (m_isUnderDrag) { + thePainter.drawWinFocusRect(0, 0, width, height()); + } + thePainter.end(); + + QPixmapCache::insert(key, theBuffer); + // Apply the buffer: + painter->drawPixmap(0, 0, theBuffer); +} + +void BasketListViewItem::setUnderDrag(bool underDrag) +{ + m_isUnderDrag = underDrag; +} + +bool BasketListViewItem::isAbbreviated() +{ + return m_isAbbreviated; +} + +/** class BasketListViewToolTip: */ + +class BasketTreeListView_ToolTip : public QToolTip { +public: + BasketTreeListView_ToolTip(BasketTreeListView* basketView) + : QToolTip(basketView->viewport()) + , m_basketView(basketView) + {} +public: + void maybeTip(const QPoint& pos) + { + QListViewItem *item = m_basketView->itemAt(m_basketView->contentsToViewport(pos)); + BasketListViewItem* bitem = dynamic_cast<BasketListViewItem*>(item); + if (bitem && bitem->isAbbreviated()) { + tip(m_basketView->itemRect(bitem), bitem->basket()->basketName()); + } + } +private: + BasketTreeListView* m_basketView; +}; + +/** class BasketTreeListView: */ + +BasketTreeListView::BasketTreeListView(QWidget *parent, const char *name) + : KListView(parent, name), m_autoOpenItem(0) + , m_itemUnderDrag(0) +{ + setWFlags(Qt::WStaticContents | WNoAutoErase); + clearWFlags(Qt::WStaticContents | WNoAutoErase); + //viewport()->clearWFlags(Qt::WStaticContents); + connect( &m_autoOpenTimer, SIGNAL(timeout()), this, SLOT(autoOpen()) ); + + new BasketTreeListView_ToolTip(this); +} + +void BasketTreeListView::viewportResizeEvent(QResizeEvent *event) +{ + KListView::viewportResizeEvent(event); + triggerUpdate(); +} + +void BasketTreeListView::contentsDragEnterEvent(QDragEnterEvent *event) +{ + if (event->provides("application/x-qlistviewitem")) { + QListViewItemIterator it(this); // TODO: Don't show expanders if it's not a basket drag... + while (it.current()) { + QListViewItem *item = it.current(); + if (!item->firstChild()) { + item->setExpandable(true); + item->setOpen(true); + } + ++it; + } + update(); + } + + KListView::contentsDragEnterEvent(event); +} + +void BasketTreeListView::removeExpands() +{ + QListViewItemIterator it(this); + while (it.current()) { + QListViewItem *item = it.current(); + if (!item->firstChild()) + item->setExpandable(false); + ++it; + } + viewport()->update(); +} + +void BasketTreeListView::contentsDragLeaveEvent(QDragLeaveEvent *event) +{ + std::cout << "BasketTreeListView::contentsDragLeaveEvent" << std::endl; + m_autoOpenItem = 0; + m_autoOpenTimer.stop(); + setItemUnderDrag(0); + removeExpands(); + KListView::contentsDragLeaveEvent(event); +} + +void BasketTreeListView::contentsDropEvent(QDropEvent *event) +{ + std::cout << "BasketTreeListView::contentsDropEvent()" << std::endl; + if (event->provides("application/x-qlistviewitem")) + { + KListView::contentsDropEvent(event); + } + else { + std::cout << "Forwarding dropped data to the basket" << std::endl; + QListViewItem *item = itemAt(contentsToViewport(event->pos())); + BasketListViewItem* bitem = dynamic_cast<BasketListViewItem*>(item); + if (bitem) { + bitem->basket()->blindDrop(event); + } + else { + std::cout << "Forwarding failed: no bitem found" << std::endl; + } + } + + m_autoOpenItem = 0; + m_autoOpenTimer.stop(); + setItemUnderDrag(0); + removeExpands(); + + Global::bnpView->save(); // TODO: Don't save if it was not a basket drop... +} + +void BasketTreeListView::contentsDragMoveEvent(QDragMoveEvent *event) +{ + std::cout << "BasketTreeListView::contentsDragMoveEvent" << std::endl; + if (event->provides("application/x-qlistviewitem")) + KListView::contentsDragMoveEvent(event); + else { + QListViewItem *item = itemAt(contentsToViewport(event->pos())); + BasketListViewItem* bitem = dynamic_cast<BasketListViewItem*>(item); + if (m_autoOpenItem != item) { + m_autoOpenItem = item; + m_autoOpenTimer.start(1700, /*singleShot=*/true); + } + if (item) { + event->acceptAction(true); + event->accept(true); + } + setItemUnderDrag(bitem); + + KListView::contentsDragMoveEvent(event); // FIXME: ADDED + } +} + +void BasketTreeListView::setItemUnderDrag(BasketListViewItem* item) +{ + if (m_itemUnderDrag != item) { + if (m_itemUnderDrag) { + // Remove drag status from the old item + m_itemUnderDrag->setUnderDrag(false); + repaintItem(m_itemUnderDrag); + } + + m_itemUnderDrag = item; + + if (m_itemUnderDrag) { + // add drag status to the new item + m_itemUnderDrag->setUnderDrag(true); + repaintItem(m_itemUnderDrag); + } + } +} + +void BasketTreeListView::autoOpen() +{ + BasketListViewItem *item = (BasketListViewItem*)m_autoOpenItem; + if (item) + Global::bnpView->setCurrentBasket(item->basket()); +} + +void BasketTreeListView::resizeEvent(QResizeEvent *event) +{ + KListView::resizeEvent(event); +} + +void BasketTreeListView::paintEmptyArea(QPainter *painter, const QRect &rect) +{ + QListView::paintEmptyArea(painter, rect); + + BasketListViewItem *last = Global::bnpView->lastListViewItem(); + if (last && !last->isShown()) + last = last->shownItemAbove(); + if (last && last->isCurrentBasket()) { + int y = last->itemPos() + last->height(); + QColor bgColor = paletteBackgroundColor(); + QColor selColor = KGlobalSettings::highlightColor(); + QColor midColor = Tools::mixColor(bgColor, selColor); + painter->setPen(selColor); + painter->drawPoint(visibleWidth() - 1, y); + painter->drawPoint(visibleWidth() - 2, y); + painter->drawPoint(visibleWidth() - 1, y + 1); + painter->setPen(midColor); + painter->drawPoint(visibleWidth() - 3, y); + painter->drawPoint(visibleWidth() - 1, y + 2); + } +} + +/** We should NEVER get focus (because of QWidget::NoFocus focusPolicy()) + * but KListView can programatically give us the focus. + * So we give it to the basket. + */ +void BasketTreeListView::focusInEvent(QFocusEvent*) +{ + //KListView::focusInEvent(event); + Basket *basket = Global::bnpView->currentBasket(); + if (basket) + basket->setFocus(); +} + +#include "basketlistview.moc" diff --git a/src/basketlistview.h b/src/basketlistview.h new file mode 100644 index 0000000..9a6a918 --- /dev/null +++ b/src/basketlistview.h @@ -0,0 +1,103 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BASKETLISTVIEW_H +#define BASKETLISTVIEW_H + +#include <klistview.h> +#include <qtimer.h> + +class Basket; + +class BasketListViewItem : public QListViewItem +{ + public: + /// CONSTRUCTOR AND DESTRUCTOR: + BasketListViewItem(QListView *parent, Basket *basket); + BasketListViewItem(QListViewItem *parent, Basket *basket); + BasketListViewItem(QListView *parent, QListViewItem *after, Basket *basket); + BasketListViewItem(QListViewItem *parent, QListViewItem *after, Basket *basket); + ~BasketListViewItem(); + /// + bool acceptDrop(const QMimeSource *mime) const; + void dropped(QDropEvent *event); + Basket *basket() { return m_basket; } + void setup(); + int width(const QFontMetrics &fontMetrics, const QListView *listView, int column) const; + BasketListViewItem* lastChild(); + BasketListViewItem* prevSibling(); + BasketListViewItem* shownItemAbove(); + BasketListViewItem* shownItemBelow(); + QStringList childNamesTree(int deep = 0); + void moveChildsBaskets(); + void ensureVisible(); + bool isShown(); + bool isCurrentBasket(); + void paintCell(QPainter *painter, const QColorGroup &colorGroup, int column, int width, int align); + QString escapedName(const QString &string); + /// + QPixmap circledTextPixmap(const QString &text, int height, const QFont &font, const QColor &color); + QPixmap foundCountPixmap(bool isLoading, int countFound, bool childsAreLoading, int countChildsFound, const QFont &font, int height); + bool haveChildsLoading(); + bool haveHiddenChildsLoading(); + bool haveChildsLocked(); + bool haveHiddenChildsLocked(); + int countChildsFound(); + int countHiddenChildsFound(); + + void setUnderDrag(bool); + bool isAbbreviated(); + /// +// QDragObject* dragObject(); +// bool acceptDrop ( const QMimeSource * mime ) const; + private: + Basket *m_basket; + int m_width; + bool m_isUnderDrag; + bool m_isAbbreviated; +}; + +class BasketTreeListView : public KListView +{ + Q_OBJECT + public: + BasketTreeListView(QWidget *parent = 0, const char *name = 0); + void contentsDragEnterEvent(QDragEnterEvent *event); + void removeExpands(); + void contentsDragLeaveEvent(QDragLeaveEvent *event); + void contentsDragMoveEvent(QDragMoveEvent *event); + void contentsDropEvent(QDropEvent *event); + void resizeEvent(QResizeEvent *event); + void paintEmptyArea(QPainter *painter, const QRect &rect); + protected: + void focusInEvent(QFocusEvent*); + void viewportResizeEvent(QResizeEvent *event); + private: + QTimer m_autoOpenTimer; + QListViewItem *m_autoOpenItem; + private slots: + void autoOpen(); + private: + void setItemUnderDrag(BasketListViewItem* item); + BasketListViewItem* m_itemUnderDrag; + +}; + +#endif // BASKETLISTVIEW_H diff --git a/src/basketproperties.cpp b/src/basketproperties.cpp new file mode 100644 index 0000000..911be2a --- /dev/null +++ b/src/basketproperties.cpp @@ -0,0 +1,203 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kicondialog.h> +#include <qlineedit.h> +#include <qcombobox.h> +#include <qvbuttongroup.h> +#include <knuminput.h> +#include <kkeybutton.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qbuttongroup.h> +#include <qradiobutton.h> +#include <qstringlist.h> +#include <klocale.h> +#include <qstyle.h> +#include <kapplication.h> +#include <kiconloader.h> +#include <kglobalsettings.h> + +#include "basketproperties.h" +#include "basket.h" +#include "kcolorcombo2.h" +#include "variouswidgets.h" +#include "global.h" +#include "backgroundmanager.h" + +BasketPropertiesDialog::BasketPropertiesDialog(Basket *basket, QWidget *parent) + : KDialogBase(KDialogBase::Swallow, i18n("Basket Properties"), KDialogBase::Ok | KDialogBase::Apply | KDialogBase::Cancel, + KDialogBase::Ok, parent, /*name=*/"BasketProperties", /*modal=*/true, /*separator=*/false), + m_basket(basket) +{ + QWidget *page = new QWidget(this); + QVBoxLayout *topLayout = new QVBoxLayout(page, /*margin=*/0, spacingHint()); + + // Icon and Name: + QHBoxLayout *nameLayout = new QHBoxLayout(0, marginHint()*2/3, spacingHint()); + m_icon = new KIconButton(page); + m_icon->setIconType(KIcon::NoGroup, KIcon::Action); + m_icon->setIconSize(16); + m_icon->setIcon(m_basket->icon()); + int size = QMAX(m_icon->sizeHint().width(), m_icon->sizeHint().height()); + m_icon->setFixedSize(size, size); // Make it square! + QToolTip::add(m_icon, i18n("Icon")); + m_name = new QLineEdit(m_basket->basketName(), page); + m_name->setMinimumWidth(m_name->fontMetrics().maxWidth()*20); + QToolTip::add(m_name, i18n("Name")); + nameLayout->addWidget(m_icon); + nameLayout->addWidget(m_name); + topLayout->addLayout(nameLayout); + + // Appearance: + QGroupBox *appearance = new QGroupBox(1, Qt::Horizontal, i18n("Appearance"), page); + QWidget *appearanceWidget = new QWidget(appearance); + QGridLayout *grid = new QGridLayout(appearanceWidget, /*nRows=*/3, /*nCols=*/2, /*margin=*/0, spacingHint()); + m_backgroundImage = new QComboBox(appearanceWidget); + m_backgroundColor = new KColorCombo2(m_basket->backgroundColorSetting(), KGlobalSettings::baseColor(), appearanceWidget); + m_textColor = new KColorCombo2(m_basket->textColorSetting(), KGlobalSettings::textColor(), appearanceWidget); + QLabel *label1 = new QLabel(m_backgroundImage, i18n("Background &image:"), appearanceWidget); + QLabel *label2 = new QLabel(m_backgroundColor, i18n("&Background color:"), appearanceWidget); + QLabel *label3 = new QLabel(m_textColor, i18n("&Text color:"), appearanceWidget); + grid->addWidget(label1, 0, 0, Qt::AlignVCenter); + grid->addWidget(label2, 1, 0, Qt::AlignVCenter); + grid->addWidget(label3, 2, 0, Qt::AlignVCenter); + grid->addWidget(m_backgroundImage, 0, 1, Qt::AlignVCenter); + grid->addWidget(m_backgroundColor, 1, 1, Qt::AlignVCenter); + grid->addWidget(m_textColor, 2, 1, Qt::AlignVCenter); + topLayout->addWidget(appearance); + + m_backgroundImage->insertItem(i18n("(None)"), 0); + m_backgroundImagesMap.insert(0, ""); + QStringList backgrounds = Global::backgroundManager->imageNames(); + int index = 1; + for (QStringList::Iterator it = backgrounds.begin(); it != backgrounds.end(); ++it) { + QPixmap *preview = Global::backgroundManager->preview(*it); + if (preview) { + m_backgroundImagesMap.insert(index, *it); + m_backgroundImage->insertItem(*preview, index); + if (m_basket->backgroundImageName() == *it) + m_backgroundImage->setCurrentItem(index); + index++; + } + } +// m_backgroundImage->insertItem(i18n("Other..."), -1); + int BUTTON_MARGIN = kapp->style().pixelMetric(QStyle::PM_ButtonMargin); + m_backgroundImage->setSizeLimit(50/*75 * 6 / m_backgroundImage->sizeHint().height()*/); + m_backgroundImage->setMinimumHeight(75 + 2 * BUTTON_MARGIN); + + // Disposition: + m_disposition = new QVButtonGroup(i18n("Disposition"), page); + QWidget *columnsWidget = new QWidget(m_disposition); + QHBoxLayout *dispoLayout = new QHBoxLayout(columnsWidget, /*margin=*/0, spacingHint()); + QRadioButton *radio = new QRadioButton(i18n("Col&umns:"), columnsWidget); + m_columnCount = new KIntNumInput(m_basket->columnsCount(), columnsWidget); + m_columnCount->setRange(1, 20, /*step=*/1, /*slider=*/false); + m_columnCount->setValue(m_basket->columnsCount()); + connect( m_columnCount, SIGNAL(valueChanged(int)), this, SLOT(selectColumnsLayout()) ); + dispoLayout->addWidget(radio); + dispoLayout->addWidget(m_columnCount); + m_disposition->insert(radio); + new QRadioButton(i18n("&Free-form"), m_disposition); + QRadioButton *mindMap = new QRadioButton(i18n("&Mind map"), m_disposition); // TODO: "Learn more..." + int height = QMAX(mindMap->sizeHint().height(), m_columnCount->sizeHint().height()); // Make all radioButtons vertically equaly-spaced! + mindMap->setMinimumSize(mindMap->sizeHint().width(), height); // Because the m_columnCount can be heigher, and make radio1 and radio2 more spaced than radio2 and radio3. + m_disposition->setButton(m_basket->isFreeLayout() ? (m_basket->isMindMap() ? 2 : 1) : 0); + topLayout->addWidget(m_disposition); + + mindMap->hide(); + + // Keyboard Shortcut: + m_shortcutRole = new QVButtonGroup(i18n("&Keyboard Shortcut"), page); + QWidget *shortcutWidget = new QWidget(m_shortcutRole); + QHBoxLayout *shortcutLayout = new QHBoxLayout(shortcutWidget, /*margin=*/0, spacingHint()); + m_shortcut = new KKeyButton(shortcutWidget); + m_shortcut->setShortcut(m_basket->shortcut(), /*bQtShortcut=*/true); + HelpLabel *helpLabel = new HelpLabel(i18n("Learn some tips..."), i18n( + "<p><strong>Easily Remember your Shortcuts</strong>:<br>" + "With the first option, giving the basket a shortcut of the form <strong>Alt+Letter</strong> will underline that letter in the basket tree.<br>" + "For instance, if you are assigning the shortcut <i>Alt+T</i> to a basket named <i>Tips</i>, the basket will be displayed as <i><u>T</u>ips</i> in the tree. " + "It helps you visualize the shortcuts to remember them more quickly.</p>" + "<p><strong>Local vs Global</strong>:<br>" + "The first option allows to show the basket while the main window is active. " + "Global shortcuts are valid from anywhere, even if the window is hidden.</p>" + "<p><strong>Show vs Switch</strong>:<br>" + "The last option makes this basket the current one without opening the main window. " + "It is useful in addition to the configurable global shortcuts, eg. to paste the clipboard or the selection into the current basket from anywhere.</p>"), + shortcutWidget); + shortcutLayout->addWidget(m_shortcut); + shortcutLayout->addStretch(); + shortcutLayout->addWidget(helpLabel); + connect( m_shortcut, SIGNAL(capturedShortcut(const KShortcut&)), this, SLOT(capturedShortcut(const KShortcut&)) ); + new QRadioButton(i18n("S&how this basket"), m_shortcutRole); + new QRadioButton(i18n("Show this basket (&global shortcut)"), m_shortcutRole); + new QRadioButton(i18n("S&witch to this basket (global shortcut)"), m_shortcutRole); + m_shortcutRole->setButton(m_basket->shortcutAction()/* + 1*/); // Id 0 is the KKeyButton! + topLayout->addWidget(m_shortcutRole); + + topLayout->addSpacing(marginHint()); + topLayout->addStretch(10); + + setMainWidget(page); +} + +BasketPropertiesDialog::~BasketPropertiesDialog() +{ +} + +void BasketPropertiesDialog::polish() +{ + KDialogBase::polish(); + m_name->setFocus(); +} + +void BasketPropertiesDialog::applyChanges() +{ + m_basket->setDisposition(m_disposition->selectedId(), m_columnCount->value()); + m_basket->setShortcut(m_shortcut->shortcut(), m_shortcutRole->selectedId()); + // Should be called LAST, because it will emit the propertiesChanged() signal and the tree will be able to show the newly set Alt+Letter shortcut: + m_basket->setAppearance(m_icon->icon(), m_name->text(), m_backgroundImagesMap[m_backgroundImage->currentItem()], m_backgroundColor->color(), m_textColor->color()); + m_basket->save(); +} + +void BasketPropertiesDialog::slotApply() +{ + applyChanges(); + KDialogBase::slotApply(); +} + +void BasketPropertiesDialog::slotOk() +{ + applyChanges(); + KDialogBase::slotOk(); +} + +void BasketPropertiesDialog::capturedShortcut(const KShortcut &shortcut) +{ + // TODO: Validate it! + m_shortcut->setShortcut(shortcut, /*bQtShortcut=*/true); +} + +void BasketPropertiesDialog::selectColumnsLayout() +{ + m_disposition->setButton(0); +} + +#include "basketproperties.moc" diff --git a/src/basketproperties.h b/src/basketproperties.h new file mode 100644 index 0000000..033aff9 --- /dev/null +++ b/src/basketproperties.h @@ -0,0 +1,69 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BASKETPROPERTIES_H +#define BASKETPROPERTIES_H + +#include <kdialogbase.h> +#include <qmap.h> +#include <qstring.h> + +class KIconButton; +class QLineEdit; +class QComboBox; +class QVButtonGroup; +class KIntNumInput; +class KKeyButton; +class KShortcut; + +class Basket; +class KColorCombo2; + +/** The dialog that hold basket settings. + * @author S�bastien Lao�t + */ +class BasketPropertiesDialog : public KDialogBase +{ + Q_OBJECT + public: + BasketPropertiesDialog(Basket *basket, QWidget *parent = 0); + ~BasketPropertiesDialog(); + void polish(); + void applyChanges(); + protected slots: + void slotApply(); + void slotOk(); + void capturedShortcut(const KShortcut &shortcut); + void selectColumnsLayout(); + private: + Basket *m_basket; + KIconButton *m_icon; + QLineEdit *m_name; + QComboBox *m_backgroundImage; + KColorCombo2 *m_backgroundColor; + KColorCombo2 *m_textColor; + QVButtonGroup *m_disposition; + KIntNumInput *m_columnCount; + KKeyButton *m_shortcut; + QVButtonGroup *m_shortcutRole; + QMap<int, QString> m_backgroundImagesMap; +}; + +#endif // BASKETPROPERTIES_H diff --git a/src/basketstatusbar.cpp b/src/basketstatusbar.cpp new file mode 100644 index 0000000..02e18f5 --- /dev/null +++ b/src/basketstatusbar.cpp @@ -0,0 +1,176 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kparts/statusbarextension.h> +#include <kstatusbar.h> +#include <klocale.h> +#include <kdebug.h> +#include <qlabel.h> +#include <qobjectlist.h> +#include "basketstatusbar.h" +#include "clickablelabel.h" +#include "global.h" +#include "bnpview.h" +#include "basket.h" +#include "tools.h" +#include <kiconloader.h> +#include <qtooltip.h> + +BasketStatusBar::BasketStatusBar(KStatusBar *bar) + : m_bar(bar), m_extension(0), m_selectionStatus(0), m_lockStatus(0), m_basketStatus(0), m_savedStatus(0) +{ +} + +BasketStatusBar::BasketStatusBar(KParts::StatusBarExtension *extension) + : m_bar(0), m_extension(extension), m_selectionStatus(0), m_lockStatus(0), m_basketStatus(0), m_savedStatus(0) +{ +} + +BasketStatusBar::~BasketStatusBar() +{ + //delete m_extension; +} + +KStatusBar *BasketStatusBar::statusBar () const +{ + if(m_extension) + return m_extension->statusBar(); + else + return m_bar; +} + +void BasketStatusBar::addWidget(QWidget * widget, int stretch, bool permanent) +{ + if(m_extension) + m_extension->addStatusBarItem(widget, stretch, permanent); + else + m_bar->addWidget(widget, stretch, permanent); +} + +void BasketStatusBar::setupStatusBar() +{ + QWidget* parent = statusBar(); + QObjectList* lst = parent->queryList("KRSqueezedTextLabel"); + + //Tools::printChildren(parent); + if(lst->count() == 0) + { + m_basketStatus = new QLabel(parent); + m_basketStatus->setSizePolicy( QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored, 0, 0, false) ); + addWidget( m_basketStatus, 1, false ); // Fit all extra space and is hiddable + } + else + m_basketStatus = static_cast<QLabel*>(lst->at(0)); + delete lst; + + m_selectionStatus = new QLabel(i18n("Loading..."), parent); + addWidget( m_selectionStatus, 0, true ); + + m_lockStatus = new ClickableLabel(0/*this*/); + m_lockStatus->setMinimumSize(18, 18); + m_lockStatus->setAlignment(Qt::AlignCenter); +// addWidget( m_lockStatus, 0, true ); + connect( m_lockStatus, SIGNAL(clicked()), Global::bnpView, SLOT(lockBasket()) ); + + m_savedStatusPixmap = SmallIcon("filesave"); + m_savedStatus = new QLabel(parent); + m_savedStatus->setPixmap(m_savedStatusPixmap); + m_savedStatus->setFixedSize(m_savedStatus->sizeHint()); + m_savedStatus->clear(); + //m_savedStatus->setPixmap(m_savedStatusIconSet.pixmap(QIconSet::Small, QIconSet::Disabled)); + //m_savedStatus->setEnabled(false); + addWidget( m_savedStatus, 0, true ); + QToolTip::add(m_savedStatus, "<p>" + i18n("Shows if there are changes that have not yet been saved.")); +} + +void BasketStatusBar::postStatusbarMessage(const QString& text) +{ + if(statusBar()) + statusBar()->message(text, 2000); +} + +void BasketStatusBar::setStatusText(const QString &txt) +{ + if(m_basketStatus && m_basketStatus->text() != txt) + m_basketStatus->setText(txt); +} + +void BasketStatusBar::setStatusBarHint(const QString &hint) +{ + if (hint.isEmpty()) + updateStatusBarHint(); + else + setStatusText(hint); +} + +void BasketStatusBar::updateStatusBarHint() +{ + QString message = ""; + + if (Global::bnpView->currentBasket()->isDuringDrag()) + message = i18n("Ctrl+drop: copy, Shift+drop: move, Shift+Ctrl+drop: link."); +// Too much noise information: +// else if (currentBasket()->inserterShown() && currentBasket()->inserterSplit() && !currentBasket()->inserterGroup()) +// message = i18n("Click to insert a note, right click for more options. Click on the right of the line to group instead of insert."); +// else if (currentBasket()->inserterShown() && currentBasket()->inserterSplit() && currentBasket()->inserterGroup()) +// message = i18n("Click to group a note, right click for more options. Click on the left of the line to group instead of insert."); + else if (Global::debugWindow) + message = "DEBUG: " + Global::bnpView->currentBasket()->folderName(); + + setStatusText(message); +} + +void BasketStatusBar::setLockStatus(bool isLocked) +{ + if(!m_lockStatus) + return; + + if (isLocked) { + m_lockStatus->setPixmap(SmallIcon("encrypted.png")); + QToolTip::add(m_lockStatus, i18n( + "<p>This basket is <b>locked</b>.<br>Click to unlock it.</p>").replace(" ", " ") ); +// QToolTip::add(m_lockStatus, i18n("This basket is locked.\nClick to unlock it.")); + } else { + m_lockStatus->clear(); + QToolTip::add(m_lockStatus, i18n( + "<p>This basket is <b>unlocked</b>.<br>Click to lock it.</p>").replace(" ", " ") ); +// QToolTip::add(m_lockStatus, i18n("This basket is unlocked.\nClick to lock it.")); + } +} + +void BasketStatusBar::setSelectionStatus(const QString &s) +{ + if (m_selectionStatus) + m_selectionStatus->setText(s); +} + +void BasketStatusBar::setUnsavedStatus(bool isUnsaved) +{ + if (!m_savedStatus) + return; + + if (isUnsaved) { + if (m_savedStatus->pixmap() == 0) + m_savedStatus->setPixmap(m_savedStatusPixmap); + } else + m_savedStatus->clear(); +} + +#include "basketstatusbar.moc" diff --git a/src/basketstatusbar.h b/src/basketstatusbar.h new file mode 100644 index 0000000..f9d8714 --- /dev/null +++ b/src/basketstatusbar.h @@ -0,0 +1,68 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef BASKETSTATUSBAR_H +#define BASKETSTATUSBAR_H + +#include <qobject.h> +#include <qpixmap.h> + +class KStatusBar; +namespace KParts { class StatusBarExtension; } +class QWidget; +class QLabel; +class ClickableLabel; + +/** + @author Sébastien Laoût <[email protected]> +*/ +class BasketStatusBar : public QObject +{ + Q_OBJECT + public: + BasketStatusBar(KStatusBar *bar); + BasketStatusBar(KParts::StatusBarExtension *extension); + ~BasketStatusBar(); + + public slots: + /** GUI Main Window actions **/ + void setStatusBarHint(const QString &hint); /// << Set a specific message or update if hint is empty + void updateStatusBarHint(); /// << Display the current state message (dragging, editing) or reset the startsbar message + void postStatusbarMessage(const QString &text); + void setSelectionStatus(const QString &s); + void setLockStatus(bool isLocked); + void setupStatusBar(); + void setUnsavedStatus(bool isUnsaved); + + protected: + KStatusBar *statusBar () const; + void addWidget(QWidget * widget, int stretch = 0, bool permanent = false); + void setStatusText(const QString &txt); + + private: + KStatusBar *m_bar; + KParts::StatusBarExtension *m_extension; + QLabel *m_selectionStatus; + ClickableLabel *m_lockStatus; + QLabel *m_basketStatus; + QLabel *m_savedStatus; + QPixmap m_savedStatusPixmap; +}; + +#endif diff --git a/src/basketui.rc b/src/basketui.rc new file mode 100644 index 0000000..de64c2a --- /dev/null +++ b/src/basketui.rc @@ -0,0 +1,233 @@ +<!DOCTYPE kpartgui SYSTEM "kpartgui.dtd"> +<kpartgui version="1.0.3.1" name="basket" > + <MenuBar noMerge="1" > + <Menu name="basket" > + <text>&Basket</text> + <Action name="basket_new_menu" /> + <Separator/> + <Action name="basket_properties" /> + <Menu icon="fileexport" > + <text>&Export</text> + <Action name="basket_export_basket_archive" /> + <Action name="basket_export_html" /> + </Menu> + <Action name="basket_remove" /> + <Separator/> + <Action name="basket_password" /> + <Action name="basket_lock" /> + <Separator/> + <Menu name="fileimport" icon="fileimport" > + <text>&Import</text> + <Action name="basket_import_basket_archive" /> + <Separator/> + <Action name="basket_import_knotes" /> + <Action name="basket_import_kjots" /> + <Action name="basket_import_knowit" /> + <Action name="basket_import_tuxcards" /> + <Separator/> + <Action name="basket_import_sticky_notes" /> + <Action name="basket_import_tomboy" /> + <Separator/> + <Action name="basket_import_text_file" /> + </Menu> + <Action name="basket_backup_restore" /> + <Separator/> + <Action name="window_hide" /> + <Action name="file_quit" /> + </Menu> + <Menu name="edit" > + <text>&Edit</text> + <Action name="edit_cut" /> + <Action name="edit_copy" /> + <Action name="edit_paste" /> + <Action name="edit_delete" /> + <Separator/> + <Action name="edit_select_all" /> + <Action name="edit_unselect_all" /> + <Action name="edit_invert_selection" /> + <Separator/> + <Action name="edit_filter" /> + <Action name="edit_filter_all_baskets" /> + <Action name="edit_filter_reset" /> + </Menu> + <Menu name="go" > + <text>&Go</text> + <Action name="go_basket_previous" /> + <Action name="go_basket_next" /> + <Action name="go_basket_fold" /> + <Action name="go_basket_expand" /> + </Menu> + <Menu name="note" > + <text>&Note</text> + <Action name="note_edit" /> + <Action name="note_open" /> + <Action name="note_open_with" /> + <Action name="note_save_to_file" /> + <Separator/> + <Action name="note_group" /> + <Action name="note_ungroup" /> + <Separator/> + <Action name="note_move_top" /> + <Action name="note_move_up" /> + <Action name="note_move_down" /> + <Action name="note_move_bottom" /> + </Menu> + <Menu name="tags" > + <text>&Tags</text> + </Menu> + <Menu name="insert" > + <text>&Insert</text> + <Action name="insert_html" /> + <Action name="insert_image" /> + <Action name="insert_link" /> + <Action name="insert_launcher" /> + <Action name="insert_color" /> + <Separator/> + <Action name="insert_screen_capture" /> + <Action name="insert_screen_color" /> + <Separator/> + <Action name="insert_from_file" /> + <Action name="insert_kmenu" /> + <Action name="insert_icon" /> + </Menu> + <Menu name="settings" > + <text>&Settings</text> + <Merge name="StandardToolBarMenuHandler" /> + <Action name="options_show_statusbar" /> + <Separator/> + <Action name="options_configure_keybinding" /> + <Action name="options_configure_global_keybinding" /> + <Action name="options_configure_toolbars" /> + <Action name="options_configure_notifications" /> + <Action name="options_configure" /> + </Menu> + <Menu name="help" > + <text>&Help</text> + <Action name="help_contents" /> + <Action name="help_plugins_contents" /> + <Action name="help_whats_this" /> + <Action name="help_show_tip" /> + <Separator/> + <Action name="help_welcome_baskets" /> +<!-- <Action name="help_report_bug" />--> + <Action name="likeback_send_a_comment" /> + <Separator/> + <Action name="help_about_app" /> + <Action name="help_about_kde" /> + </Menu> + </MenuBar> + <ToolBar newline="false" position="Top" noMerge="1" name="mainToolBar" fullWidth="true" > + <text>Main Toolbar</text> + <Action name="basket_new_menu" /> + <Action name="basket_properties" /> + <Separator lineSeparator="true" name="separator_0" /> + <Action name="edit_cut" /> + <Action name="edit_copy" /> + <Action name="edit_paste" /> + <Action name="edit_delete" /> + <Separator lineSeparator="true" name="separator_1" /> + <Action name="note_group" /> + </ToolBar> + <ToolBar newline="false" position="Top" noMerge="1" name="richTextEditToolBar" fullWidth="true" > + <text>Text Formating Toolbar</text> + <Action name="richtext_undo" /> + <Action name="richtext_redo" /> + <Separator lineSeparator="true" name="separator_1" /> + <Action name="richtext_font" /> + <Separator lineSeparator="false" name="separator_2" /> + <Action name="richtext_font_size" /> + <Separator lineSeparator="false" name="separator_3" /> + <Action name="richtext_color" /> + <Separator lineSeparator="true" name="separator_4" /> + <Action name="richtext_bold" /> + <Action name="richtext_italic" /> + <Action name="richtext_underline" /> +<!-- + <Separator lineSeparator="true" name="separator_2" /> + <Action name="richtext_super" /> + <Action name="richtext_sub" /> +--> + <Separator lineSeparator="true" name="separator_0" /> + <Action name="richtext_left" /> + <Action name="richtext_center" /> + <Action name="richtext_right" /> + <Action name="richtext_block" /> + </ToolBar> + <Menu name="basket_popup" > + <Action name="basket_new_menu" /> + <Separator/> + <Action name="basket_properties" /> + <Menu icon="fileexport" > + <text>&Export</text> + <Action name="basket_export_basket_archive" /> + <Action name="basket_export_html" /> + </Menu> + <Action name="basket_remove" /> + <Separator/> + <Action name="basket_password" /> + <Action name="basket_lock" /> + <Separator/> + <Menu icon="fileimport" > + <text>&Import</text> + <Action name="basket_import_basket_archive" /> + <Separator/> + <Action name="basket_import_knotes" /> + <Action name="basket_import_kjots" /> + <Action name="basket_import_knowit" /> + <Action name="basket_import_tuxcards" /> + <Separator/> + <Action name="basket_import_sticky_notes" /> + <Action name="basket_import_tomboy" /> + <Separator/> + <Action name="basket_import_text_file" /> + </Menu> + <Action name="basket_backup_restore" /> + </Menu> + <Menu name="tab_bar_popup" > + <Action name="basket_new" /> + <Menu icon="fileimport" > + <text>&Import</text> + <Action name="basket_import_basket_archive" /> + <Separator/> + <Action name="basket_import_knotes" /> + <Action name="basket_import_kjots" /> + <Action name="basket_import_knowit" /> + <Action name="basket_import_tuxcards" /> + <Separator/> + <Action name="basket_import_sticky_notes" /> + <Action name="basket_import_tomboy" /> + <Separator/> + <Action name="basket_import_text_file" /> + </Menu> + <Action name="basket_backup_restore" /> + </Menu> + <Menu noMerge="1" name="note_popup" > + <Action name="note_edit" /> + <Action name="note_open" /> + <Action name="note_open_with" /> + <Action name="note_save_to_file" /> + <Separator/> + <Action name="note_group" /> + <Action name="note_ungroup" /> + <Separator/> + <Action name="edit_cut" /> + <Action name="edit_copy" /> + <Action name="edit_delete" /> + </Menu> + <Menu noMerge="1" name="insert_popup" > + <Action name="insert_html" /> + <Action name="insert_image" /> + <Action name="insert_link" /> + <Action name="insert_launcher" /> + <Action name="insert_color" /> + <Separator/> + <Action name="insert_screen_capture" /> + <Action name="insert_screen_color" /> + <Separator/> + <Action name="insert_from_file" /> + <Action name="insert_kmenu" /> + <Action name="insert_icon" /> + <Separator/> + <Action name="edit_paste" /> + </Menu> +</kpartgui> diff --git a/src/bnpview.cpp b/src/bnpview.cpp new file mode 100644 index 0000000..b0d83f7 --- /dev/null +++ b/src/bnpview.cpp @@ -0,0 +1,2390 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + + /// NEW: + +#include <qwidgetstack.h> +#include <qregexp.h> +#include <qpixmap.h> +#include <qpainter.h> +#include <qimage.h> +#include <qbitmap.h> +#include <qwhatsthis.h> +#include <kpopupmenu.h> +#include <qsignalmapper.h> +#include <qdir.h> +#include <kicontheme.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kstringhandler.h> +#include <kmessagebox.h> +#include <kfiledialog.h> +#include <kprogress.h> +#include <kstandarddirs.h> +#include <kaboutdata.h> +#include <kwin.h> +#include <kaccel.h> +#include <kpassivepopup.h> +#include <kxmlguifactory.h> +#include <kcmdlineargs.h> +#include <kglobalaccel.h> +#include <kapplication.h> +#include <kkeydialog.h> +#include <dcopclient.h> +#include <kdebug.h> +#include <cstdlib> +#include <iostream> +#include "bnpview.h" +#include "basket.h" +#include "tools.h" +#include "settings.h" +#include "debugwindow.h" +#include "xmlwork.h" +#include "basketfactory.h" +#include "softwareimporters.h" +#include "colorpicker.h" +#include "regiongrabber.h" +#include "basketlistview.h" +#include "basketproperties.h" +#include "password.h" +#include "newbasketdialog.h" +#include "notedrag.h" +#include "formatimporter.h" +#include "basketstatusbar.h" +#include "backgroundmanager.h" +#include "noteedit.h" // To launch InlineEditors::initToolBars() +#include "archive.h" +#include "htmlexporter.h" +#include "crashhandler.h" +#include "likeback.h" +#include "backup.h" + +/** class BNPView: */ + +const int BNPView::c_delayTooltipTime = 275; + +BNPView::BNPView(QWidget *parent, const char *name, KXMLGUIClient *aGUIClient, + KActionCollection *actionCollection, BasketStatusBar *bar) + : DCOPObject("BasketIface"), QSplitter(Qt::Horizontal, parent, name), m_actLockBasket(0), m_actPassBasket(0), + m_loading(true), m_newBasketPopup(false), m_firstShow(true), + m_regionGrabber(0), m_passiveDroppedSelection(0), m_passivePopup(0), m_actionCollection(actionCollection), + m_guiClient(aGUIClient), m_statusbar(bar), m_tryHideTimer(0), m_hideTimer(0) +{ + /* Settings */ + Settings::loadConfig(); + + Global::bnpView = this; + + // Needed when loading the baskets: + Global::globalAccel = new KGlobalAccel(this); // FIXME: might be null (KPart case)! + Global::backgroundManager = new BackgroundManager(); + + setupGlobalShortcuts(); + initialize(); + QTimer::singleShot(0, this, SLOT(lateInit())); +} + +BNPView::~BNPView() +{ + int treeWidth = Global::bnpView->sizes()[Settings::treeOnLeft() ? 0 : 1]; + + Settings::setBasketTreeWidth(treeWidth); + + if (currentBasket() && currentBasket()->isDuringEdit()) + currentBasket()->closeEditor(); + + Settings::saveConfig(); + + Global::bnpView = 0; + + delete Global::systemTray; + Global::systemTray = 0; + delete m_colorPicker; + delete m_statusbar; + + NoteDrag::createAndEmptyCuttingTmpFolder(); // Clean the temporary folder we used +} + +void BNPView::lateInit() +{ +/* + InlineEditors* instance = InlineEditors::instance(); + + if(instance) + { + KToolBar* toolbar = instance->richTextToolBar(); + + if(toolbar) + toolbar->hide(); + } +*/ + if(!isPart()) + { + if (Settings::useSystray() && KCmdLineArgs::parsedArgs() && KCmdLineArgs::parsedArgs()->isSet("start-hidden")) + if(Global::mainWindow()) Global::mainWindow()->hide(); + else if (Settings::useSystray() && kapp->isRestored()) + if(Global::mainWindow()) Global::mainWindow()->setShown(!Settings::startDocked()); + else + showMainWindow(); + } + + // If the main window is hidden when session is saved, Container::queryClose() + // isn't called and the last value would be kept + Settings::setStartDocked(true); + Settings::saveConfig(); + + /* System tray icon */ + Global::systemTray = new SystemTray(Global::mainWindow()); + connect( Global::systemTray, SIGNAL(showPart()), this, SIGNAL(showPart()) ); + if (Settings::useSystray()) + Global::systemTray->show(); + + // Load baskets + DEBUG_WIN << "Baskets are loaded from " + Global::basketsFolder(); + + NoteDrag::createAndEmptyCuttingTmpFolder(); // If last exec hasn't done it: clean the temporary folder we will use + Tag::loadTags(); // Tags should be ready before loading baskets, but tags need the mainContainer to be ready to create KActions! + load(); + + // If no basket has been found, try to import from an older version, + if (!firstListViewItem()) { + QDir dir; + dir.mkdir(Global::basketsFolder()); + if (FormatImporter::shouldImportBaskets()) { + FormatImporter::importBaskets(); + load(); + } + if (!firstListViewItem()) { + // Create first basket: + BasketFactory::newBasket(/*icon=*/"", /*name=*/i18n("General"), /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/0); + } + } + + // Load the Welcome Baskets if it is the First Time: + if (!Settings::welcomeBasketsAdded()) { + addWelcomeBaskets(); + Settings::setWelcomeBasketsAdded(true); + Settings::saveConfig(); + } + + m_tryHideTimer = new QTimer(this); + m_hideTimer = new QTimer(this); + connect( m_tryHideTimer, SIGNAL(timeout()), this, SLOT(timeoutTryHide()) ); + connect( m_hideTimer, SIGNAL(timeout()), this, SLOT(timeoutHide()) ); + + // Preload every baskets for instant filtering: +/*StopWatch::start(100); + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + item->basket()->load(); + kapp->processEvents(); + ++it; + } +StopWatch::check(100);*/ +} + +void BNPView::addWelcomeBaskets() +{ + // Possible paths where to find the welcome basket archive, trying the translated one, and falling back to the English one: + QStringList possiblePaths; + if (QString(KGlobal::locale()->encoding()) == QString("UTF-8")) { // Welcome baskets are encoded in UTF-8. If the system is not, then use the English version: + possiblePaths.append(KGlobal::dirs()->findResource("data", "basket/welcome/Welcome_" + KGlobal::locale()->language() + ".baskets")); + possiblePaths.append(KGlobal::dirs()->findResource("data", "basket/welcome/Welcome_" + QStringList::split("_", KGlobal::locale()->language())[0] + ".baskets")); + } + possiblePaths.append(KGlobal::dirs()->findResource("data", "basket/welcome/Welcome_en_US.baskets")); + + // Take the first EXISTING basket archive found: + QDir dir; + QString path; + for (QStringList::Iterator it = possiblePaths.begin(); it != possiblePaths.end(); ++it) { + if (dir.exists(*it)) { + path = *it; + break; + } + } + + // Extract: + if (!path.isEmpty()) + Archive::open(path); +} + +void BNPView::onFirstShow() +{ + // Don't enable LikeBack until bnpview is shown. This way it works better with kontact. + /* LikeBack */ +/* Global::likeBack = new LikeBack(LikeBack::AllButtons, / *showBarByDefault=* /true, Global::config(), Global::about()); + Global::likeBack->setServer("basket.linux62.org", "/likeback/send.php"); + Global:likeBack->setAcceptedLanguages(QStringList::split(";", "en;fr"), i18n("Only english and french languages are accepted.")); + if (isPart()) + Global::likeBack->disableBar(); // See BNPView::shown() and BNPView::hide(). +*/ + + if (isPart()) + Global::likeBack->disableBar(); // See BNPView::shown() and BNPView::hide(). + +/* + LikeBack::init(Global::config(), Global::about(), LikeBack::AllButtons); + LikeBack::setServer("basket.linux62.org", "/likeback/send.php"); +// LikeBack::setServer("localhost", "/~seb/basket/likeback/send.php"); + LikeBack::setCustomLanguageMessage(i18n("Only english and french languages are accepted.")); +// LikeBack::setWindowNamesListing(LikeBack:: / *NoListing* / / *WarnUnnamedWindows* / AllWindows); +*/ + + // In late init, because we need kapp->mainWidget() to be set! + if (!isPart()) + connectTagsMenu(); + + m_statusbar->setupStatusBar(); + + int treeWidth = Settings::basketTreeWidth(); + if (treeWidth < 0) + treeWidth = m_tree->fontMetrics().maxWidth() * 11; + QValueList<int> splitterSizes; + splitterSizes.append(treeWidth); + setSizes(splitterSizes); +} + +void BNPView::setupGlobalShortcuts() +{ + /* Global shortcuts */ + KGlobalAccel *globalAccel = Global::globalAccel; // Better for the following lines + + // Ctrl+Shift+W only works when started standalone: + QWidget *basketMainWindow = (QWidget*) (Global::bnpView->parent()->inherits("MainWindow") ? Global::bnpView->parent() : 0); + + if (basketMainWindow) { + globalAccel->insert( "global_show_hide_main_window", i18n("Show/hide main window"), + i18n("Allows you to show main Window if it is hidden, and to hide it if it is shown."), + Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_W, Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_W, + basketMainWindow, SLOT(changeActive()), true, true ); + } + globalAccel->insert( "global_paste", i18n("Paste clipboard contents in current basket"), + i18n("Allows you to paste clipboard contents in the current basket without having to open the main window."), + Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_V, Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_V, + Global::bnpView, SLOT(globalPasteInCurrentBasket()), true, true ); + globalAccel->insert( "global_show_current_basket", i18n("Show current basket name"), + i18n("Allows you to know basket is current without opening the main window."), + "", "", + Global::bnpView, SLOT(showPassiveContentForced()), true, true ); + globalAccel->insert( "global_paste_selection", i18n("Paste selection in current basket"), + i18n("Allows you to paste clipboard selection in the current basket without having to open the main window."), + Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_S, Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_S, + Global::bnpView, SLOT(pasteSelInCurrentBasket()), true, true ); + globalAccel->insert( "global_new_basket", i18n("Create a new basket"), + i18n("Allows you to create a new basket without having to open the main window (you then can use the other global shortcuts to add a note, paste clipboard or paste selection in this new basket)."), + "", "", + Global::bnpView, SLOT(askNewBasket()), true, true ); + globalAccel->insert( "global_previous_basket", i18n("Go to previous basket"), + i18n("Allows you to change current basket to the previous one without having to open the main window."), + "", "", + Global::bnpView, SLOT(goToPreviousBasket()), true, true ); + globalAccel->insert( "global_next_basket", i18n("Go to next basket"), + i18n("Allows you to change current basket to the next one without having to open the main window."), + "", "", + Global::bnpView, SLOT(goToNextBasket()), true, true ); +// globalAccel->insert( "global_note_add_text", i18n("Insert plain text note"), +// i18n("Add a plain text note to the current basket without having to open the main window."), +// "", "", //Qt::CTRL+Qt::ALT+Qt::Key_T, Qt::CTRL+Qt::ALT+Qt::Key_T, +// Global::bnpView, SLOT(addNoteText()), true, true ); + globalAccel->insert( "global_note_add_html", i18n("Insert text note"), + i18n("Add a text note to the current basket without having to open the main window."), + Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_T, Qt::CTRL + Qt::ALT + Qt::SHIFT + Qt::Key_T, //"", "", + Global::bnpView, SLOT(addNoteHtml()), true, true ); + globalAccel->insert( "global_note_add_image", i18n("Insert image note"), + i18n("Add an image note to the current basket without having to open the main window."), + "", "", + Global::bnpView, SLOT(addNoteImage()), true, true ); + globalAccel->insert( "global_note_add_link", i18n("Insert link note"), + i18n("Add a link note to the current basket without having to open the main window."), + "", "", + Global::bnpView, SLOT(addNoteLink()), true, true ); + globalAccel->insert( "global_note_add_color", i18n("Insert color note"), + i18n("Add a color note to the current basket without having to open the main window."), + "", "", + Global::bnpView, SLOT(addNoteColor()), true, true ); + globalAccel->insert( "global_note_pick_color", i18n("Pick color from screen"), + i18n("Add a color note picked from one pixel on screen to the current basket without " + "having to open the main window."), + "", "", + Global::bnpView, SLOT(slotColorFromScreenGlobal()), true, true ); + globalAccel->insert( "global_note_grab_screenshot", i18n("Grab screen zone"), + i18n("Grab a screen zone as an image in the current basket without " + "having to open the main window."), + "", "", + Global::bnpView, SLOT(grabScreenshotGlobal()), true, true ); + globalAccel->readSettings(); + globalAccel->updateConnections(); +} + +void BNPView::initialize() +{ + /// Configure the List View Columns: + m_tree = new BasketTreeListView(this); + m_tree->addColumn(i18n("Baskets")); + m_tree->setColumnWidthMode(0, QListView::Maximum); + m_tree->setFullWidth(true); + m_tree->setSorting(-1/*Disabled*/); + m_tree->setRootIsDecorated(true); + m_tree->setTreeStepSize(16); + m_tree->setLineWidth(1); + m_tree->setMidLineWidth(0); + m_tree->setFocusPolicy(QWidget::NoFocus); + + /// Configure the List View Drag and Drop: + m_tree->setDragEnabled(true); + m_tree->setAcceptDrops(true); + m_tree->setItemsMovable(true); + m_tree->setDragAutoScroll(true); + m_tree->setDropVisualizer(true); + m_tree->setDropHighlighter(true); + + /// Configure the Splitter: + m_stack = new QWidgetStack(this); + + setOpaqueResize(true); + + setCollapsible(m_tree, true); + setCollapsible(m_stack, false); + setResizeMode(m_tree, QSplitter::KeepSize); + setResizeMode(m_stack, QSplitter::Stretch); + + /// Configure the List View Signals: + connect( m_tree, SIGNAL(returnPressed(QListViewItem*)), this, SLOT(slotPressed(QListViewItem*)) ); + connect( m_tree, SIGNAL(selectionChanged(QListViewItem*)), this, SLOT(slotPressed(QListViewItem*)) ); + connect( m_tree, SIGNAL(pressed(QListViewItem*)), this, SLOT(slotPressed(QListViewItem*)) ); + connect( m_tree, SIGNAL(expanded(QListViewItem*)), this, SLOT(needSave(QListViewItem*)) ); + connect( m_tree, SIGNAL(collapsed(QListViewItem*)), this, SLOT(needSave(QListViewItem*)) ); + connect( m_tree, SIGNAL(contextMenu(KListView*, QListViewItem*, const QPoint&)), this, SLOT(slotContextMenu(KListView*, QListViewItem*, const QPoint&)) ); + connect( m_tree, SIGNAL(mouseButtonPressed(int, QListViewItem*, const QPoint&, int)), this, SLOT(slotMouseButtonPressed(int, QListViewItem*, const QPoint&, int)) ); + connect( m_tree, SIGNAL(doubleClicked(QListViewItem*, const QPoint&, int)), this, SLOT(slotShowProperties(QListViewItem*, const QPoint&, int)) ); + + connect( m_tree, SIGNAL(expanded(QListViewItem*)), this, SIGNAL(basketChanged()) ); + connect( m_tree, SIGNAL(collapsed(QListViewItem*)), this, SIGNAL(basketChanged()) ); + connect( this, SIGNAL(basketNumberChanged(int)), this, SIGNAL(basketChanged()) ); + + connect( this, SIGNAL(basketNumberChanged(int)), this, SLOT(slotBasketNumberChanged(int)) ); + connect( this, SIGNAL(basketChanged()), this, SLOT(slotBasketChanged()) ); + + /* LikeBack */ + Global::likeBack = new LikeBack(LikeBack::AllButtons, /*showBarByDefault=*/false, Global::config(), Global::about()); + Global::likeBack->setServer("basket.linux62.org", "/likeback/send.php"); + +// There are too much comments, and people reading comments are more and more international, so we accept only English: +// Global::likeBack->setAcceptedLanguages(QStringList::split(";", "en;fr"), i18n("Please write in English or French.")); + +// if (isPart()) +// Global::likeBack->disableBar(); // See BNPView::shown() and BNPView::hide(). + + Global::likeBack->sendACommentAction(actionCollection()); // Just create it! + setupActions(); + + /// What's This Help for the tree: + QWhatsThis::add(m_tree, i18n( + "<h2>Basket Tree</h2>" + "Here is the list of your baskets. " + "You can organize your data by putting them in different baskets. " + "You can group baskets by subject by creating new baskets inside others. " + "You can browse between them by clicking a basket to open it, or reorganize them using drag and drop.")); + + setTreePlacement(Settings::treeOnLeft()); +} + +void BNPView::setupActions() +{ + m_actSaveAsArchive = new KAction( i18n("&Basket Archive..."), "baskets", 0, + this, SLOT(saveAsArchive()), actionCollection(), "basket_export_basket_archive" ); + m_actOpenArchive = new KAction( i18n("&Basket Archive..."), "baskets", 0, + this, SLOT(openArchive()), actionCollection(), "basket_import_basket_archive" ); + + m_actHideWindow = new KAction( i18n("&Hide Window"), "", KStdAccel::shortcut(KStdAccel::Close), + this, SLOT(hideOnEscape()), actionCollection(), "window_hide" ); + m_actHideWindow->setEnabled(Settings::useSystray()); // Init here ! + + m_actExportToHtml = new KAction( i18n("&HTML Web Page..."), "html", 0, + this, SLOT(exportToHTML()), actionCollection(), "basket_export_html" ); + new KAction( i18n("K&Notes"), "knotes", 0, + this, SLOT(importKNotes()), actionCollection(), "basket_import_knotes" ); + new KAction( i18n("K&Jots"), "kjots", 0, + this, SLOT(importKJots()), actionCollection(), "basket_import_kjots" ); + new KAction( i18n("&KnowIt..."), "knowit", 0, + this, SLOT(importKnowIt()), actionCollection(), "basket_import_knowit" ); + new KAction( i18n("Tux&Cards..."), "tuxcards", 0, + this, SLOT(importTuxCards()), actionCollection(), "basket_import_tuxcards" ); + new KAction( i18n("&Sticky Notes"), "gnome", 0, + this, SLOT(importStickyNotes()), actionCollection(), "basket_import_sticky_notes" ); + new KAction( i18n("&Tomboy"), "tintin", 0, + this, SLOT(importTomboy()), actionCollection(), "basket_import_tomboy" ); + new KAction( i18n("Text &File..."), "txt", 0, + this, SLOT(importTextFile()), actionCollection(), "basket_import_text_file" ); + + new KAction( i18n("&Backup && Restore..."), "", 0, + this, SLOT(backupRestore()), actionCollection(), "basket_backup_restore" ); + + /** Note : ****************************************************************/ + + m_actDelNote = new KAction( i18n("D&elete"), "editdelete", "Delete", + this, SLOT(delNote()), actionCollection(), "edit_delete" ); + m_actCutNote = KStdAction::cut( this, SLOT(cutNote()), actionCollection() ); + m_actCopyNote = KStdAction::copy( this, SLOT(copyNote()), actionCollection() ); + + m_actSelectAll = KStdAction::selectAll( this, SLOT( slotSelectAll() ), actionCollection() ); + m_actSelectAll->setStatusText( i18n( "Selects all notes" ) ); + m_actUnselectAll = new KAction( i18n( "U&nselect All" ), "", this, SLOT( slotUnselectAll() ), + actionCollection(), "edit_unselect_all" ); + m_actUnselectAll->setStatusText( i18n( "Unselects all selected notes" ) ); + m_actInvertSelection = new KAction( i18n( "&Invert Selection" ), CTRL+Key_Asterisk, + this, SLOT( slotInvertSelection() ), + actionCollection(), "edit_invert_selection" ); + m_actInvertSelection->setStatusText( i18n( "Inverts the current selection of notes" ) ); + + m_actEditNote = new KAction( i18n("Verb; not Menu", "&Edit..."), "edit", "Return", + this, SLOT(editNote()), actionCollection(), "note_edit" ); + + m_actOpenNote = KStdAction::open( this, SLOT(openNote()), actionCollection(), "note_open" ); + m_actOpenNote->setIcon("window_new"); + m_actOpenNote->setText(i18n("&Open")); + m_actOpenNote->setShortcut("F9"); + + m_actOpenNoteWith = new KAction( i18n("Open &With..."), "", "Shift+F9", + this, SLOT(openNoteWith()), actionCollection(), "note_open_with" ); + m_actSaveNoteAs = KStdAction::saveAs( this, SLOT(saveNoteAs()), actionCollection(), "note_save_to_file" ); + m_actSaveNoteAs->setIcon(""); + m_actSaveNoteAs->setText(i18n("&Save to File...")); + m_actSaveNoteAs->setShortcut("F10"); + + m_actGroup = new KAction( i18n("&Group"), "attach", "Ctrl+G", + this, SLOT(noteGroup()), actionCollection(), "note_group" ); + m_actUngroup = new KAction( i18n("U&ngroup"), "", "Ctrl+Shift+G", + this, SLOT(noteUngroup()), actionCollection(), "note_ungroup" ); + + m_actMoveOnTop = new KAction( i18n("Move on &Top"), "2uparrow", "Ctrl+Shift+Home", + this, SLOT(moveOnTop()), actionCollection(), "note_move_top" ); + m_actMoveNoteUp = new KAction( i18n("Move &Up"), "1uparrow", "Ctrl+Shift+Up", + this, SLOT(moveNoteUp()), actionCollection(), "note_move_up" ); + m_actMoveNoteDown = new KAction( i18n("Move &Down"), "1downarrow", "Ctrl+Shift+Down", + this, SLOT(moveNoteDown()), actionCollection(), "note_move_down" ); + m_actMoveOnBottom = new KAction( i18n("Move on &Bottom"), "2downarrow", "Ctrl+Shift+End", + this, SLOT(moveOnBottom()), actionCollection(), "note_move_bottom" ); +#if KDE_IS_VERSION( 3, 1, 90 ) // KDE 3.2.x + m_actPaste = KStdAction::pasteText( this, SLOT(pasteInCurrentBasket()), actionCollection() ); +#else + m_actPaste = KStdAction::paste( this, SLOT(pasteInCurrentBasket()), actionCollection() ); +#endif + + /** Insert : **************************************************************/ + + QSignalMapper *insertEmptyMapper = new QSignalMapper(this); + QSignalMapper *insertWizardMapper = new QSignalMapper(this); + connect( insertEmptyMapper, SIGNAL(mapped(int)), this, SLOT(insertEmpty(int)) ); + connect( insertWizardMapper, SIGNAL(mapped(int)), this, SLOT(insertWizard(int)) ); + +// m_actInsertText = new KAction( i18n("Plai&n Text"), "text", "Ctrl+T", actionCollection(), "insert_text" ); + m_actInsertHtml = new KAction( i18n("&Text"), "html", "Insert", actionCollection(), "insert_html" ); + m_actInsertLink = new KAction( i18n("&Link"), "link", "Ctrl+Y", actionCollection(), "insert_link" ); + m_actInsertImage = new KAction( i18n("&Image"), "image", "", actionCollection(), "insert_image" ); + m_actInsertColor = new KAction( i18n("&Color"), "colorset", "", actionCollection(), "insert_color" ); + m_actInsertLauncher=new KAction( i18n("L&auncher"), "launch", "", actionCollection(), "insert_launcher" ); + + m_actImportKMenu = new KAction( i18n("Import Launcher from &KDE Menu..."), "kmenu", "", actionCollection(), "insert_kmenu" ); + m_actImportIcon = new KAction( i18n("Im&port Icon..."), "icons", "", actionCollection(), "insert_icon" ); + m_actLoadFile = new KAction( i18n("Load From &File..."), "fileimport", "", actionCollection(), "insert_from_file" ); + +// connect( m_actInsertText, SIGNAL(activated()), insertEmptyMapper, SLOT(map()) ); + connect( m_actInsertHtml, SIGNAL(activated()), insertEmptyMapper, SLOT(map()) ); + connect( m_actInsertImage, SIGNAL(activated()), insertEmptyMapper, SLOT(map()) ); + connect( m_actInsertLink, SIGNAL(activated()), insertEmptyMapper, SLOT(map()) ); + connect( m_actInsertColor, SIGNAL(activated()), insertEmptyMapper, SLOT(map()) ); + connect( m_actInsertLauncher, SIGNAL(activated()), insertEmptyMapper, SLOT(map()) ); +// insertEmptyMapper->setMapping(m_actInsertText, NoteType::Text ); + insertEmptyMapper->setMapping(m_actInsertHtml, NoteType::Html ); + insertEmptyMapper->setMapping(m_actInsertImage, NoteType::Image ); + insertEmptyMapper->setMapping(m_actInsertLink, NoteType::Link ); + insertEmptyMapper->setMapping(m_actInsertColor, NoteType::Color ); + insertEmptyMapper->setMapping(m_actInsertLauncher, NoteType::Launcher); + + connect( m_actImportKMenu, SIGNAL(activated()), insertWizardMapper, SLOT(map()) ); + connect( m_actImportIcon, SIGNAL(activated()), insertWizardMapper, SLOT(map()) ); + connect( m_actLoadFile, SIGNAL(activated()), insertWizardMapper, SLOT(map()) ); + insertWizardMapper->setMapping(m_actImportKMenu, 1 ); + insertWizardMapper->setMapping(m_actImportIcon, 2 ); + insertWizardMapper->setMapping(m_actLoadFile, 3 ); + + m_colorPicker = new DesktopColorPicker(); + m_actColorPicker = new KAction( i18n("C&olor from Screen"), "kcolorchooser", "", + this, SLOT(slotColorFromScreen()), actionCollection(), "insert_screen_color" ); + connect( m_colorPicker, SIGNAL(pickedColor(const QColor&)), this, SLOT(colorPicked(const QColor&)) ); + connect( m_colorPicker, SIGNAL(canceledPick()), this, SLOT(colorPickingCanceled()) ); + + m_actGrabScreenshot = new KAction( i18n("Grab Screen &Zone"), "ksnapshot", "", + this, SLOT(grabScreenshot()), actionCollection(), "insert_screen_capture" ); + //connect( m_actGrabScreenshot, SIGNAL(regionGrabbed(const QPixmap&)), this, SLOT(screenshotGrabbed(const QPixmap&)) ); + //connect( m_colorPicker, SIGNAL(canceledPick()), this, SLOT(colorPickingCanceled()) ); + +// m_insertActions.append( m_actInsertText ); + m_insertActions.append( m_actInsertHtml ); + m_insertActions.append( m_actInsertLink ); + m_insertActions.append( m_actInsertImage ); + m_insertActions.append( m_actInsertColor ); + m_insertActions.append( m_actImportKMenu ); + m_insertActions.append( m_actInsertLauncher ); + m_insertActions.append( m_actImportIcon ); + m_insertActions.append( m_actLoadFile ); + m_insertActions.append( m_actColorPicker ); + m_insertActions.append( m_actGrabScreenshot ); + + /** Basket : **************************************************************/ + + // At this stage, main.cpp has not set kapp->mainWidget(), so Global::runInsideKontact() + // returns true. We do it ourself: + bool runInsideKontact = true; + QWidget *parentWidget = (QWidget*) parent(); + while (parentWidget) { + if (parentWidget->inherits("MainWindow")) + runInsideKontact = false; + parentWidget = (QWidget*) parentWidget->parent(); + } + + // Use the "basket" incon in Kontact so it is consistent with the Kontact "New..." icon + actNewBasket = new KAction( i18n("&New Basket..."), (runInsideKontact ? "basket" : "filenew"), KStdAccel::shortcut(KStdAccel::New), + this, SLOT(askNewBasket()), actionCollection(), "basket_new" ); + actNewSubBasket = new KAction( i18n("New &Sub-Basket..."), "", "Ctrl+Shift+N", + this, SLOT(askNewSubBasket()), actionCollection(), "basket_new_sub" ); + actNewSiblingBasket = new KAction( i18n("New Si&bling Basket..."), "", "", + this, SLOT(askNewSiblingBasket()), actionCollection(), "basket_new_sibling" ); + + KActionMenu *newBasketMenu = new KActionMenu(i18n("&New"), "filenew", actionCollection(), "basket_new_menu"); + newBasketMenu->insert(actNewBasket); + newBasketMenu->insert(actNewSubBasket); + newBasketMenu->insert(actNewSiblingBasket); + connect( newBasketMenu, SIGNAL(activated()), this, SLOT(askNewBasket()) ); + + m_actPropBasket = new KAction( i18n("&Properties..."), "misc", "F2", + this, SLOT(propBasket()), actionCollection(), "basket_properties" ); + m_actDelBasket = new KAction( i18n("Remove Basket", "&Remove"), "", 0, + this, SLOT(delBasket()), actionCollection(), "basket_remove" ); +#ifdef HAVE_LIBGPGME + m_actPassBasket = new KAction( i18n("Password protection", "Pass&word..."), "", 0, + this, SLOT(password()), actionCollection(), "basket_password" ); + m_actLockBasket = new KAction( i18n("Lock Basket", "&Lock"), "", "Ctrl+L", + this, SLOT(lockBasket()), actionCollection(), "basket_lock" ); +#endif + /** Edit : ****************************************************************/ + + //m_actUndo = KStdAction::undo( this, SLOT(undo()), actionCollection() ); + //m_actUndo->setEnabled(false); // Not yet implemented ! + //m_actRedo = KStdAction::redo( this, SLOT(redo()), actionCollection() ); + //m_actRedo->setEnabled(false); // Not yet implemented ! + + m_actShowFilter = new KToggleAction( i18n("&Filter"), "filter", KStdAccel::shortcut(KStdAccel::Find), + actionCollection(), "edit_filter" ); + connect( m_actShowFilter, SIGNAL(toggled(bool)), this, SLOT(showHideFilterBar(bool)) ); + + m_actFilterAllBaskets = new KToggleAction( i18n("Filter all &Baskets"), "find", "Ctrl+Shift+F", + actionCollection(), "edit_filter_all_baskets" ); + connect( m_actFilterAllBaskets, SIGNAL(toggled(bool)), this, SLOT(toggleFilterAllBaskets(bool)) ); + + m_actResetFilter = new KAction( i18n( "&Reset Filter" ), "locationbar_erase", "Ctrl+R", + this, SLOT( slotResetFilter() ), actionCollection(), "edit_filter_reset" ); + + /** Go : ******************************************************************/ + + m_actPreviousBasket = new KAction( i18n( "&Previous Basket" ), "up", "Alt+Up", + this, SLOT(goToPreviousBasket()), actionCollection(), "go_basket_previous" ); + m_actNextBasket = new KAction( i18n( "&Next Basket" ), "down", "Alt+Down", + this, SLOT(goToNextBasket()), actionCollection(), "go_basket_next" ); + m_actFoldBasket = new KAction( i18n( "&Fold Basket" ), "back", "Alt+Left", + this, SLOT(foldBasket()), actionCollection(), "go_basket_fold" ); + m_actExpandBasket = new KAction( i18n( "&Expand Basket" ), "forward", "Alt+Right", + this, SLOT(expandBasket()), actionCollection(), "go_basket_expand" ); + // FOR_BETA_PURPOSE: +// m_convertTexts = new KAction( i18n("Convert text notes to rich text notes"), "compfile", "", +// this, SLOT(convertTexts()), actionCollection(), "beta_convert_texts" ); + + InlineEditors::instance()->initToolBars(actionCollection()); + + actConfigGlobalShortcuts = KStdAction::keyBindings(this, SLOT(showGlobalShortcutsSettingsDialog()), + actionCollection(), "options_configure_global_keybinding"); + actConfigGlobalShortcuts->setText(i18n("Configure &Global Shortcuts...")); + + /** Help : ****************************************************************/ + + new KAction( i18n("&Welcome Baskets"), "", "", this, SLOT(addWelcomeBaskets()), actionCollection(), "help_welcome_baskets" ); +} + +QListViewItem* BNPView::firstListViewItem() +{ + return m_tree->firstChild(); +} + +void BNPView::slotShowProperties(QListViewItem *item, const QPoint&, int) +{ + if (item) + propBasket(); +} + +void BNPView::slotMouseButtonPressed(int button, QListViewItem *item, const QPoint &/*pos*/, int /*column*/) +{ + if (item && (button & Qt::MidButton)) { + // TODO: Paste into ((BasketListViewItem*)listViewItem)->basket() + } +} + +void BNPView::slotContextMenu(KListView */*listView*/, QListViewItem *item, const QPoint &pos) +{ + QString menuName; + if (item) { + Basket* basket = ((BasketListViewItem*)item)->basket(); + + setCurrentBasket(basket); + menuName = "basket_popup"; + } else { + menuName = "tab_bar_popup"; + /* + * "File -> New" create a new basket with the same parent basket as the the current one. + * But when invoked when right-clicking the empty area at the bottom of the basket tree, + * it is obvious the user want to create a new basket at the bottom of the tree (with no parent). + * So we set a temporary variable during the time the popup menu is shown, + * so the slot askNewBasket() will do the right thing: + */ + setNewBasketPopup(); + } + + QPopupMenu *menu = popupMenu(menuName); + connect( menu, SIGNAL(aboutToHide()), this, SLOT(aboutToHideNewBasketPopup()) ); + menu->exec(pos); +} + +void BNPView::save() +{ + DEBUG_WIN << "Basket Tree: Saving..."; + + // Create Document: + QDomDocument document("basketTree"); + QDomElement root = document.createElement("basketTree"); + document.appendChild(root); + + // Save Basket Tree: + save(m_tree->firstChild(), document, root); + + // Write to Disk: + Basket::safelySaveToFile(Global::basketsFolder() + "baskets.xml", "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + document.toString()); +// QFile file(Global::basketsFolder() + "baskets.xml"); +// if (file.open(IO_WriteOnly)) { +// QTextStream stream(&file); +// stream.setEncoding(QTextStream::UnicodeUTF8); +// QString xml = document.toString(); +// stream << "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"; +// stream << xml; +// file.close(); +// } +} + +void BNPView::save(QListViewItem *firstItem, QDomDocument &document, QDomElement &parentElement) +{ + QListViewItem *item = firstItem; + while (item) { +// Basket *basket = ((BasketListViewItem*)item)->basket(); + QDomElement basketElement = this->basketElement(item, document, parentElement); +/* + QDomElement basketElement = document.createElement("basket"); + parentElement.appendChild(basketElement); + // Save Attributes: + basketElement.setAttribute("folderName", basket->folderName()); + if (item->firstChild()) // If it can be expanded/folded: + basketElement.setAttribute("folded", XMLWork::trueOrFalse(!item->isOpen())); + if (((BasketListViewItem*)item)->isCurrentBasket()) + basketElement.setAttribute("lastOpened", "true"); + // Save Properties: + QDomElement properties = document.createElement("properties"); + basketElement.appendChild(properties); + basket->saveProperties(document, properties); +*/ + // Save Child Basket: + if (item->firstChild()) + save(item->firstChild(), document, basketElement); + // Next Basket: + item = item->nextSibling(); + } +} + +QDomElement BNPView::basketElement(QListViewItem *item, QDomDocument &document, QDomElement &parentElement) +{ + Basket *basket = ((BasketListViewItem*)item)->basket(); + QDomElement basketElement = document.createElement("basket"); + parentElement.appendChild(basketElement); + // Save Attributes: + basketElement.setAttribute("folderName", basket->folderName()); + if (item->firstChild()) // If it can be expanded/folded: + basketElement.setAttribute("folded", XMLWork::trueOrFalse(!item->isOpen())); + if (((BasketListViewItem*)item)->isCurrentBasket()) + basketElement.setAttribute("lastOpened", "true"); + // Save Properties: + QDomElement properties = document.createElement("properties"); + basketElement.appendChild(properties); + basket->saveProperties(document, properties); + return basketElement; +} + +void BNPView::saveSubHierarchy(QListViewItem *item, QDomDocument &document, QDomElement &parentElement, bool recursive) +{ + QDomElement element = basketElement(item, document, parentElement); + if (recursive && item->firstChild()) + save(item->firstChild(), document, element); +} + +void BNPView::load() +{ + QDomDocument *doc = XMLWork::openFile("basketTree", Global::basketsFolder() + "baskets.xml"); + //BEGIN Compatibility with 0.6.0 Pre-Alpha versions: + if (!doc) + doc = XMLWork::openFile("basketsTree", Global::basketsFolder() + "baskets.xml"); + //END + if (doc != 0) { + QDomElement docElem = doc->documentElement(); + load(m_tree, 0L, docElem); + } + m_loading = false; +} + +void BNPView::load(KListView */*listView*/, QListViewItem *item, const QDomElement &baskets) +{ + QDomNode n = baskets.firstChild(); + while ( ! n.isNull() ) { + QDomElement element = n.toElement(); + if ( (!element.isNull()) && element.tagName() == "basket" ) { + QString folderName = element.attribute("folderName"); + if (!folderName.isEmpty()) { + Basket *basket = loadBasket(folderName); + BasketListViewItem *basketItem = appendBasket(basket, item); + basketItem->setOpen(!XMLWork::trueOrFalse(element.attribute("folded", "false"), false)); + basket->loadProperties(XMLWork::getElement(element, "properties")); + if (XMLWork::trueOrFalse(element.attribute("lastOpened", element.attribute("lastOpenned", "false")), false)) // Compat with 0.6.0-Alphas + setCurrentBasket(basket); + // Load Sub-baskets: + load(/*(QListView*)*/0L, basketItem, element); + } + } + n = n.nextSibling(); + } +} + +Basket* BNPView::loadBasket(const QString &folderName) +{ + if (folderName.isEmpty()) + return 0; + + DecoratedBasket *decoBasket = new DecoratedBasket(m_stack, folderName); + Basket *basket = decoBasket->basket(); + m_stack->addWidget(decoBasket); + connect( basket, SIGNAL(countsChanged(Basket*)), this, SLOT(countsChanged(Basket*)) ); + // Important: Create listViewItem and connect signal BEFORE loadProperties(), so we get the listViewItem updated without extra work: + connect( basket, SIGNAL(propertiesChanged(Basket*)), this, SLOT(updateBasketListViewItem(Basket*)) ); + + connect( basket->decoration()->filterBar(), SIGNAL(newFilter(const FilterData&)), this, SLOT(newFilterFromFilterBar()) ); + + return basket; +} + +int BNPView::basketCount(QListViewItem *parent) +{ + int count = 0; + + QListViewItem *item = (parent ? parent->firstChild() : m_tree->firstChild()); + while (item) { + count += 1 + basketCount(item); + item = item->nextSibling(); + } + + return count; +} + +bool BNPView::canFold() +{ + BasketListViewItem *item = listViewItemForBasket(currentBasket()); + if (!item) + return false; + return item->parent() || (item->firstChild() && item->isOpen()); +} + +bool BNPView::canExpand() +{ + BasketListViewItem *item = listViewItemForBasket(currentBasket()); + if (!item) + return false; + return item->firstChild(); +} + +BasketListViewItem* BNPView::appendBasket(Basket *basket, QListViewItem *parentItem) +{ + BasketListViewItem *newBasketItem; + if (parentItem) + newBasketItem = new BasketListViewItem(parentItem, ((BasketListViewItem*)parentItem)->lastChild(), basket); + else { + QListViewItem *child = m_tree->firstChild(); + QListViewItem *lastChild = 0; + while (child) { + lastChild = child; + child = child->nextSibling(); + } + newBasketItem = new BasketListViewItem(m_tree, lastChild, basket); + } + + emit basketNumberChanged(basketCount()); + + return newBasketItem; +} + +void BNPView::loadNewBasket(const QString &folderName, const QDomElement &properties, Basket *parent) +{ + Basket *basket = loadBasket(folderName); + appendBasket(basket, (basket ? listViewItemForBasket(parent) : 0)); + basket->loadProperties(properties); + setCurrentBasket(basket); +// save(); +} + +BasketListViewItem* BNPView::lastListViewItem() +{ + QListViewItem *child = m_tree->firstChild(); + QListViewItem *lastChild = 0; + // Set lastChild to the last primary child of the list view: + while (child) { + lastChild = child; + child = child->nextSibling(); + } + // If this child have child(s), recursivly browse through them to find the real last one: + while (lastChild && lastChild->firstChild()) { + child = lastChild->firstChild(); + while (child) { + lastChild = child; + child = child->nextSibling(); + } + } + return (BasketListViewItem*)lastChild; +} + +void BNPView::goToPreviousBasket() +{ + if (!m_tree->firstChild()) + return; + + BasketListViewItem *item = listViewItemForBasket(currentBasket()); + BasketListViewItem *toSwitch = item->shownItemAbove(); + if (!toSwitch) { + toSwitch = lastListViewItem(); + if (toSwitch && !toSwitch->isShown()) + toSwitch = toSwitch->shownItemAbove(); + } + + if (toSwitch) + setCurrentBasket(toSwitch->basket()); + + if (Settings::usePassivePopup()) + showPassiveContent(); +} + +void BNPView::goToNextBasket() +{ + if (!m_tree->firstChild()) + return; + + BasketListViewItem *item = listViewItemForBasket(currentBasket()); + BasketListViewItem *toSwitch = item->shownItemBelow(); + if (!toSwitch) + toSwitch = ((BasketListViewItem*)m_tree->firstChild()); + + if (toSwitch) + setCurrentBasket(toSwitch->basket()); + + if (Settings::usePassivePopup()) + showPassiveContent(); +} + +void BNPView::foldBasket() +{ + BasketListViewItem *item = listViewItemForBasket(currentBasket()); + if (item && !item->firstChild()) + item->setOpen(false); // If Alt+Left is hitted and there is nothing to close, make sure the focus will go to the parent basket + + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Left, 0, 0); + QApplication::postEvent(m_tree, keyEvent); +} + +void BNPView::expandBasket() +{ + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Right, 0, 0); + QApplication::postEvent(m_tree, keyEvent); +} + +void BNPView::closeAllEditors() +{ + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = (BasketListViewItem*)(it.current()); + item->basket()->closeEditor(); + ++it; + } +} + +bool BNPView::convertTexts() +{ + bool convertedNotes = false; + KProgressDialog dialog( + /*parent=*/0, + /*name=*/"", + /*caption=*/i18n("Plain Text Notes Conversion"), + /*text=*/i18n("Converting plain text notes to rich text ones..."), + /*modal=*/true); + dialog.progressBar()->setTotalSteps(basketCount()); + dialog.show(); //setMinimumDuration(50/*ms*/); + + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = (BasketListViewItem*)(it.current()); + if (item->basket()->convertTexts()) + convertedNotes = true; + dialog.progressBar()->advance(1); + if (dialog.wasCancelled()) + break; + ++it; + } + + return convertedNotes; +} + +/** isRunning is to avoid recursive calls because this method can be called + * when clicking the menu action or when using the filter-bar icon... either of those calls + * call the other to be checked... and it can cause recursive calls. + * PS: Uggly hack? Yes, I think so :-) + */ +void BNPView::toggleFilterAllBaskets(bool doFilter) +{ + static bool isRunning = false; + if (isRunning) + return; + isRunning = true; + + // Set the state: + m_actFilterAllBaskets->setChecked(doFilter); + //currentBasket()->decoration()->filterBar()->setFilterAll(doFilter); + +// Basket *current = currentBasket(); + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + item->basket()->decoration()->filterBar()->setFilterAll(doFilter); + ++it; + } + + // Protection is not necessary anymore: + isRunning = false; + + if (doFilter) + currentBasket()->decoration()->filterBar()->setEditFocus(); + + // Filter every baskets: + newFilter(); +} + +/** This function can be called recursively because we call kapp->processEvents(). + * If this function is called whereas another "instance" is running, + * this new "instance" leave and set up a flag that is read by the first "instance" + * to know it should re-begin the work. + * PS: Yes, that's a very lame pseudo-threading but that works, and it's programmer-efforts cheap :-) + */ +void BNPView::newFilter() +{ + static bool alreadyEntered = false; + static bool shouldRestart = false; + + if (alreadyEntered) { + shouldRestart = true; + return; + } + alreadyEntered = true; + shouldRestart = false; + + Basket *current = currentBasket(); + const FilterData &filterData = current->decoration()->filterBar()->filterData(); + + // Set the filter data for every other baskets, or reset the filter for every other baskets if we just disabled the filterInAllBaskets: + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + if (item->basket() != current) + if (isFilteringAllBaskets()) + item->basket()->decoration()->filterBar()->setFilterData(filterData); // Set the new FilterData for every other baskets + else + item->basket()->decoration()->filterBar()->setFilterData(FilterData()); // We just disabled the global filtering: remove the FilterData + ++it; + } + + // Show/hide the "little filter icons" (during basket load) + // or the "little numbers" (to show number of found notes in the baskets) is the tree: + m_tree->triggerUpdate(); + kapp->processEvents(); + + // Load every baskets for filtering, if they are not already loaded, and if necessary: + if (filterData.isFiltering) { + Basket *current = currentBasket(); + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + if (item->basket() != current) { + Basket *basket = item->basket(); + if (!basket->loadingLaunched() && !basket->isLocked()) + basket->load(); + basket->filterAgain(); + m_tree->triggerUpdate(); + kapp->processEvents(); + if (shouldRestart) { + alreadyEntered = false; + shouldRestart = false; + newFilter(); + return; + } + } + ++it; + } + } + + m_tree->triggerUpdate(); +// kapp->processEvents(); + + alreadyEntered = false; + shouldRestart = false; +} + +void BNPView::newFilterFromFilterBar() +{ + if (isFilteringAllBaskets()) + QTimer::singleShot(0, this, SLOT(newFilter())); // Keep time for the QLineEdit to display the filtered character and refresh correctly! +} + +bool BNPView::isFilteringAllBaskets() +{ + return m_actFilterAllBaskets->isChecked(); +} + + +BasketListViewItem* BNPView::listViewItemForBasket(Basket *basket) +{ + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + if (item->basket() == basket) + return item; + ++it; + } + return 0L; +} + +Basket* BNPView::currentBasket() +{ + DecoratedBasket *decoBasket = (DecoratedBasket*)m_stack->visibleWidget(); + if (decoBasket) + return decoBasket->basket(); + else + return 0; +} + +Basket* BNPView::parentBasketOf(Basket *basket) +{ + BasketListViewItem *item = (BasketListViewItem*)(listViewItemForBasket(basket)->parent()); + if (item) + return item->basket(); + else + return 0; +} + +void BNPView::setCurrentBasket(Basket *basket) +{ + if (currentBasket() == basket) + return; + + if (currentBasket()) + currentBasket()->closeBasket(); + + if (basket) + basket->aboutToBeActivated(); + + BasketListViewItem *item = listViewItemForBasket(basket); + if (item) { + m_tree->setSelected(item, true); + item->ensureVisible(); + m_stack->raiseWidget(basket->decoration()); + // If the window has changed size, only the current basket receive the event, + // the others will receive ony one just before they are shown. + // But this triggers unwanted animations, so we eliminate it: + basket->relayoutNotes(/*animate=*/false); + basket->openBasket(); + setCaption(item->basket()->basketName()); + countsChanged(basket); + updateStatusBarHint(); + if (Global::systemTray) + Global::systemTray->updateToolTip(); + m_tree->ensureItemVisible(m_tree->currentItem()); + item->basket()->setFocus(); + } + m_tree->viewport()->update(); + emit basketChanged(); +} + +void BNPView::removeBasket(Basket *basket) +{ + if (basket->isDuringEdit()) + basket->closeEditor(); + + // Find a new basket to switch to and select it. + // Strategy: get the next sibling, or the previous one if not found. + // If there is no such one, get the parent basket: + BasketListViewItem *basketItem = listViewItemForBasket(basket); + BasketListViewItem *nextBasketItem = (BasketListViewItem*)(basketItem->nextSibling()); + if (!nextBasketItem) + nextBasketItem = basketItem->prevSibling(); + if (!nextBasketItem) + nextBasketItem = (BasketListViewItem*)(basketItem->parent()); + + if (nextBasketItem) + setCurrentBasket(nextBasketItem->basket()); + + // Remove from the view: + basket->unsubscribeBackgroundImages(); + m_stack->removeWidget(basket->decoration()); +// delete basket->decoration(); + delete basketItem; +// delete basket; + + // If there is no basket anymore, add a new one: + if (!nextBasketItem) + BasketFactory::newBasket(/*icon=*/"", /*name=*/i18n("General"), /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/0); + else // No need to save two times if we add a basket + save(); + + emit basketNumberChanged(basketCount()); +} + +void BNPView::setTreePlacement(bool onLeft) +{ + if (onLeft) + moveToFirst(m_tree); + else + moveToLast(m_tree); + //updateGeometry(); + kapp->postEvent( this, new QResizeEvent(size(), size()) ); +} + +void BNPView::relayoutAllBaskets() +{ + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + //item->basket()->unbufferizeAll(); + item->basket()->unsetNotesWidth(); + item->basket()->relayoutNotes(true); + ++it; + } +} + +void BNPView::recomputeAllStyles() +{ + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + item->basket()->recomputeAllStyles(); + item->basket()->unsetNotesWidth(); + item->basket()->relayoutNotes(true); + ++it; + } +} + +void BNPView::removedStates(const QValueList<State*> &deletedStates) +{ + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + item->basket()->removedStates(deletedStates); + ++it; + } +} + +void BNPView::linkLookChanged() +{ + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = ((BasketListViewItem*)it.current()); + item->basket()->linkLookChanged(); + ++it; + } +} + +void BNPView::filterPlacementChanged(bool onTop) +{ + QListViewItemIterator it(m_tree); + while (it.current()) { + BasketListViewItem *item = static_cast<BasketListViewItem*>(it.current()); + DecoratedBasket *decoration = static_cast<DecoratedBasket*>(item->basket()->parent()); + decoration->setFilterBarPosition(onTop); + ++it; + } +} + +void BNPView::updateBasketListViewItem(Basket *basket) +{ + BasketListViewItem *item = listViewItemForBasket(basket); + if (item) + item->setup(); + + if (basket == currentBasket()) { + setCaption(basket->basketName()); + if (Global::systemTray) + Global::systemTray->updateToolTip(); + } + + // Don't save if we are loading! + if (!m_loading) + save(); +} + +void BNPView::needSave(QListViewItem*) +{ + if (!m_loading) + // A basket has been collapsed/expanded or a new one is select: this is not urgent: + QTimer::singleShot(500/*ms*/, this, SLOT(save())); +} + +void BNPView::slotPressed(QListViewItem *item, const QPoint &/*pos*/, int /*column*/) +{ + Basket *basket = currentBasket(); + if (basket == 0) + return; + + // Impossible to Select no Basket: + if (!item) + m_tree->setSelected(listViewItemForBasket(basket), true); + else if (dynamic_cast<BasketListViewItem*>(item) != 0 && currentBasket() != ((BasketListViewItem*)item)->basket()) { + setCurrentBasket( ((BasketListViewItem*)item)->basket() ); + needSave(0); + } + basket->setFocus(); +} + +DecoratedBasket* BNPView::currentDecoratedBasket() +{ + if (currentBasket()) + return currentBasket()->decoration(); + else + return 0; +} + +// Redirected actions : + +void BNPView::exportToHTML() { HTMLExporter exporter(currentBasket()); } +void BNPView::editNote() { currentBasket()->noteEdit(); } +void BNPView::cutNote() { currentBasket()->noteCut(); } +void BNPView::copyNote() { currentBasket()->noteCopy(); } +void BNPView::delNote() { currentBasket()->noteDelete(); } +void BNPView::openNote() { currentBasket()->noteOpen(); } +void BNPView::openNoteWith() { currentBasket()->noteOpenWith(); } +void BNPView::saveNoteAs() { currentBasket()->noteSaveAs(); } +void BNPView::noteGroup() { currentBasket()->noteGroup(); } +void BNPView::noteUngroup() { currentBasket()->noteUngroup(); } +void BNPView::moveOnTop() { currentBasket()->noteMoveOnTop(); } +void BNPView::moveOnBottom() { currentBasket()->noteMoveOnBottom(); } +void BNPView::moveNoteUp() { currentBasket()->noteMoveNoteUp(); } +void BNPView::moveNoteDown() { currentBasket()->noteMoveNoteDown(); } +void BNPView::slotSelectAll() { currentBasket()->selectAll(); } +void BNPView::slotUnselectAll() { currentBasket()->unselectAll(); } +void BNPView::slotInvertSelection() { currentBasket()->invertSelection(); } +void BNPView::slotResetFilter() { currentDecoratedBasket()->resetFilter(); } + +void BNPView::importKJots() { SoftwareImporters::importKJots(); } +void BNPView::importKNotes() { SoftwareImporters::importKNotes(); } +void BNPView::importKnowIt() { SoftwareImporters::importKnowIt(); } +void BNPView::importTuxCards() { SoftwareImporters::importTuxCards(); } +void BNPView::importStickyNotes() { SoftwareImporters::importStickyNotes(); } +void BNPView::importTomboy() { SoftwareImporters::importTomboy(); } +void BNPView::importTextFile() { SoftwareImporters::importTextFile(); } + +void BNPView::backupRestore() +{ + BackupDialog dialog; + dialog.exec(); +} + +void BNPView::countsChanged(Basket *basket) +{ + if (basket == currentBasket()) + notesStateChanged(); +} + +void BNPView::notesStateChanged() +{ + Basket *basket = currentBasket(); + + // Update statusbar message : + if (currentBasket()->isLocked()) + setSelectionStatus(i18n("Locked")); + else if (!basket->isLoaded()) + setSelectionStatus(i18n("Loading...")); + else if (basket->count() == 0) + setSelectionStatus(i18n("No notes")); + else { + QString count = i18n("%n note", "%n notes", basket->count() ); + QString selecteds = i18n("%n selected", "%n selected", basket->countSelecteds()); + QString showns = (currentDecoratedBasket()->filterData().isFiltering ? i18n("all matches") : i18n("no filter")); + if (basket->countFounds() != basket->count()) + showns = i18n("%n match", "%n matches", basket->countFounds()); + setSelectionStatus( + i18n("e.g. '18 notes, 10 matches, 5 selected'", "%1, %2, %3").arg(count, showns, selecteds) ); + } + + // If we added a note that match the global filter, update the count number in the tree: + if (isFilteringAllBaskets()) + listViewItemForBasket(basket)->listView()->triggerUpdate(); + + if (currentBasket()->redirectEditActions()) { + m_actSelectAll ->setEnabled( !currentBasket()->selectedAllTextInEditor() ); + m_actUnselectAll ->setEnabled( currentBasket()->hasSelectedTextInEditor() ); + } else { + m_actSelectAll ->setEnabled( basket->countSelecteds() < basket->countFounds() ); + m_actUnselectAll ->setEnabled( basket->countSelecteds() > 0 ); + } + m_actInvertSelection ->setEnabled( basket->countFounds() > 0 ); + + updateNotesActions(); +} + +void BNPView::updateNotesActions() +{ + bool isLocked = currentBasket()->isLocked(); + bool oneSelected = currentBasket()->countSelecteds() == 1; + bool oneOrSeveralSelected = currentBasket()->countSelecteds() >= 1; + bool severalSelected = currentBasket()->countSelecteds() >= 2; + + // FIXME: m_actCheckNotes is also modified in void BNPView::areSelectedNotesCheckedChanged(bool checked) + // bool Basket::areSelectedNotesChecked() should return false if bool Basket::showCheckBoxes() is false +// m_actCheckNotes->setChecked( oneOrSeveralSelected && +// currentBasket()->areSelectedNotesChecked() && +// currentBasket()->showCheckBoxes() ); + + Note *selectedGroup = (severalSelected ? currentBasket()->selectedGroup() : 0); + + m_actEditNote ->setEnabled( !isLocked && oneSelected && !currentBasket()->isDuringEdit() ); + if (currentBasket()->redirectEditActions()) { + m_actCutNote ->setEnabled( currentBasket()->hasSelectedTextInEditor() ); + m_actCopyNote ->setEnabled( currentBasket()->hasSelectedTextInEditor() ); + m_actPaste ->setEnabled( true ); + m_actDelNote ->setEnabled( currentBasket()->hasSelectedTextInEditor() ); + } else { + m_actCutNote ->setEnabled( !isLocked && oneOrSeveralSelected ); + m_actCopyNote ->setEnabled( oneOrSeveralSelected ); + m_actPaste ->setEnabled( !isLocked ); + m_actDelNote ->setEnabled( !isLocked && oneOrSeveralSelected ); + } + m_actOpenNote ->setEnabled( oneOrSeveralSelected ); + m_actOpenNoteWith ->setEnabled( oneSelected ); // TODO: oneOrSeveralSelected IF SAME TYPE + m_actSaveNoteAs ->setEnabled( oneSelected ); // IDEM? + m_actGroup ->setEnabled( !isLocked && severalSelected && (!selectedGroup || selectedGroup->isColumn()) ); + m_actUngroup ->setEnabled( !isLocked && selectedGroup && !selectedGroup->isColumn() ); + m_actMoveOnTop ->setEnabled( !isLocked && oneOrSeveralSelected && !currentBasket()->isFreeLayout() ); + m_actMoveNoteUp ->setEnabled( !isLocked && oneOrSeveralSelected ); // TODO: Disable when unavailable! + m_actMoveNoteDown ->setEnabled( !isLocked && oneOrSeveralSelected ); + m_actMoveOnBottom ->setEnabled( !isLocked && oneOrSeveralSelected && !currentBasket()->isFreeLayout() ); + + for (KAction *action = m_insertActions.first(); action; action = m_insertActions.next()) + action->setEnabled( !isLocked ); + + // From the old Note::contextMenuEvent(...) : +/* if (useFile() || m_type == Link) { + m_type == Link ? i18n("&Open target") : i18n("&Open") + m_type == Link ? i18n("Open target &with...") : i18n("Open &with...") + m_type == Link ? i18n("&Save target as...") : i18n("&Save a copy as...") + // If useFile() theire is always a file to open / open with / save, but : + if (m_type == Link) { + if (url().prettyURL().isEmpty() && runCommand().isEmpty()) // no URL nor runCommand : + popupMenu->setItemEnabled(7, false); // no possible Open ! + if (url().prettyURL().isEmpty()) // no URL : + popupMenu->setItemEnabled(8, false); // no possible Open with ! + if (url().prettyURL().isEmpty() || url().path().endsWith("/")) // no URL or target a folder : + popupMenu->setItemEnabled(9, false); // not possible to save target file +} +} else if (m_type != Color) { + popupMenu->insertSeparator(); + popupMenu->insertItem( SmallIconSet("filesaveas"), i18n("&Save a copy as..."), this, SLOT(slotSaveAs()), 0, 10 ); +}*/ +} + +// BEGIN Color picker (code from KColorEdit): + +/* Activate the mode + */ +void BNPView::slotColorFromScreen(bool global) +{ + m_colorPickWasGlobal = global; + if (isMainWindowActive()) { + if(Global::mainWindow()) Global::mainWindow()->hide(); + m_colorPickWasShown = true; + } else + m_colorPickWasShown = false; + + currentBasket()->saveInsertionData(); + m_colorPicker->pickColor(); + +/* m_gettingColorFromScreen = true; + kapp->processEvents(); + QTimer::singleShot( 100, this, SLOT(grabColorFromScreen()) );*/ +} + +void BNPView::slotColorFromScreenGlobal() +{ + slotColorFromScreen(true); +} + +void BNPView::colorPicked(const QColor &color) +{ + if (!currentBasket()->isLoaded()) { + showPassiveLoading(currentBasket()); + currentBasket()->load(); + } + currentBasket()->insertColor(color); + + if (m_colorPickWasShown) + showMainWindow(); + + if (Settings::usePassivePopup()) + showPassiveDropped(i18n("Picked color to basket <i>%1</i>")); +} + +void BNPView::colorPickingCanceled() +{ + if (m_colorPickWasShown) + showMainWindow(); +} + +void BNPView::slotConvertTexts() +{ +/* + int result = KMessageBox::questionYesNoCancel( + this, + i18n( + "<p>This will convert every text notes into rich text notes.<br>" + "The content of the notes will not change and you will be able to apply formating to those notes.</p>" + "<p>This process cannot be reverted back: you will not be able to convert the rich text notes to plain text ones later.</p>" + "<p>As a beta-tester, you are strongly encouraged to do the convert process because it is to test if plain text notes are still needed.<br>" + "If nobody complain about not having plain text notes anymore, then the final version is likely to not support plain text notes anymore.</p>" + "<p><b>Which basket notes do you want to convert?</b></p>" + ), + i18n("Convert Text Notes"), + KGuiItem(i18n("Only in the Current Basket")), + KGuiItem(i18n("In Every Baskets")) + ); + if (result == KMessageBox::Cancel) + return; +*/ + + bool conversionsDone; +// if (result == KMessageBox::Yes) +// conversionsDone = currentBasket()->convertTexts(); +// else + conversionsDone = convertTexts(); + + if (conversionsDone) + KMessageBox::information(this, i18n("The plain text notes have been converted to rich text."), i18n("Conversion Finished")); + else + KMessageBox::information(this, i18n("There are no plain text notes to convert."), i18n("Conversion Finished")); +} + +QPopupMenu* BNPView::popupMenu(const QString &menuName) +{ + QPopupMenu *menu = 0; + bool hack = false; // TODO fix this + // When running in kontact and likeback Information message is shown + // factory is 0. Don't show error then and don't crash either :-) + + if(m_guiClient) + { + KXMLGUIFactory* factory = m_guiClient->factory(); + if(factory) + { + menu = (QPopupMenu *)factory->container(menuName, m_guiClient); + } + else + hack = isPart(); + } + if (menu == 0) { + if(!hack) + { + KStandardDirs stdDirs; + KMessageBox::error( this, i18n( + "<p><b>The file basketui.rc seems to not exist or is too old.<br>" + "%1 cannot run without it and will stop.</b></p>" + "<p>Please check your installation of %2.</p>" + "<p>If you do not have administrator access to install the application " + "system wide, you can copy the file basketui.rc from the installation " + "archive to the folder <a href='file://%3'>%4</a>.</p>" + "<p>As last ressort, if you are sure the application is correctly installed " + "but you had a preview version of it, try to remove the " + "file %5basketui.rc</p>") + .arg(kapp->aboutData()->programName(), kapp->aboutData()->programName(), + stdDirs.saveLocation("data", "basket/")).arg(stdDirs.saveLocation("data", "basket/"), stdDirs.saveLocation("data", "basket/")), + i18n("Ressource not Found"), KMessageBox::AllowLink ); + } + if(!isPart()) + exit(1); // We SHOULD exit right now and abord everything because the caller except menu != 0 to not crash. + else + menu = new KPopupMenu; // When running in kpart we cannot exit + } + return menu; +} + +void BNPView::showHideFilterBar(bool show, bool switchFocus) +{ +// if (show != m_actShowFilter->isChecked()) +// m_actShowFilter->setChecked(show); + m_actShowFilter->setChecked(currentDecoratedBasket()->filterData().isFiltering); + + currentDecoratedBasket()->setFilterBarShown(show, switchFocus); + currentDecoratedBasket()->resetFilter(); +} + +void BNPView::insertEmpty(int type) +{ + if (currentBasket()->isLocked()) { + showPassiveImpossible(i18n("Cannot add note.")); + return; + } + currentBasket()->insertEmptyNote(type); +} + +void BNPView::insertWizard(int type) +{ + if (currentBasket()->isLocked()) { + showPassiveImpossible(i18n("Cannot add note.")); + return; + } + currentBasket()->insertWizard(type); +} + +// BEGIN Screen Grabbing: // FIXME + +void BNPView::grabScreenshot(bool global) +{ + if (m_regionGrabber) { + KWin::activateWindow(m_regionGrabber->winId()); + return; + } + + // Delay before to take a screenshot because if we hide the main window OR the systray popup menu, + // we should wait the windows below to be repainted!!! + // A special case is where the action is triggered with the global keyboard shortcut. + // In this case, global is true, and we don't wait. + // In the future, if global is also defined for other cases, check for + // enum KAction::ActivationReason { UnknownActivation, EmulatedActivation, AccelActivation, PopupMenuActivation, ToolBarActivation }; + int delay = (isMainWindowActive() ? 500 : (global/*kapp->activePopupWidget()*/ ? 0 : 200)); + + m_colorPickWasGlobal = global; + if (isMainWindowActive()) { + if(Global::mainWindow()) Global::mainWindow()->hide(); + m_colorPickWasShown = true; + } else + m_colorPickWasShown = false; + + currentBasket()->saveInsertionData(); + m_regionGrabber = new RegionGrabber(delay); + connect( m_regionGrabber, SIGNAL(regionGrabbed(const QPixmap&)), this, SLOT(screenshotGrabbed(const QPixmap&)) ); +} + +void BNPView::grabScreenshotGlobal() +{ + grabScreenshot(true); +} + +void BNPView::screenshotGrabbed(const QPixmap &pixmap) +{ + delete m_regionGrabber; + m_regionGrabber = 0; + + // Cancelled (pressed Escape): + if (pixmap.isNull()) { + if (m_colorPickWasShown) + showMainWindow(); + return; + } + + if (!currentBasket()->isLoaded()) { + showPassiveLoading(currentBasket()); + currentBasket()->load(); + } + currentBasket()->insertImage(pixmap); + + if (m_colorPickWasShown) + showMainWindow(); + + if (Settings::usePassivePopup()) + showPassiveDropped(i18n("Grabbed screen zone to basket <i>%1</i>")); +} + +Basket* BNPView::basketForFolderName(const QString &/*folderName*/) +{ +/* QPtrList<Basket> basketsList = listBaskets(); + Basket *basket; + for (basket = basketsList.first(); basket; basket = basketsList.next()) + if (basket->folderName() == folderName) + return basket; +*/ + return 0; +} + +void BNPView::setFiltering(bool filtering) +{ + m_actShowFilter->setChecked(filtering); + m_actResetFilter->setEnabled(filtering); +} + +void BNPView::undo() +{ + // TODO +} + +void BNPView::redo() +{ + // TODO +} + +void BNPView::pasteToBasket(int /*index*/, QClipboard::Mode /*mode*/) +{ + //TODO: REMOVE! + //basketAt(index)->pasteNote(mode); +} + +void BNPView::propBasket() +{ + BasketPropertiesDialog dialog(currentBasket(), this); + dialog.exec(); +} + +void BNPView::delBasket() +{ +// DecoratedBasket *decoBasket = currentDecoratedBasket(); + Basket *basket = currentBasket(); + +#if 0 + KDialogBase *dialog = new KDialogBase(this, /*name=*/0, /*modal=*/true, /*caption=*/i18n("Delete Basket"), + KDialogBase::User1 | KDialogBase::User2 | KDialogBase::No, KDialogBase::User1, + /*separator=*/false, + /*user1=*/KGuiItem(i18n("Delete Only that Basket")/*, icon=""*/), + /*user2=*/KGuiItem(i18n("Delete Note & Children")/*, icon=""*/) ); + QStringList basketsList; + basketsList.append("Basket 1"); + basketsList.append(" Basket 2"); + basketsList.append(" Basket 3"); + basketsList.append(" Basket 4"); + KMessageBox::createKMessageBox( + dialog, QMessageBox::Information, + i18n("<qt>Do you really want to remove the basket <b>%1</b> and its contents?</qt>") + .arg(Tools::textToHTMLWithoutP(basket->basketName())), + basketsList, /*ask=*/"", /*checkboxReturn=*/0, /*options=*/KMessageBox::Notify/*, const QString &details=QString::null*/); +#endif + + int really = KMessageBox::questionYesNo( this, + i18n("<qt>Do you really want to remove the basket <b>%1</b> and its contents?</qt>") + .arg(Tools::textToHTMLWithoutP(basket->basketName())), + i18n("Remove Basket") +#if KDE_IS_VERSION( 3, 2, 90 ) // KDE 3.3.x + , KGuiItem(i18n("&Remove Basket"), "editdelete"), KStdGuiItem::cancel()); +#else + ); +#endif + + if (really == KMessageBox::No) + return; + + QStringList basketsList = listViewItemForBasket(basket)->childNamesTree(); + if (basketsList.count() > 0) { + int deleteChilds = KMessageBox::questionYesNoList( this, + i18n("<qt><b>%1</b> have the following children baskets.<br>Do you want to remove them too?</qt>") + .arg(Tools::textToHTMLWithoutP(basket->basketName())), + basketsList, + i18n("Remove Children Baskets") +#if KDE_IS_VERSION( 3, 2, 90 ) // KDE 3.3.x + , KGuiItem(i18n("&Remove Children Baskets"), "editdelete")); +#else + ); +#endif + + if (deleteChilds == KMessageBox::No) + listViewItemForBasket(basket)->moveChildsBaskets(); + } + + doBasketDeletion(basket); + +// basketNumberChanged(); +// rebuildBasketsMenu(); +} + +void BNPView::doBasketDeletion(Basket *basket) +{ + basket->closeEditor(); + + QListViewItem *basketItem = listViewItemForBasket(basket); + QListViewItem *nextOne; + for (QListViewItem *child = basketItem->firstChild(); child; child = nextOne) { + nextOne = child->nextSibling(); + // First delete the child baskets: + doBasketDeletion(((BasketListViewItem*)child)->basket()); + } + // Then, basket have no child anymore, delete it: + DecoratedBasket *decoBasket = basket->decoration(); + basket->deleteFiles(); + removeBasket(basket); + // Remove the action to avoir keyboard-shortcut clashes: + delete basket->m_action; // FIXME: It's quick&dirty. In the future, the Basket should be deleted, and then the KAction deleted in the Basket destructor. + delete decoBasket; +// delete basket; +} + +void BNPView::password() +{ +#ifdef HAVE_LIBGPGME + PasswordDlg dlg(kapp->activeWindow(), "Password"); + Basket *cur = currentBasket(); + + dlg.setType(cur->encryptionType()); + dlg.setKey(cur->encryptionKey()); + if(dlg.exec()) { + cur->setProtection(dlg.type(), dlg.key()); + if (cur->encryptionType() != Basket::NoEncryption) + cur->lock(); + } +#endif +} + +void BNPView::lockBasket() +{ +#ifdef HAVE_LIBGPGME + Basket *cur = currentBasket(); + + cur->lock(); +#endif +} + +void BNPView::saveAsArchive() +{ + Basket *basket = currentBasket(); + + QDir dir; + + KConfig *config = KGlobal::config(); + config->setGroup("Basket Archive"); + QString folder = config->readEntry("lastFolder", QDir::homeDirPath()) + "/"; + QString url = folder + QString(basket->basketName()).replace("/", "_") + ".baskets"; + + QString filter = "*.baskets|" + i18n("Basket Archives") + "\n*|" + i18n("All Files"); + QString destination = url; + for (bool askAgain = true; askAgain; ) { + destination = KFileDialog::getSaveFileName(destination, filter, this, i18n("Save as Basket Archive")); + if (destination.isEmpty()) // User canceled + return; + if (dir.exists(destination)) { + int result = KMessageBox::questionYesNoCancel( + this, + "<qt>" + i18n("The file <b>%1</b> already exists. Do you really want to override it?") + .arg(KURL(destination).fileName()), + i18n("Override File?"), + KGuiItem(i18n("&Override"), "filesave") + ); + if (result == KMessageBox::Cancel) + return; + else if (result == KMessageBox::Yes) + askAgain = false; + } else + askAgain = false; + } + bool withSubBaskets = true;//KMessageBox::questionYesNo(this, i18n("Do you want to export sub-baskets too?"), i18n("Save as Basket Archive")) == KMessageBox::Yes; + + config->writeEntry("lastFolder", KURL(destination).directory()); + config->sync(); + + Archive::save(basket, withSubBaskets, destination); +} + +QString BNPView::s_fileToOpen = ""; + +void BNPView::delayedOpenArchive() +{ + Archive::open(s_fileToOpen); +} + +void BNPView::openArchive() +{ + QString filter = "*.baskets|" + i18n("Basket Archives") + "\n*|" + i18n("All Files"); + QString path = KFileDialog::getOpenFileName(QString::null, filter, this, i18n("Open Basket Archive")); + if (!path.isEmpty()) // User has not canceled + Archive::open(path); +} + + +void BNPView::activatedTagShortcut() +{ + Tag *tag = Tag::tagForKAction((KAction*)sender()); + currentBasket()->activatedTagShortcut(tag); +} + +void BNPView::slotBasketNumberChanged(int number) +{ + m_actPreviousBasket->setEnabled(number > 1); + m_actNextBasket ->setEnabled(number > 1); +} + +void BNPView::slotBasketChanged() +{ + m_actFoldBasket->setEnabled(canFold()); + m_actExpandBasket->setEnabled(canExpand()); + setFiltering(currentBasket() && currentBasket()->decoration()->filterData().isFiltering); +} + +void BNPView::currentBasketChanged() +{ +} + +void BNPView::isLockedChanged() +{ + bool isLocked = currentBasket()->isLocked(); + + setLockStatus(isLocked); + +// m_actLockBasket->setChecked(isLocked); + m_actPropBasket->setEnabled(!isLocked); + m_actDelBasket ->setEnabled(!isLocked); + updateNotesActions(); +} + +void BNPView::askNewBasket() +{ + askNewBasket(0, 0); +} + +void BNPView::askNewBasket(Basket *parent, Basket *pickProperties) +{ + NewBasketDefaultProperties properties; + if (pickProperties) { + properties.icon = pickProperties->icon(); + properties.backgroundImage = pickProperties->backgroundImageName(); + properties.backgroundColor = pickProperties->backgroundColorSetting(); + properties.textColor = pickProperties->textColorSetting(); + properties.freeLayout = pickProperties->isFreeLayout(); + properties.columnCount = pickProperties->columnsCount(); + } + + NewBasketDialog(parent, properties, this).exec(); +} + +void BNPView::askNewSubBasket() +{ + askNewBasket( /*parent=*/currentBasket(), /*pickPropertiesOf=*/currentBasket() ); +} + +void BNPView::askNewSiblingBasket() +{ + askNewBasket( /*parent=*/parentBasketOf(currentBasket()), /*pickPropertiesOf=*/currentBasket() ); +} + +void BNPView::globalPasteInCurrentBasket() +{ + currentBasket()->setInsertPopupMenu(); + pasteInCurrentBasket(); + currentBasket()->cancelInsertPopupMenu(); +} + +void BNPView::pasteInCurrentBasket() +{ + currentBasket()->pasteNote(); + + if (Settings::usePassivePopup()) + showPassiveDropped(i18n("Clipboard content pasted to basket <i>%1</i>")); +} + +void BNPView::pasteSelInCurrentBasket() +{ + currentBasket()->pasteNote(QClipboard::Selection); + + if (Settings::usePassivePopup()) + showPassiveDropped(i18n("Selection pasted to basket <i>%1</i>")); +} + +void BNPView::showPassiveDropped(const QString &title) +{ + if ( ! currentBasket()->isLocked() ) { + // TODO: Keep basket, so that we show the message only if something was added to a NOT visible basket + m_passiveDroppedTitle = title; + m_passiveDroppedSelection = currentBasket()->selectedNotes(); + QTimer::singleShot( c_delayTooltipTime, this, SLOT(showPassiveDroppedDelayed()) ); + // DELAY IT BELOW: + } else + showPassiveImpossible(i18n("No note was added.")); +} + +void BNPView::showPassiveDroppedDelayed() +{ + if (isMainWindowActive() || m_passiveDroppedSelection == 0) + return; + + QString title = m_passiveDroppedTitle; + + delete m_passivePopup; // Delete previous one (if exists): it will then hide it (only one at a time) + m_passivePopup = new KPassivePopup(Settings::useSystray() ? (QWidget*)Global::systemTray : this); + QPixmap contentsPixmap = NoteDrag::feedbackPixmap(m_passiveDroppedSelection); + QMimeSourceFactory::defaultFactory()->setPixmap("_passivepopup_image_", contentsPixmap); + m_passivePopup->setView( + title.arg(Tools::textToHTMLWithoutP(currentBasket()->basketName())), + (contentsPixmap.isNull() ? "" : "<img src=\"_passivepopup_image_\">"), + kapp->iconLoader()->loadIcon(currentBasket()->icon(), KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true)); + m_passivePopup->show(); +} + +void BNPView::showPassiveImpossible(const QString &message) +{ + delete m_passivePopup; // Delete previous one (if exists): it will then hide it (only one at a time) + m_passivePopup = new KPassivePopup(Settings::useSystray() ? (QWidget*)Global::systemTray : (QWidget*)this); + m_passivePopup->setView( + QString("<font color=red>%1</font>") + .arg(i18n("Basket <i>%1</i> is locked")) + .arg(Tools::textToHTMLWithoutP(currentBasket()->basketName())), + message, + kapp->iconLoader()->loadIcon(currentBasket()->icon(), KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true)); + m_passivePopup->show(); +} + +void BNPView::showPassiveContentForced() +{ + showPassiveContent(/*forceShow=*/true); +} + +void BNPView::showPassiveContent(bool forceShow/* = false*/) +{ + if (!forceShow && isMainWindowActive()) + return; + + // FIXME: Duplicate code (2 times) + QString message; + + delete m_passivePopup; // Delete previous one (if exists): it will then hide it (only one at a time) + m_passivePopup = new KPassivePopup(Settings::useSystray() ? (QWidget*)Global::systemTray : (QWidget*)this); + m_passivePopup->setView( + "<qt>" + kapp->makeStdCaption( currentBasket()->isLocked() + ? QString("%1 <font color=gray30>%2</font>") + .arg(Tools::textToHTMLWithoutP(currentBasket()->basketName()), i18n("(Locked)")) + : Tools::textToHTMLWithoutP(currentBasket()->basketName()) ), + message, + kapp->iconLoader()->loadIcon(currentBasket()->icon(), KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true)); + m_passivePopup->show(); +} + +void BNPView::showPassiveLoading(Basket *basket) +{ + if (isMainWindowActive()) + return; + + delete m_passivePopup; // Delete previous one (if exists): it will then hide it (only one at a time) + m_passivePopup = new KPassivePopup(Settings::useSystray() ? (QWidget*)Global::systemTray : (QWidget*)this); + m_passivePopup->setView( + Tools::textToHTMLWithoutP(basket->basketName()), + i18n("Loading..."), + kapp->iconLoader()->loadIcon(basket->icon(), KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true)); + m_passivePopup->show(); +} + +void BNPView::addNoteText() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Text); } +void BNPView::addNoteHtml() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Html); } +void BNPView::addNoteImage() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Image); } +void BNPView::addNoteLink() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Link); } +void BNPView::addNoteColor() { showMainWindow(); currentBasket()->insertEmptyNote(NoteType::Color); } + +void BNPView::aboutToHideNewBasketPopup() +{ + QTimer::singleShot(0, this, SLOT(cancelNewBasketPopup())); +} + +void BNPView::cancelNewBasketPopup() +{ + m_newBasketPopup = false; +} + +void BNPView::setNewBasketPopup() +{ + m_newBasketPopup = true; +} + +void BNPView::setCaption(QString s) +{ + emit setWindowCaption(s); +} + +void BNPView::updateStatusBarHint() +{ + m_statusbar->updateStatusBarHint(); +} + +void BNPView::setSelectionStatus(QString s) +{ + m_statusbar->setSelectionStatus(s); +} + +void BNPView::setLockStatus(bool isLocked) +{ + m_statusbar->setLockStatus(isLocked); +} + +void BNPView::postStatusbarMessage(const QString& msg) +{ + m_statusbar->postStatusbarMessage(msg); +} + +void BNPView::setStatusBarHint(const QString &hint) +{ + m_statusbar->setStatusBarHint(hint); +} + +void BNPView::setUnsavedStatus(bool isUnsaved) +{ + m_statusbar->setUnsavedStatus(isUnsaved); +} + +void BNPView::setActive(bool active) +{ +// std::cout << "Main Window Position: setActive(" << (active ? "true" : "false") << ")" << std::endl; + KMainWindow* win = Global::mainWindow(); + if(!win) + return; + +#if KDE_IS_VERSION( 3, 2, 90 ) // KDE 3.3.x + if (active) { + kapp->updateUserTimestamp(); // If "activate on mouse hovering systray", or "on drag throught systray" + Global::systemTray->setActive(); // FIXME: add this in the places it need + } else + Global::systemTray->setInactive(); +#elif KDE_IS_VERSION( 3, 1, 90 ) // KDE 3.2.x + // Code from Kopete (that seem to work, in waiting KSystemTray make puplic the toggleSHown) : + if (active) { + win->show(); + //raise() and show() should normaly deIconify the window. but it doesn't do here due + // to a bug in Qt or in KDE (qt3.1.x or KDE 3.1.x) then, i have to call KWin's method + if (win->isMinimized()) + KWin::deIconifyWindow(winId()); + + if ( ! KWin::windowInfo(winId(), NET::WMDesktop).onAllDesktops() ) + KWin::setOnDesktop(winId(), KWin::currentDesktop()); + win->raise(); + // Code from me: expected and correct behavviour: + kapp->updateUserTimestamp(); // If "activate on mouse hovering systray", or "on drag throught systray" + KWin::activateWindow(win->winId()); + } else + win->hide(); +#else // KDE 3.1.x and lower + if (win->active) { + if (win->isMinimized()) + win->hide(); // If minimized, show() doesn't work ! + win->show(); // Show it + // showNormal(); // If it was minimized + win->raise(); // Raise it on top + win->setActiveWindow(); // And set it the active window + } else + win->hide(); +#endif +} + +void BNPView::hideOnEscape() +{ + if (Settings::useSystray()) + setActive(false); +} + +bool BNPView::isPart() +{ + return (strcmp(name(), "BNPViewPart") == 0); +} + +bool BNPView::isMainWindowActive() +{ + KMainWindow* main = Global::mainWindow(); + if (main && main->isActiveWindow()) + return true; + return false; +} + +void BNPView::newBasket() +{ + askNewBasket(); +} + +void BNPView::handleCommandLine() +{ + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + + /* Custom data folder */ + QCString customDataFolder = args->getOption("data-folder"); + if (customDataFolder != 0 && !customDataFolder.isEmpty()) + { + Global::setCustomSavesFolder(customDataFolder); + } + /* Debug window */ + if (args->isSet("debug")) { + new DebugWindow(); + Global::debugWindow->show(); + } + + /* Crash Handler to Mail Developers when Crashing: */ +#ifndef BASKET_USE_DRKONQI + if (!args->isSet("use-drkonquy")) + KCrash::setCrashHandler(Crash::crashHandler); +#endif +} + +/** Scenario of "Hide main window to system tray icon when mouse move out of the window" : + * - At enterEvent() we stop m_tryHideTimer + * - After that and before next, we are SURE cursor is hovering window + * - At leaveEvent() we restart m_tryHideTimer + * - Every 'x' ms, timeoutTryHide() seek if cursor hover a widget of the application or not + * - If yes, we musn't hide the window + * - But if not, we start m_hideTimer to hide main window after a configured elapsed time + * - timeoutTryHide() continue to be called and if cursor move again to one widget of the app, m_hideTimer is stopped + * - If after the configured time cursor hasn't go back to a widget of the application, timeoutHide() is called + * - It then hide the main window to systray icon + * - When the user will show it, enterEvent() will be called the first time he enter mouse to it + * - ... + */ + +/** Why do as this ? Problems with the use of only enterEvent() and leaveEvent() : + * - Resize window or hover titlebar isn't possible : leave/enterEvent + * are + * > Use the grip or Alt+rightDND to resize window + * > Use Alt+DND to move window + * - Each menu trigger the leavEvent + */ + +void BNPView::enterEvent(QEvent*) +{ + if(m_tryHideTimer) + m_tryHideTimer->stop(); + if(m_hideTimer) + m_hideTimer->stop(); +} + +void BNPView::leaveEvent(QEvent*) +{ + if (Settings::useSystray() && Settings::hideOnMouseOut() && m_tryHideTimer) + m_tryHideTimer->start(50); +} + +void BNPView::timeoutTryHide() +{ + // If a menu is displayed, do nothing for the moment + if (kapp->activePopupWidget() != 0L) + return; + + if (kapp->widgetAt(QCursor::pos()) != 0L) + m_hideTimer->stop(); + else if ( ! m_hideTimer->isActive() ) // Start only one time + m_hideTimer->start(Settings::timeToHideOnMouseOut() * 100, true); + + // If a sub-dialog is oppened, we musn't hide the main window: + if (kapp->activeWindow() != 0L && kapp->activeWindow() != Global::mainWindow()) + m_hideTimer->stop(); +} + +void BNPView::timeoutHide() +{ + // We check that because the setting can have been set to off + if (Settings::useSystray() && Settings::hideOnMouseOut()) + setActive(false); + m_tryHideTimer->stop(); +} + +void BNPView::changedSelectedNotes() +{ +// tabChanged(0); // FIXME: NOT OPTIMIZED +} + +/*void BNPView::areSelectedNotesCheckedChanged(bool checked) +{ + m_actCheckNotes->setChecked(checked && currentBasket()->showCheckBoxes()); +}*/ + +void BNPView::enableActions() +{ + Basket *basket = currentBasket(); + if(!basket) + return; + if(m_actLockBasket) + m_actLockBasket->setEnabled(!basket->isLocked() && basket->isEncrypted()); + if(m_actPassBasket) + m_actPassBasket->setEnabled(!basket->isLocked()); + m_actPropBasket->setEnabled(!basket->isLocked()); + m_actDelBasket->setEnabled(!basket->isLocked()); + m_actExportToHtml->setEnabled(!basket->isLocked()); + m_actShowFilter->setEnabled(!basket->isLocked()); + m_actFilterAllBaskets->setEnabled(!basket->isLocked()); + m_actResetFilter->setEnabled(!basket->isLocked()); + basket->decoration()->filterBar()->setEnabled(!basket->isLocked()); +} + +void BNPView::showMainWindow() +{ + KMainWindow *win = Global::mainWindow(); + + if (win) + { + win->show(); + } + setActive(true); + emit showPart(); +} + +void BNPView::populateTagsMenu() +{ + KPopupMenu *menu = (KPopupMenu*)(popupMenu("tags")); + if (menu == 0 || currentBasket() == 0) // TODO: Display a messagebox. [menu is 0, surely because on first launch, the XMLGUI does not work!] + return; + menu->clear(); + + Note *referenceNote; + if (currentBasket()->focusedNote() && currentBasket()->focusedNote()->isSelected()) + referenceNote = currentBasket()->focusedNote(); + else + referenceNote = currentBasket()->firstSelected(); + + populateTagsMenu(*menu, referenceNote); + + m_lastOpenedTagsMenu = menu; +// connect( menu, SIGNAL(aboutToHide()), this, SLOT(disconnectTagsMenu()) ); +} + +void BNPView::populateTagsMenu(KPopupMenu &menu, Note *referenceNote) +{ + if (currentBasket() == 0) + return; + + currentBasket()->m_tagPopupNote = referenceNote; + bool enable = currentBasket()->countSelecteds() > 0; + + QValueList<Tag*>::iterator it; + Tag *currentTag; + State *currentState; + int i = 10; + for (it = Tag::all.begin(); it != Tag::all.end(); ++it) { + // Current tag and first state of it: + currentTag = *it; + currentState = currentTag->states().first(); + QKeySequence sequence; + if (!currentTag->shortcut().isNull()) + sequence = currentTag->shortcut().operator QKeySequence(); + menu.insertItem(StateMenuItem::checkBoxIconSet( + (referenceNote ? referenceNote->hasTag(currentTag) : false), + menu.colorGroup()), + new StateMenuItem(currentState, sequence, true), + i + ); + if (!currentTag->shortcut().isNull()) + menu.setAccel(sequence, i); + menu.setItemEnabled(i, enable); + ++i; + } + + menu.insertSeparator(); +// menu.insertItem( /*SmallIconSet("editdelete"),*/ "&Assign new Tag...", 1 ); + //id = menu.insertItem( SmallIconSet("editdelete"), "&Remove All", -2 ); + //if (referenceNote->states().isEmpty()) + // menu.setItemEnabled(id, false); +// menu.insertItem( SmallIconSet("configure"), "&Customize...", 3 ); + menu.insertItem( new IndentedMenuItem(i18n("&Assign new Tag...")), 1 ); + menu.insertItem( new IndentedMenuItem(i18n("&Remove All"), "editdelete"), 2 ); + menu.insertItem( new IndentedMenuItem(i18n("&Customize..."), "configure"), 3 ); + + menu.setItemEnabled(1, enable); + if (!currentBasket()->selectedNotesHaveTags()) + menu.setItemEnabled(2, false); + + connect( &menu, SIGNAL(activated(int)), currentBasket(), SLOT(toggledTagInMenu(int)) ); + connect( &menu, SIGNAL(aboutToHide()), currentBasket(), SLOT(unlockHovering()) ); + connect( &menu, SIGNAL(aboutToHide()), currentBasket(), SLOT(disableNextClick()) ); +} + +void BNPView::connectTagsMenu() +{ + connect( popupMenu("tags"), SIGNAL(aboutToShow()), this, SLOT(populateTagsMenu()) ); + connect( popupMenu("tags"), SIGNAL(aboutToHide()), this, SLOT(disconnectTagsMenu()) ); +} + +/* + * The Tags menu is ONLY created once the BasKet KPart is first shown. + * So we can use this menu only from then? + * When the KPart is changed in Kontact, and then the BasKet KPart is shown again, + * Kontact created a NEW Tags menu. So we should connect again. + * But when Kontact main window is hidden and then re-shown, the menu does not change. + * So we disconnect at hide event to ensure only one connection: the next show event will not connects another time. + */ + +void BNPView::showEvent(QShowEvent*) +{ + if (isPart()) + QTimer::singleShot( 0, this, SLOT(connectTagsMenu()) ); + + if (m_firstShow) { + m_firstShow = false; + onFirstShow(); + } + if (isPart()/*TODO: && !LikeBack::enabledBar()*/) { + Global::likeBack->enableBar(); + } +} + +void BNPView::hideEvent(QHideEvent*) +{ + if (isPart()) { + disconnect( popupMenu("tags"), SIGNAL(aboutToShow()), this, SLOT(populateTagsMenu()) ); + disconnect( popupMenu("tags"), SIGNAL(aboutToHide()), this, SLOT(disconnectTagsMenu()) ); + } + + if (isPart()) + Global::likeBack->disableBar(); +} + +void BNPView::disconnectTagsMenu() +{ + QTimer::singleShot( 0, this, SLOT(disconnectTagsMenuDelayed()) ); +} + +void BNPView::disconnectTagsMenuDelayed() +{ + disconnect( m_lastOpenedTagsMenu, SIGNAL(activated(int)), currentBasket(), SLOT(toggledTagInMenu(int)) ); + disconnect( m_lastOpenedTagsMenu, SIGNAL(aboutToHide()), currentBasket(), SLOT(unlockHovering()) ); + disconnect( m_lastOpenedTagsMenu, SIGNAL(aboutToHide()), currentBasket(), SLOT(disableNextClick()) ); +} + +void BNPView::showGlobalShortcutsSettingsDialog() +{ + KKeyDialog::configure(Global::globalAccel); + //.setCaption(..) + Global::globalAccel->writeSettings(); +} + +#include "bnpview.moc" diff --git a/src/bnpview.h b/src/bnpview.h new file mode 100644 index 0000000..b7e99ef --- /dev/null +++ b/src/bnpview.h @@ -0,0 +1,354 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef BNPVIEW_H +#define BNPVIEW_H + +#include <klistview.h> +#include <kxmlguiclient.h> +#include <qtimer.h> +#include <qclipboard.h> +#include <qsplitter.h> +#include <qlistview.h> +#include <dcopref.h> +#include "global.h" +#include "basketdcopiface.h" + + /// NEW: + +class QWidgetStack; +class QDomDocument; +class QDomElement; +class KToggleAction; +class KPassivePopup; +class QPopupMenu; +class KPopupMenu; +class KTar; + +class DesktopColorPicker; +class RegionGrabber; + +class Basket; +class DecoratedBasket; +class BasketListViewItem; +class NoteSelection; +class BasketStatusBar; +class Tag; +class State; +class Note; + +class BNPView : public QSplitter, virtual public BasketDcopInterface +{ + Q_OBJECT + public: + /// CONSTRUCTOR AND DESTRUCTOR: + BNPView(QWidget *parent, const char *name, KXMLGUIClient *aGUIClient, + KActionCollection *actionCollection, BasketStatusBar *bar); + ~BNPView(); + /// MANAGE CONFIGURATION EVENTS!: + void setTreePlacement(bool onLeft); + void relayoutAllBaskets(); + void recomputeAllStyles(); + void removedStates(const QValueList<State*> &deletedStates); + void linkLookChanged(); + void filterPlacementChanged(bool onTop); + /// MANAGE BASKETS: + BasketListViewItem* listViewItemForBasket(Basket *basket); + Basket* currentBasket(); + Basket* parentBasketOf(Basket *basket); + void setCurrentBasket(Basket *basket); + void removeBasket(Basket *basket); + /// For NewBasketDialog (and later some other classes): + QListViewItem* firstListViewItem(); + /// + BasketListViewItem* lastListViewItem(); + int basketCount(QListViewItem *parent = 0); + bool canFold(); + bool canExpand(); + void enableActions(); + + private: + QDomElement basketElement(QListViewItem *item, QDomDocument &document, QDomElement &parentElement); + public slots: + void countsChanged(Basket *basket); + void notesStateChanged(); + bool convertTexts(); + + void updateBasketListViewItem(Basket *basket); + void save(); + void save(QListViewItem *firstItem, QDomDocument &document, QDomElement &parentElement); + void saveSubHierarchy(QListViewItem *item, QDomDocument &document, QDomElement &parentElement, bool recursive); + void load(); + void load(KListView *listView, QListViewItem *item, const QDomElement &baskets); + void loadNewBasket(const QString &folderName, const QDomElement &properties, Basket *parent); + void goToPreviousBasket(); + void goToNextBasket(); + void foldBasket(); + void expandBasket(); + void closeAllEditors(); + /// + void toggleFilterAllBaskets(bool doFilter); + void newFilter(); + void newFilterFromFilterBar(); + bool isFilteringAllBaskets(); + // From main window + void importKNotes(); + void importKJots(); + void importKnowIt(); + void importTuxCards(); + void importStickyNotes(); + void importTomboy(); + void importTextFile(); + void backupRestore(); + + /** Note */ + void activatedTagShortcut(); + void exportToHTML(); + void editNote(); + void cutNote(); + void copyNote(); + void delNote(); + void openNote(); + void openNoteWith(); + void saveNoteAs(); + void noteGroup(); + void noteUngroup(); + void moveOnTop(); + void moveOnBottom(); + void moveNoteUp(); + void moveNoteDown(); + void slotSelectAll(); + void slotUnselectAll(); + void slotInvertSelection(); + void slotResetFilter(); + + void slotColorFromScreen(bool global = false); + void slotColorFromScreenGlobal(); + void colorPicked(const QColor &color); + void colorPickingCanceled(); + void slotConvertTexts(); + + /** Global shortcuts */ + void addNoteText(); + void addNoteHtml(); + void addNoteImage(); + void addNoteLink(); + void addNoteColor(); + /** Passive Popups for Global Actions */ + void showPassiveDropped(const QString &title); + void showPassiveDroppedDelayed(); // Do showPassiveDropped(), but delayed + void showPassiveContent(bool forceShow = false); + void showPassiveContentForced(); + void showPassiveImpossible(const QString &message); + void showPassiveLoading(Basket *basket); + // For GUI : + void setFiltering(bool filtering); + /** Edit */ + void undo(); + void redo(); + void globalPasteInCurrentBasket(); + void pasteInCurrentBasket(); + void pasteSelInCurrentBasket(); + void pasteToBasket(int index, QClipboard::Mode mode = QClipboard::Clipboard); + void showHideFilterBar(bool show, bool switchFocus = true); + /** Insert **/ + void insertEmpty(int type); + void insertWizard(int type); + void grabScreenshot(bool global = false); + void grabScreenshotGlobal(); + void screenshotGrabbed(const QPixmap &pixmap); + /** Basket */ + void askNewBasket(); + void askNewBasket(Basket *parent, Basket *pickProperties); + void askNewSubBasket(); + void askNewSiblingBasket(); + void aboutToHideNewBasketPopup(); + void setNewBasketPopup(); + void cancelNewBasketPopup(); + void propBasket(); + void delBasket(); + void doBasketDeletion(Basket *basket); + void password(); + void saveAsArchive(); + void openArchive(); + void delayedOpenArchive(); + void lockBasket(); + void hideOnEscape(); + + void changedSelectedNotes(); + void timeoutTryHide(); + void timeoutHide(); + + public: + static QString s_fileToOpen; + + public slots: + void addWelcomeBaskets(); + private slots: + void updateNotesActions(); + void slotBasketNumberChanged(int number); + void slotBasketChanged(); + void currentBasketChanged(); + void isLockedChanged(); + void lateInit(); + void onFirstShow(); + void showGlobalShortcutsSettingsDialog(); + + public: + KAction *m_actEditNote; + KAction *m_actOpenNote; + KAction *m_actPaste; + KAction *m_actGrabScreenshot; + KAction *m_actColorPicker; + KAction *m_actLockBasket; + KAction *m_actPassBasket; + KAction *actNewBasket; + KAction *actNewSubBasket; + KAction *actNewSiblingBasket; + KAction *m_actHideWindow; + KAction *m_actExportToHtml; + KAction *m_actPropBasket; + KAction *m_actDelBasket; + KToggleAction *m_actFilterAllBaskets; + + private: + // Basket actions: + KAction *m_actSaveAsArchive; + KAction *m_actOpenArchive; + // Notes actions : + KAction *m_actOpenNoteWith; + KAction *m_actSaveNoteAs; + KAction *m_actGroup; + KAction *m_actUngroup; + KAction *m_actMoveOnTop; + KAction *m_actMoveNoteUp; + KAction *m_actMoveNoteDown; + KAction *m_actMoveOnBottom; + // Edit actions : + KAction *m_actUndo; + KAction *m_actRedo; + KAction *m_actCutNote; + KAction *m_actCopyNote; + KAction *m_actDelNote; + KAction *m_actSelectAll; + KAction *m_actUnselectAll; + KAction *m_actInvertSelection; + // Insert actions : +// KAction *m_actInsertText; + KAction *m_actInsertHtml; + KAction *m_actInsertLink; + KAction *m_actInsertImage; + KAction *m_actInsertColor; + KAction *m_actImportKMenu; + KAction *m_actInsertLauncher; + KAction *m_actImportIcon; + KAction *m_actLoadFile; + QPtrList<KAction> m_insertActions; + // Basket actions : + KToggleAction *m_actShowFilter; + KAction *m_actResetFilter; + // Go actions : + KAction *m_actPreviousBasket; + KAction *m_actNextBasket; + KAction *m_actFoldBasket; + KAction *m_actExpandBasket; +// KAction *m_convertTexts; // FOR_BETA_PURPOSE + KAction *actConfigGlobalShortcuts; + + void setupActions(); + void setupGlobalShortcuts(); + DecoratedBasket* currentDecoratedBasket(); + + public: + Basket* loadBasket(const QString &folderName); // Public only for class Archive + BasketListViewItem* appendBasket(Basket *basket, QListViewItem *parentItem); // Public only for class Archive + + Basket* basketForFolderName(const QString &folderName); + QPopupMenu* popupMenu(const QString &menuName); + bool isPart(); + bool isMainWindowActive(); + void showMainWindow(); + + // dcop calls + virtual void newBasket(); + virtual void handleCommandLine(); + + public slots: + void setCaption(QString s); + void updateStatusBarHint(); + void setSelectionStatus(QString s); + void setLockStatus(bool isLocked); + void postStatusbarMessage(const QString&); + void setStatusBarHint(const QString&); + void setUnsavedStatus(bool isUnsaved); + void setActive(bool active = true); + KActionCollection *actionCollection() { return m_actionCollection; }; + + void populateTagsMenu(); + void populateTagsMenu(KPopupMenu &menu, Note *referenceNote); + void connectTagsMenu(); + void disconnectTagsMenu(); + void disconnectTagsMenuDelayed(); + protected: + void showEvent(QShowEvent*); + void hideEvent(QHideEvent*); + private: + KPopupMenu *m_lastOpenedTagsMenu; + + private slots: + void slotPressed(QListViewItem *item, const QPoint &/*pos*/ = QPoint(), int /*column*/ = 0); + void needSave(QListViewItem*); + void slotContextMenu(KListView *listView, QListViewItem *item, const QPoint &pos); + void slotMouseButtonPressed(int button, QListViewItem *item, const QPoint &pos, int column); + void slotShowProperties(QListViewItem *item, const QPoint&, int); + void initialize(); + + signals: + void basketNumberChanged(int number); + void basketChanged(); + void setWindowCaption(const QString &s); + void showPart(); + + protected: + void enterEvent(QEvent*); + void leaveEvent(QEvent*); + + private: + KListView *m_tree; + QWidgetStack *m_stack; + bool m_loading; + bool m_newBasketPopup; + bool m_firstShow; + DesktopColorPicker *m_colorPicker; + bool m_colorPickWasShown; + bool m_colorPickWasGlobal; + RegionGrabber *m_regionGrabber; + QString m_passiveDroppedTitle; + NoteSelection *m_passiveDroppedSelection; + KPassivePopup *m_passivePopup; + static const int c_delayTooltipTime; + KActionCollection *m_actionCollection; + KXMLGUIClient *m_guiClient; + BasketStatusBar *m_statusbar; + QTimer *m_tryHideTimer; + QTimer *m_hideTimer; +}; + +#endif // BNPVIEW_H diff --git a/src/clickablelabel.cpp b/src/clickablelabel.cpp new file mode 100644 index 0000000..11a5ce9 --- /dev/null +++ b/src/clickablelabel.cpp @@ -0,0 +1,29 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "clickablelabel.h" + +void ClickableLabel::mousePressEvent(QMouseEvent *event) +{ + if (event->button() & Qt::LeftButton) + emit clicked(); +} + +#include "clickablelabel.moc" diff --git a/src/clickablelabel.h b/src/clickablelabel.h new file mode 100644 index 0000000..0198974 --- /dev/null +++ b/src/clickablelabel.h @@ -0,0 +1,43 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef CLICKABLELABEL_H +#define CLICKABLELABEL_H + +#include <qlabel.h> + +/** This class is a QLabel that can emit a clicked() signal when clicked ! + * @author S�astien Laot + */ +class ClickableLabel : public QLabel +{ + Q_OBJECT + public: + /** Construtor, initializer and destructor */ + ClickableLabel(QWidget *parent = 0, const char *name = 0) + : QLabel(parent, name) {} + ~ClickableLabel() {} + signals: + void clicked(); + protected: + virtual void mousePressEvent(QMouseEvent *event); +}; + +#endif // CLICKABLELABEL_H diff --git a/src/colorpicker.cpp b/src/colorpicker.cpp new file mode 100644 index 0000000..37049bb --- /dev/null +++ b/src/colorpicker.cpp @@ -0,0 +1,96 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "colorpicker.h" +#include "qtimer.h" +#include <kcolordialog.h> + +/// /// + +/** DektopColorPicker */ + +/* From Qt documentation: + * " Note that only visible widgets can grab mouse input. + * If isVisible() returns FALSE for a widget, that widget cannot call grabMouse(). " + * So, we should use an always visible widget to be able to pick a color from screen, + * even by first hidding the main window (user seldomly want to grab a color from BasKet!) + * or use a global shortcut (main window can be hidden when hitting that shortcut). + */ + +DesktopColorPicker::DesktopColorPicker() + : QDesktopWidget() +{ + setName("DesktopColorPicker"); + m_gettingColorFromScreen = false; +} + +DesktopColorPicker::~DesktopColorPicker() +{ +} + +void DesktopColorPicker::pickColor() +{ + m_gettingColorFromScreen = true; +// Global::mainContainer->setActive(false); + QTimer::singleShot( 50, this, SLOT(slotDelayedPick()) ); +} + +/* When firered from basket context menu, and not from menu, grabMouse doesn't work! + * It's perhapse because context menu call slotColorFromScreen() and then + * ungrab the mouse (since menus grab the mouse). + * But why isn't there such bug with normal menus?... + * By calling this method with a QTimer::singleShot, we are sure context menu code is + * finished and we can grab the mouse without loosing the grab: + */ +void DesktopColorPicker::slotDelayedPick() +{ + grabKeyboard(); + grabMouse(crossCursor); +} + +/* Validate the color + */ +void DesktopColorPicker::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_gettingColorFromScreen) { + m_gettingColorFromScreen = false; + releaseMouse(); + releaseKeyboard(); + QColor color = KColorDialog::grabColor(event->globalPos()); + emit pickedColor(color); + } else + QDesktopWidget::mouseReleaseEvent(event); +} + +/* Cancel the mode + */ +void DesktopColorPicker::keyPressEvent(QKeyEvent *event) +{ + if (m_gettingColorFromScreen) + if (event->key() == Qt::Key_Escape) { + m_gettingColorFromScreen = false; + releaseMouse(); + releaseKeyboard(); + emit canceledPick(); + } + QDesktopWidget::keyPressEvent(event); +} + +#include "colorpicker.moc" diff --git a/src/colorpicker.h b/src/colorpicker.h new file mode 100644 index 0000000..1f109fa --- /dev/null +++ b/src/colorpicker.h @@ -0,0 +1,57 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef COLORPICKER_H +#define COLORPICKER_H + +#include <qdesktopwidget.h> + +/** Class to pick a color on the screen + * @author S�astien Laot + */ +class DesktopColorPicker : public QDesktopWidget +{ + Q_OBJECT + public: + /** Construtor, initializer and destructor */ + DesktopColorPicker(); + ~DesktopColorPicker(); + public slots: + /** Begin color picking. + * This function returns immediatly, and pickedColor() is emitted if user has + * choosen a color, and not canceled the process (by pressing Escape). + */ + void pickColor(); + signals: + /** When user picked a color, this signal is emitted. + */ + void pickedColor(const QColor &color); + /** When user cancel a picking (by pressing Escape), this signal is emitted. + */ + void canceledPick(); + protected slots: + void slotDelayedPick(); + protected: + void mouseReleaseEvent(QMouseEvent *event); + void keyPressEvent(QKeyEvent *event); + bool m_gettingColorFromScreen; +}; + +#endif // COLORPICKER_H diff --git a/src/cr128-app-basket.png b/src/cr128-app-basket.png Binary files differnew file mode 100644 index 0000000..a9b207d --- /dev/null +++ b/src/cr128-app-basket.png diff --git a/src/cr16-action-likeback_bug.png b/src/cr16-action-likeback_bug.png Binary files differnew file mode 100755 index 0000000..1edc6c1 --- /dev/null +++ b/src/cr16-action-likeback_bug.png diff --git a/src/cr16-action-likeback_dislike.png b/src/cr16-action-likeback_dislike.png Binary files differnew file mode 100644 index 0000000..fdc9a13 --- /dev/null +++ b/src/cr16-action-likeback_dislike.png diff --git a/src/cr16-action-likeback_feature.png b/src/cr16-action-likeback_feature.png Binary files differnew file mode 100755 index 0000000..8d72794 --- /dev/null +++ b/src/cr16-action-likeback_feature.png diff --git a/src/cr16-action-likeback_like.png b/src/cr16-action-likeback_like.png Binary files differnew file mode 100644 index 0000000..b62b611 --- /dev/null +++ b/src/cr16-action-likeback_like.png diff --git a/src/cr16-app-basket.png b/src/cr16-app-basket.png Binary files differnew file mode 100644 index 0000000..3fc0b3a --- /dev/null +++ b/src/cr16-app-basket.png diff --git a/src/cr22-app-basket.png b/src/cr22-app-basket.png Binary files differnew file mode 100644 index 0000000..6ff816e --- /dev/null +++ b/src/cr22-app-basket.png diff --git a/src/cr32-app-basket.png b/src/cr32-app-basket.png Binary files differnew file mode 100644 index 0000000..1177cb3 --- /dev/null +++ b/src/cr32-app-basket.png diff --git a/src/cr48-app-basket.png b/src/cr48-app-basket.png Binary files differnew file mode 100644 index 0000000..1484fb0 --- /dev/null +++ b/src/cr48-app-basket.png diff --git a/src/cr64-app-basket.png b/src/cr64-app-basket.png Binary files differnew file mode 100644 index 0000000..620808d --- /dev/null +++ b/src/cr64-app-basket.png diff --git a/src/crashhandler.cpp b/src/crashhandler.cpp new file mode 100644 index 0000000..64e745b --- /dev/null +++ b/src/crashhandler.cpp @@ -0,0 +1,226 @@ +// Code from Amarok. + +/*************************************************************************** + * Copyright (C) 2005 Max Howell <[email protected]> * + * * + * 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. * + * * + ***************************************************************************/ + +//#include "amarok.h" +//#include "amarokconfig.h" +#include "crashhandler.h" +//#include "debug.h" +#include "config.h" + +#include <kapplication.h> //invokeMailer() +#include <kaboutdata.h> +#include <kdeversion.h> +#include <klocale.h> +#include <ktempfile.h> + +#include <qfile.h> +#include <qregexp.h> +#include <qtextstream.h> + +#include <cstdio> //popen, fread +#include <iostream> +#include <sys/types.h> //pid_t +#include <sys/wait.h> //waitpid +//#include <taglib/taglib.h> +#include <unistd.h> //write, getpid + + + +//#ifndef TAGLIB_PATCH_VERSION +//// seems to be wheel's style +//#define TAGLIB_PATCH_VERSION 0 +//#endif + + + #if 0 + class CrashHandlerWidget : public KDialog { + public: + CrashHandlerWidget(); + }; + #endif + + static QString + runCommand( const QCString &command ) + { + static const uint SIZE = 40960; //40 KiB + static char stdoutBuf[ SIZE ]; + +// debug() << "Running: " << command << endl; + + FILE *process = ::popen( command, "r" ); + stdoutBuf[ std::fread( static_cast<void*>( stdoutBuf ), sizeof(char), SIZE-1, process ) ] = '\0'; + ::pclose( process ); + + return QString::fromLocal8Bit( stdoutBuf ); + } + + void + Crash::crashHandler( int /*signal*/ ) + { + // we need to fork to be able to get a + // semi-decent bt - I dunno why + const pid_t pid = ::fork(); + + if( pid <= 0 ) + { + // we are the child process (the result of the fork) +// debug() << "amaroK is crashing...\n"; + + QString subject = "[basket-crash] " VERSION " "; + QString body = i18n( + "%1 has crashed! We're sorry about this.\n" + "\n" + "But, all is not lost! You could potentially help us fix the crash. " + "Information describing the crash is below, so just click send, " + "or if you have time, write a brief description of how the crash happened first.\n\n" + "Many thanks." ).arg(kapp->aboutData()->programName()) + "\n\n"; + body += "\n\n\n\n\n\n" + i18n( + "The information below is to help the developers identify the problem, " + "please do not modify it." ) + "\n\n\n\n"; + + + body += "======== DEBUG INFORMATION =======\n" + "Version: " VERSION "\n" + "Build date: " __DATE__ "\n" + "CC version: " __VERSION__ "\n" //assuming we're using GCC + "KDElibs: " KDE_VERSION_STRING "\n" +;// "TagLib: %2.%3.%4\n"; + +/* body = body + .arg( TAGLIB_MAJOR_VERSION ) + .arg( TAGLIB_MINOR_VERSION ) + .arg( TAGLIB_PATCH_VERSION );*/ + + #ifdef NDEBUG + body += "NDEBUG: true"; + #endif + body += "\n"; + + /// obtain the backtrace with gdb + + KTempFile temp; + temp.setAutoDelete( true ); + + const int handle = temp.handle(); + +// QCString gdb_command_string = +// "file amarokapp\n" +// "attach " + QCString().setNum( ::getppid() ) + "\n" +// "bt\n" "echo \\n\n" +// "thread apply all bt\n"; + + const QCString gdb_batch = + "bt\n" + "echo \\n\\n\n" + "bt full\n" + "echo \\n\\n\n" + "echo ==== (gdb) thread apply all bt ====\\n\n" + "thread apply all bt\n"; + + ::write( handle, gdb_batch, gdb_batch.length() ); + ::fsync( handle ); + + // so we can read stderr too + ::dup2( fileno( stdout ), fileno( stderr ) ); + + + QCString gdb; + gdb = "gdb --nw -n --batch -x "; + gdb += temp.name().latin1(); + gdb += " basket "; + gdb += QCString().setNum( ::getppid() ); + + QString bt = runCommand( gdb ); + + /// clean up + bt.remove( "(no debugging symbols found)..." ); + bt.remove( "(no debugging symbols found)\n" ); + bt.replace( QRegExp("\n{2,}"), "\n" ); //clean up multiple \n characters + bt.stripWhiteSpace(); + + /// analyze usefulness + bool useful = true; + const QString fileCommandOutput = runCommand( "file `which basket`" ); + + if( fileCommandOutput.find( "not stripped", false ) == -1 ) + subject += "[___stripped]"; //same length as below + else + subject += "[NOTstripped]"; + + if( !bt.isEmpty() ) { + const int invalidFrames = bt.contains( QRegExp("\n#[0-9]+\\s+0x[0-9A-Fa-f]+ in \\?\\?") ); + const int validFrames = bt.contains( QRegExp("\n#[0-9]+\\s+0x[0-9A-Fa-f]+ in [^?]") ); + const int totalFrames = invalidFrames + validFrames; + + if( totalFrames > 0 ) { + const double validity = double(validFrames) / totalFrames; + subject += QString("[validity: %1]").arg( validity, 0, 'f', 2 ); + if( validity <= 0.5 ) useful = false; + } + subject += QString("[frames: %1]").arg( totalFrames, 3 /*padding*/ ); + + if( bt.find( QRegExp(" at \\w*\\.cpp:\\d+\n") ) >= 0 ) + subject += "[line numbers]"; + } + else + useful = false; + +// subject += QString("[%1]").arg( AmarokConfig::soundSystem().remove( QRegExp("-?engine") ) ); + +// debug() << subject << endl; + + + //TODO -fomit-frame-pointer buggers up the backtrace, so detect it + //TODO -O optimization can rearrange execution and stuff so show a warning for the developer + //TODO pass the CXXFLAGS used with the email + + if( useful ) { + body += "==== file `which basket` ==========\n"; + body += fileCommandOutput + "\n"; + body += "==== (gdb) bt =====================\n"; + body += bt;//+ "\n\n"; +// body += "==== kdBacktrace() ================\n"; +// body += kdBacktrace(); + + //TODO startup notification + kapp->invokeMailer( + /*to*/ "[email protected]", + /*cc*/ QString(), + /*bcc*/ QString(), + /*subject*/ subject, + /*body*/ body, + /*messageFile*/ QString(), + /*attachURLs*/ QStringList(), + /*startup_id*/ "" ); + } + else { + std::cout << "\n" + i18n( "%1 has crashed! We're sorry about this.\n\n" + "But, all is not lost! Perhaps an upgrade is already available " + "which fixes the problem. Please check your distribution's software repository." ) + .arg(kapp->aboutData()->programName()).local8Bit() << std::endl; + } + + //_exit() exits immediately, otherwise this + //function is called repeatedly ad finitum + ::_exit( 255 ); + } + + else { + // we are the process that crashed + + ::alarm( 0 ); + + // wait for child to exit + ::waitpid( pid, NULL, 0 ); + ::_exit( 253 ); + } + } diff --git a/src/crashhandler.h b/src/crashhandler.h new file mode 100644 index 0000000..7163def --- /dev/null +++ b/src/crashhandler.h @@ -0,0 +1,31 @@ +// Code from Amarok. + +/*************************************************************************** + * Copyright (C) 2005 Max Howell <[email protected]> * + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef CRASH_H +#define CRASH_H + +#include <kcrash.h> //for main.cpp + + /** + * @author Max Howell + * @short The amaroK crash-handler + * + * I'm not entirely sure why this had to be inside a class, but it + * wouldn't work otherwise *shrug* + */ + class Crash + { + public: + static void crashHandler( int signal ); + }; + +#endif diff --git a/src/crsc-app-basket.svg b/src/crsc-app-basket.svg new file mode 100644 index 0000000..1bd0c02 --- /dev/null +++ b/src/crsc-app-basket.svg @@ -0,0 +1,1233 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.0" + x="0.0000000" + y="0.0000000" + width="125.95000" + height="125.95000" + id="svg1" + sodipodi:version="0.32" + inkscape:version="0.43+0.44pre3" + sodipodi:docname="crsc-app-basket-small.svg" + sodipodi:docbase="/home/dd/Desktop/basket-icon" + inkscape:export-filename="/home/dd/Desktop/basket-icon/cr16-app-basket.png" + inkscape:export-xdpi="12.86" + inkscape:export-ydpi="12.86"> + <metadata + id="metadata157"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="5.3804605" + inkscape:cx="109.82959" + inkscape:cy="30.277546" + inkscape:window-width="1272" + inkscape:window-height="960" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:current-layer="layer5" /> + <defs + id="defs2"> + <linearGradient + x1="99.597000" + y1="27.749901" + x2="21.819700" + y2="27.749901" + id="defitem0" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#a5a5a5;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop4" /> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop5" /> + </linearGradient> + <linearGradient + x1="11.125700" + y1="40.099499" + x2="104.75200" + y2="40.099499" + id="defitem1" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffdf00;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop7" /> + <stop + style="stop-color:#ff9000;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop8" /> + </linearGradient> + <linearGradient + x1="11.854800" + y1="64.663101" + x2="104.84800" + y2="64.663101" + id="defitem2" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffdf00;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop10" /> + <stop + style="stop-color:#ff9000;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop11" /> + </linearGradient> + <linearGradient + x1="30.502899" + y1="81.021698" + x2="60.128502" + y2="55.232700" + id="defitem3" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop13" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop14" /> + </linearGradient> + <linearGradient + x1="97.928001" + y1="22.252501" + x2="21.068501" + y2="22.252501" + id="defitem4" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#a5a5a5;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop16" /> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop17" /> + </linearGradient> + <linearGradient + x1="19.594999" + y1="27.792101" + x2="101.37900" + y2="27.792101" + id="defitem5" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#a5a5a5;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop19" /> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop20" /> + </linearGradient> + <linearGradient + x1="64.755302" + y1="25.712400" + x2="110.93200" + y2="130.62399" + id="defitem6" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop22" /> + <stop + style="stop-color:#efefef;stop-opacity:1.0000000;" + offset="0.25799999" + id="stop23" /> + <stop + style="stop-color:#cdcdcd;stop-opacity:1.0000000;" + offset="0.73960000" + id="stop24" /> + <stop + style="stop-color:#c3c3c3;stop-opacity:1.0000000;" + offset="0.85949999" + id="stop25" /> + </linearGradient> + <linearGradient + x1="60.389801" + y1="116.30300" + x2="79.354897" + y2="67.517403" + id="defitem7" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#b3b3b3;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop27" /> + <stop + style="stop-color:#828282;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop28" /> + </linearGradient> + <linearGradient + x1="79.514603" + y1="82.517097" + x2="130.13800" + y2="18.255899" + id="defitem8" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop30" /> + <stop + style="stop-color:#e6e6e6;stop-opacity:1.0000000;" + offset="0.50330001" + id="stop31" /> + <stop + style="stop-color:#d4d4d4;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop32" /> + </linearGradient> + <linearGradient + x1="68.365402" + y1="73.734200" + x2="118.99000" + y2="9.4719200" + id="defitem9" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop34" /> + <stop + style="stop-color:#e6e6e6;stop-opacity:1.0000000;" + offset="0.50330001" + id="stop35" /> + <stop + style="stop-color:#d4d4d4;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop36" /> + </linearGradient> + <linearGradient + x1="62.866600" + y1="97.084396" + x2="116.78800" + y2="73.350800" + id="defitem10" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#b3caff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop38" /> + <stop + style="stop-color:#0052bc;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop39" /> + </linearGradient> + <radialGradient + cx="89.610001" + cy="32.573101" + r="15.583500" + fx="89.610001" + fy="32.573101" + id="defitem11" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="0.0000000" + id="stop41" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop42" /> + </radialGradient> + <linearGradient + x1="91.409698" + y1="41.617599" + x2="80.945503" + y2="27.787001" + id="defitem12" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop44" /> + <stop + style="stop-color:#cccccc;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop45" /> + </linearGradient> + <linearGradient + x1="84.319199" + y1="49.683201" + x2="87.490402" + y2="41.525299" + id="defitem13" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#b3b3b3;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop47" /> + <stop + style="stop-color:#828282;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop48" /> + </linearGradient> + <linearGradient + x1="85.781502" + y1="23.774000" + x2="45.193100" + y2="34.420200" + id="defitem14" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#a5a5a5;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop50" /> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop51" /> + </linearGradient> + <linearGradient + x1="68.927399" + y1="25.886000" + x2="27.287701" + y2="42.358799" + id="defitem15" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#a5a5a5;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop53" /> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop54" /> + </linearGradient> + <linearGradient + x1="74.806297" + y1="18.759001" + x2="36.551102" + y2="57.944401" + id="defitem16" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#b3b3b3;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop56" /> + <stop + style="stop-color:#828282;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop57" /> + </linearGradient> + <linearGradient + x1="45.193699" + y1="42.748402" + x2="-18.549200" + y2="80.890900" + id="defitem17" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="0.0000000" + id="stop59" /> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop61" /> + </linearGradient> + <radialGradient + cx="10.882900" + cy="87.761703" + r="11.140400" + fx="10.882900" + fy="87.761703" + id="defitem18" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="0.0000000" + id="stop63" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop64" /> + </radialGradient> + <linearGradient + x1="15.207300" + y1="77.703796" + x2="12.610400" + y2="97.136299" + id="defitem19" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop66" /> + <stop + style="stop-color:#cc0000;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop67" /> + </linearGradient> + <linearGradient + x1="23.591400" + y1="73.303596" + x2="17.194401" + y2="79.856201" + id="defitem20" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#b3b3b3;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop69" /> + <stop + style="stop-color:#828282;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop70" /> + </linearGradient> + <linearGradient + x1="11.125700" + y1="87.100502" + x2="104.75200" + y2="87.100502" + id="defitem21" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffdf00;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop72" /> + <stop + style="stop-color:#ff9000;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop73" /> + </linearGradient> + <linearGradient + x1="104.76600" + y1="64.339401" + x2="107.58800" + y2="20.723301" + id="defitem22" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop75" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop76" /> + </linearGradient> + <linearGradient + x1="17.692640" + y1="61.412624" + x2="19.019884" + y2="21.978533" + id="defitem23" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop78" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop79" /> + </linearGradient> + <linearGradient + x1="88.329002" + y1="51.221001" + x2="85.953598" + y2="52.866299" + id="defitem24" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop81" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop82" /> + </linearGradient> + <linearGradient + x1="81.272400" + y1="53.569000" + x2="82.794899" + y2="51.185299" + id="defitem25" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop84" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop85" /> + </linearGradient> + <radialGradient + cx="42.755699" + cy="104.64000" + r="21.837799" + fx="43.139099" + fy="104.33800" + id="defitem26" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop87" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop88" /> + </radialGradient> + <radialGradient + cx="89.902802" + cy="74.683296" + r="20.662201" + fx="89.902802" + fy="74.683296" + id="defitem27" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop90" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop91" /> + </radialGradient> + <radialGradient + cx="46.999100" + cy="97.870102" + r="49.695499" + fx="47.110699" + fy="97.303802" + id="defitem28" + gradientUnits="userSpaceOnUse"> + <stop + style="stop-color:#ffffff;stop-opacity:1.0000000;" + offset="0.0000000" + id="stop93" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.0000000;" + offset="1.0000000" + id="stop94" /> + </radialGradient> + <linearGradient + id="linearGradient975" + xlink:href="#defitem17" + x1="21.197054" + y1="65.809898" + x2="48.444603" + y2="57.362999" + gradientUnits="userSpaceOnUse" /> + <linearGradient + id="linearGradient976" + xlink:href="#defitem9" + x1="44.808582" + y1="40.005417" + x2="16.589544" + y2="81.307549" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="68.345337" + y1="90.622963" + x2="101.27685" + y2="68.024048" + id="linearGradient977" + xlink:href="#defitem17" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="64.755302" + y1="25.712400" + x2="110.93200" + y2="130.62399" + id="linearGradient978" + xlink:href="#defitem12" + gradientUnits="userSpaceOnUse" /> + <linearGradient + x1="77.465843" + y1="52.442471" + x2="70.698074" + y2="41.078487" + id="linearGradient980" + xlink:href="#defitem12" + gradientTransform="scale(1.200094,0.833268)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + xlink:href="#defitem12" + id="linearGradient972" + x1="15.207300" + y1="77.703796" + x2="12.610400" + y2="97.136299" + gradientUnits="userSpaceOnUse" /> + <linearGradient + xlink:href="#defitem10" + id="linearGradient1075" + x1="68.345337" + y1="90.622963" + x2="101.27685" + y2="68.024048" + gradientUnits="userSpaceOnUse" /> + <linearGradient + xlink:href="#defitem24" + id="linearGradient1076" + x1="87.698097" + y1="51.387012" + x2="85.953598" + y2="52.866299" + gradientUnits="userSpaceOnUse" /> + <linearGradient + xlink:href="#defitem3" + id="linearGradient1077" + x1="110.01673" + y1="39.602791" + x2="110.69554" + y2="37.525436" + gradientTransform="scale(0.744732,1.342765)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem2" + id="linearGradient1511" + gradientUnits="userSpaceOnUse" + x1="11.854800" + y1="64.663101" + x2="104.84800" + y2="64.663101" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem1" + id="linearGradient1518" + gradientUnits="userSpaceOnUse" + x1="11.125700" + y1="40.099499" + x2="104.75200" + y2="40.099499" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem10" + id="linearGradient1548" + gradientUnits="userSpaceOnUse" + x1="68.345337" + y1="90.622963" + x2="101.27685" + y2="68.024048" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem13" + id="linearGradient1552" + gradientUnits="userSpaceOnUse" + x1="84.319199" + y1="49.683201" + x2="87.490402" + y2="41.525299" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem12" + id="linearGradient1555" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.200094,0,0,-0.833268,2.787865,128.3662)" + x1="77.465843" + y1="52.442471" + x2="70.698074" + y2="41.078487" /> + <radialGradient + inkscape:collect="always" + xlink:href="#defitem11" + id="radialGradient1559" + gradientUnits="userSpaceOnUse" + cx="89.610001" + cy="32.573101" + fx="89.610001" + fy="32.573101" + r="15.583500" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem17" + id="linearGradient1562" + gradientUnits="userSpaceOnUse" + x1="68.345337" + y1="90.622963" + x2="101.27685" + y2="68.024048" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem7" + id="linearGradient1565" + gradientUnits="userSpaceOnUse" + x1="60.389801" + y1="116.30300" + x2="79.354897" + y2="67.517403" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem12" + id="linearGradient1568" + gradientUnits="userSpaceOnUse" + x1="64.755302" + y1="25.712400" + x2="110.93200" + y2="130.62399" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <radialGradient + inkscape:collect="always" + xlink:href="#defitem28" + id="radialGradient10588" + gradientUnits="userSpaceOnUse" + cx="46.999100" + cy="97.870102" + fx="47.110699" + fy="97.303802" + r="49.695499" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <radialGradient + inkscape:collect="always" + xlink:href="#defitem27" + id="radialGradient10591" + gradientUnits="userSpaceOnUse" + cx="89.902802" + cy="74.683296" + fx="89.902802" + fy="74.683296" + r="20.662201" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem3" + id="linearGradient10597" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.744732,0,0,-1.342765,2.787865,128.3662)" + x1="110.01673" + y1="39.602791" + x2="110.69554" + y2="37.525436" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem24" + id="linearGradient10600" + gradientUnits="userSpaceOnUse" + x1="87.698097" + y1="51.387012" + x2="85.953598" + y2="52.866299" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem0" + id="linearGradient10615" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" + x1="20.037008" + y1="28.482220" + x2="94.889908" + y2="26.172848" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem22" + id="linearGradient11357" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-1,-29.11641,114.7126)" + x1="110.28571" + y1="57.111237" + x2="107.58800" + y2="20.723301" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem3" + id="linearGradient11363" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.975701,0,0,-1,4.184334,128.3662)" + x1="29.560040" + y1="79.313225" + x2="60.128502" + y2="55.232700" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem5" + id="linearGradient11377" + x1="33.368298" + y1="107.71607" + x2="109.67058" + y2="86.740944" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(2.787865,2.41615)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem1" + id="linearGradient11449" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.000000,0.000000,0.000000,-1.000000,0.000000,125.9500)" + x1="11.125700" + y1="40.099499" + x2="104.75200" + y2="40.099499" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem1" + id="linearGradient11453" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.000000,0.000000,0.000000,-1.000000,0.000000,125.9500)" + x1="11.125700" + y1="40.099499" + x2="104.75200" + y2="40.099499" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem20" + id="linearGradient11507" + gradientUnits="userSpaceOnUse" + x1="23.591400" + y1="73.303596" + x2="17.194401" + y2="79.856201" + gradientTransform="matrix(1,0,0,-0.808173,6.93555,122.2776)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem12" + id="linearGradient11510" + gradientUnits="userSpaceOnUse" + x1="15.207300" + y1="77.703796" + x2="12.610400" + y2="97.136299" + gradientTransform="matrix(1,0,0,-0.808173,6.93555,122.2776)" /> + <radialGradient + inkscape:collect="always" + xlink:href="#defitem18" + id="radialGradient11514" + gradientUnits="userSpaceOnUse" + cx="10.882900" + cy="87.761703" + fx="10.882900" + fy="87.761703" + r="11.140400" + gradientTransform="matrix(1,0,0,-0.808173,6.93555,122.2776)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem17" + id="linearGradient11517" + gradientUnits="userSpaceOnUse" + x1="21.197054" + y1="65.809898" + x2="48.444603" + y2="57.362999" + gradientTransform="matrix(1,0,0,-0.808173,6.93555,122.2776)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem16" + id="linearGradient11520" + gradientUnits="userSpaceOnUse" + x1="74.806297" + y1="18.759001" + x2="36.551102" + y2="57.944401" + gradientTransform="matrix(1,0,0,-0.808173,2.660823,120.419)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem9" + id="linearGradient11523" + gradientUnits="userSpaceOnUse" + x1="44.808582" + y1="40.005417" + x2="16.589544" + y2="81.307549" + gradientTransform="matrix(1,0,0,-0.808173,2.660823,120.419)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem23" + id="linearGradient2092" + gradientUnits="userSpaceOnUse" + x1="17.692640" + y1="61.412624" + x2="19.019884" + y2="21.978533" + gradientTransform="matrix(1,0,0,-1,-8.921169,121.8611)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem22" + id="linearGradient2095" + gradientUnits="userSpaceOnUse" + x1="104.76600" + y1="64.339401" + x2="107.58800" + y2="20.723301" + gradientTransform="matrix(1,0,0,-1,10.54172,124.2921)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem21" + id="linearGradient2098" + gradientUnits="userSpaceOnUse" + x1="11.125700" + y1="87.100502" + x2="104.75200" + y2="87.100502" + gradientTransform="matrix(1,0,0,-1,2.787865,128.3662)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem16" + id="linearGradient3026" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-0.808173,6.93555,122.2776)" + x1="74.806297" + y1="18.759001" + x2="36.551102" + y2="57.944401" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem9" + id="linearGradient3030" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,-0.808173,6.93555,122.2776)" + x1="44.808582" + y1="40.005417" + x2="16.589544" + y2="81.307549" /> + <linearGradient + inkscape:collect="always" + xlink:href="#defitem12" + id="linearGradient2985" + x1="18.613743" + y1="107.65684" + x2="109.26532" + y2="107.65684" + gradientUnits="userSpaceOnUse" /> + </defs> + <g + inkscape:groupmode="layer" + id="layer12" + inkscape:label="SHADOW" + style="display:inline"> + <path + sodipodi:type="arc" + style="fill:#afafaf;fill-opacity:0.31791908" + id="path11475" + sodipodi:cx="67.484802" + sodipodi:cy="111.03368" + sodipodi:rx="39.886345" + sodipodi:ry="10.185145" + d="M 107.37115 111.03368 A 39.886345 10.185145 0 1 1 27.598457,111.03368 A 39.886345 10.185145 0 1 1 107.37115 111.03368 z" + transform="matrix(1.256307,0,0,1.120323,-20.32516,-13.76383)" /> + <path + sodipodi:type="arc" + style="fill:#676767;fill-opacity:0.19075144" + id="path11473" + sodipodi:cx="67.484802" + sodipodi:cy="111.03368" + sodipodi:rx="39.886345" + sodipodi:ry="10.185145" + d="M 107.37115 111.03368 A 39.886345 10.185145 0 1 1 27.598457,111.03368 A 39.886345 10.185145 0 1 1 107.37115 111.03368 z" + transform="matrix(1.097568,0,0,0.971273,-9.984449,1.970245)" /> + </g> + <g + inkscape:groupmode="layer" + id="layer11" + inkscape:label="RIM" + style="opacity:1;display:none"> + <path + style="fill:#5a4b0c;fill-opacity:1;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round" + d="M 45.381615,44.60365 C 38.534896,45.256224 31.713113,46.126616 24.959806,47.451306 C 21.65922,48.149272 18.423232,49.29547 15.258756,50.439504 C 12.218408,51.604456 9.3756421,53.387474 7.1684225,55.760771 C 4.8450873,59.034884 5.7506761,63.136926 5.889108,66.798351 C 6.9525065,72.642615 8.953945,78.459772 10.80127,84.086014 C 12.817249,89.232585 14.803528,94.454934 17.458607,99.3097 C 19.211108,102.78424 21.918338,105.85946 24.633977,108.62656 C 29.24241,111.20417 34.10757,113.41805 39.323358,114.38914 C 44.761531,115.55968 50.298652,116.35311 55.81857,117.00723 C 62.174978,117.49087 68.532069,116.94767 74.8783,116.59432 C 81.705021,115.38151 88.770977,114.82993 95.124248,111.84883 C 97.64799,110.8495 100.07538,109.45445 102.23293,107.83194 C 104.56616,105.7776 104.57208,106.82575 106.22081,104.18414 C 108.15784,102.79704 112.8577,93.528343 114.36668,89.396867 C 115.89573,85.676934 117.07284,81.526699 118.11167,77.646231 C 119.15728,71.364052 120.6594,64.899081 119.31912,58.54115 C 118.03573,56.093231 115.71781,54.46406 113.6235,52.779869 C 110.13997,51.191699 106.76865,49.320809 102.95675,48.655857 C 95.376069,46.728136 87.601797,45.580861 79.822527,44.815029 C 71.063003,44.023774 62.259554,44.248363 53.476155,44.231816 C 50.777975,44.355761 48.079795,44.479705 45.381615,44.60365 z M 75.287865,46.9474 C 84.506374,47.801377 93.765623,48.649747 102.75857,50.95256 C 106.0374,51.616131 109.07275,53.200095 112.08543,54.586277 C 113.95935,55.570376 115.72744,56.937258 116.90216,58.660726 C 117.96688,62.688982 117.36072,66.924915 116.82985,70.986787 C 115.6433,77.321167 114.31627,83.702942 111.59045,89.578673 C 110.19569,92.985146 108.33403,96.219971 106.3375,99.304676 C 104.11346,102.67852 101.49656,105.87321 97.909114,107.86179 C 93.803494,110.66383 88.7551,111.50916 84.071781,112.92131 C 78.330803,113.7463 72.615132,114.83031 66.79467,114.83273 C 63.154286,115.18825 59.528253,114.98507 55.909654,114.50525 C 51.421031,114.04574 46.892676,113.83166 42.543498,112.53069 C 38.528,111.40311 34.212409,111.12754 30.546388,109.00775 C 28.170257,107.87159 25.65445,106.93816 23.953757,104.82167 C 20.035927,100.03474 17.774862,94.055688 15.412865,88.47865 C 12.771445,81.343575 10.178886,74.14899 8.9212,66.618451 C 8.5604462,63.709677 7.6053696,60.656229 8.6316155,57.79115 C 10.61891,55.223323 13.336652,53.338886 16.450151,52.447194 C 20.602554,50.589143 25.091937,49.78853 29.530837,48.938716 C 35.598929,48.188758 41.641189,47.096397 47.725267,46.558373 C 55.589676,46.442491 63.456191,46.193844 71.309697,46.765267 C 72.635753,46.825978 73.961809,46.886689 75.287865,46.9474 z " + id="path11447" + sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccc" /> + </g> + <g + inkscape:groupmode="layer" + id="layer13" + inkscape:label="rim - dark" + style="display:inline"> + <path + style="opacity:1;fill:#907813;fill-opacity:1;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round;display:inline" + d="M 45.381615,44.60365 C 38.534896,45.256224 31.713113,46.126616 24.959806,47.451306 C 21.65922,48.149272 18.423232,49.29547 15.258756,50.439504 C 12.218408,51.604456 9.3756421,53.387474 7.1684225,55.760771 C 4.8450873,59.034884 6.1223915,63.322784 6.2608234,66.984209 C 7.3242219,72.828473 8.953945,78.459772 10.80127,84.086014 C 12.817249,89.232585 14.803528,94.454934 17.458607,99.3097 C 19.211108,102.78424 21.918338,105.85946 24.633977,108.62656 C 29.24241,111.20417 34.10757,113.41805 39.323358,114.38914 C 44.761531,115.55968 50.298652,116.35311 55.81857,117.00723 C 62.174978,117.49087 68.532069,116.94767 74.8783,116.59432 C 81.705021,115.38151 88.770977,114.82993 95.124248,111.84883 C 97.64799,110.8495 100.07538,109.45445 102.23293,107.83194 C 104.56616,105.7776 104.57208,106.82575 106.22081,104.18414 C 108.15784,102.79704 112.8577,93.528343 114.36668,89.396867 C 115.89573,85.676934 117.07284,81.526699 118.11167,77.646231 C 119.15728,71.364052 120.6594,64.899081 119.31912,58.54115 C 118.03573,56.093231 115.71781,54.46406 113.6235,52.779869 C 110.13997,51.191699 106.76865,49.320809 102.95675,48.655857 C 95.376069,46.728136 87.601797,45.580861 79.822527,44.815029 C 71.063003,44.023774 62.259554,44.248363 53.476155,44.231816 C 50.777975,44.355761 48.079795,44.479705 45.381615,44.60365 z M 75.287865,46.9474 C 84.506374,47.801377 93.765623,48.649747 102.75857,50.95256 C 106.0374,51.616131 109.07275,53.200095 112.08543,54.586277 C 113.95935,55.570376 115.72744,56.937258 116.90216,58.660726 C 117.96688,62.688982 117.36072,66.924915 116.82985,70.986787 C 115.6433,77.321167 114.31627,83.702942 111.59045,89.578673 C 110.19569,92.985146 108.33403,96.219971 106.3375,99.304676 C 104.11346,102.67852 101.49656,105.87321 97.909114,107.86179 C 93.803494,110.66383 88.7551,111.50916 84.071781,112.92131 C 78.330803,113.7463 72.615132,114.83031 66.79467,114.83273 C 63.154286,115.18825 59.528253,114.98507 55.909654,114.50525 C 51.421031,114.04574 46.892676,113.83166 42.543498,112.53069 C 38.528,111.40311 34.212409,111.12754 30.546388,109.00775 C 28.170257,107.87159 25.65445,106.93816 23.953757,104.82167 C 20.035927,100.03474 17.774862,94.055688 15.412865,88.47865 C 12.771445,81.343575 10.178886,74.14899 8.9212,66.618451 C 8.5604462,63.709677 7.6053696,60.656229 8.6316155,57.79115 C 10.61891,55.223323 13.336652,53.338886 16.450151,52.447194 C 20.602554,50.589143 25.091937,49.78853 29.530837,48.938716 C 35.598929,48.188758 41.641189,47.096397 47.725267,46.558373 C 55.589676,46.442491 63.456191,46.193844 71.309697,46.765267 C 72.635753,46.825978 73.961809,46.886689 75.287865,46.9474 z " + id="path2951" + sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccc" /> + </g> + <g + inkscape:groupmode="layer" + id="layer8" + inkscape:label="BACK" + style="display:inline"> + <path + id="path833" + style="fill:url(#linearGradient1518);fill-opacity:0.468927;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round" + d="M 6.6513222,59.451286 C 6.1917793,71.62677 17.089725,100.14832 24.047708,107.1063 C 27.591015,110.64961 41.993165,116.41635 63.509465,116.41635 C 85.025765,116.41635 98.457156,110.64961 102.00046,107.1063 C 113.46125,95.64552 119.717,75.216541 118.80287,61.40145 C 118.12469,51.152223 90.015065,45.38695 63.509465,44.99755 C 31.976165,44.53535 6.99439,50.361783 6.6513222,59.451286 z " + sodipodi:nodetypes="csssscc" /> + <path + sodipodi:nodetypes="ccccc" + id="path11349" + style="fill:url(#linearGradient11357);fill-rule:evenodd;stroke-width:1.25;display:inline" + d="M 102.21412,52.509259 C 99.002759,64.172982 96.328947,70.006782 94.64708,72.185392 C 93.05057,74.034012 70.801036,73.924025 69.083787,74.641695 C 73.198974,73.823157 76.942547,56.442701 77.837477,48.151763 C 86.248436,48.414605 99.322855,51.457889 102.21412,52.509259 z " /> + <path + sodipodi:nodetypes="ccccccc" + id="path839" + style="fill:url(#linearGradient10615);fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round" + d="M 23.778982,107.01161 L 18.613742,99.017372 C 25.453018,93.035417 40.008106,87.974303 62.180981,87.820208 C 86.325232,87.829002 104.85948,92.986097 109.00248,98.034597 C 106.77865,101.19096 104.45779,104.53809 101.91006,106.98553 C 99.799304,100.85607 83.163965,95.12845 62.394165,94.93205 C 39.463465,94.71695 30.018682,101.99811 23.778982,107.01161 z " /> + </g> + <g + inkscape:groupmode="layer" + id="layer7" + inkscape:label="bottom front" + style="display:inline"> + <path + id="path837" + style="fill:url(#linearGradient11377);fill-opacity:1;fill-rule:evenodd;stroke-width:0.86876702" + d="M 101.75987,106.04467 C 101.75987,112.14583 84.297565,117.09755 62.781265,117.09755 C 41.265065,117.09755 24.094873,111.48334 24.065407,106.17609 C 24.030756,99.935095 41.002223,94.597628 62.518423,94.597628 C 84.034723,94.597628 101.75987,99.943516 101.75987,106.04467 z " + sodipodi:nodetypes="csssc" /> + </g> + <g + inkscape:groupmode="layer" + id="layer9" + inkscape:label="LID" + style="display:inline"> + <path + id="path860" + style="fill:url(#linearGradient1511);fill-opacity:0.51000001;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round" + d="M 6.7907155,59.49775 C 15.713065,73.19995 39.552565,75.37065 63.190765,75.78775 C 86.829065,76.04545 109.51958,70.033361 118.86858,61.467161 C 121.25358,51.458661 91.9001,44.948863 63.350165,44.83825 C 25.497703,44.468979 6.0597051,52.739179 6.7907155,59.49775 z " + sodipodi:nodetypes="ccccc" /> + <path + id="path866" + style="opacity:0.72881356;fill:url(#linearGradient11363);fill-opacity:0.63276798;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round" + d="M 9.404133,60.023435 C 9.641419,65.707448 26.120418,70.804357 39.750747,72.165401 C 47.817662,72.970916 51.21012,73.229438 53.548773,69.746794 C 56.990249,64.621862 59.957952,53.473213 60.521858,47.619263 C 31.726091,46.894321 9.1355112,53.737569 9.404133,60.023435 z " + sodipodi:nodetypes="csscs" /> + </g> + <path + id="path934" + style="fill:white;fill-rule:evenodd;stroke-width:1.45019996" + d="M 62.869371,45.243254 C 47.609647,45.243254 34.103706,46.243998 24.038533,48.922541 C 19.006098,50.261761 15.062656,51.550766 12.157745,53.377445 C 10.705264,54.290785 9.3303773,55.210337 8.4982929,56.291466 C 7.7878202,57.214724 7.1303659,58.055213 7.0066155,60.35365 L 8.8350788,60.35365 C 8.8207941,58.718452 9.2164485,58.019239 9.6985475,57.392816 C 10.335004,56.565645 11.646895,55.829375 12.983665,54.988772 C 15.657256,53.30767 19.565788,51.749002 24.505615,50.434372 C 34.385269,47.805318 47.847598,46.88303 63.000792,46.88303 C 78.153884,46.883134 91.76595,48.910147 101.6453,51.539201 C 106.58523,52.853934 110.54178,54.442773 113.21512,56.123979 C 114.55179,56.964685 115.57371,57.815207 116.21027,58.642378 C 116.69146,59.267871 116.77217,59.80606 116.87205,60.35365 L 118.73228,60.35365 C 118.60013,59.258263 118.18047,58.208335 117.47127,57.286317 C 116.63901,56.205084 115.48695,55.261576 114.03427,54.348236 C 111.12991,52.521557 107.02406,50.908955 101.99213,49.569735 C 91.926553,46.891192 78.128993,45.243254 62.869371,45.243254 z " + sodipodi:nodetypes="csssccsssssssccsssc" /> + <g + inkscape:groupmode="layer" + id="layer1" + inkscape:label="doc1" + style="display:inline"> + <path + d="M 118.79287,49.96525 L 71.847965,29.30275 L 49.094765,80.99715 L 82.231565,95.58225 L 102.11667,87.85205 L 118.79287,49.96525 L 118.79287,49.96525 z " + style="fill:white;stroke-width:0.41061199" + id="path889" /> + <path + d="M 116.84787,50.56365 L 72.720165,31.14095 L 50.487165,81.47525 L 81.881365,95.29335 L 100.32907,88.09455 L 116.84787,50.56385 L 116.84787,50.56365 z " + style="fill:url(#linearGradient1568);stroke-width:0.41061199" + id="path902" /> + <path + d="M 118.88287,49.75995 L 71.732965,29.00705 L 48.799065,81.11215 L 82.226165,95.82495 L 102.28447,88.02735 L 119.08787,49.85035 L 118.88287,49.75995 z M 117.97119,50.211671 C 117.79219,50.616871 102.02287,87.50835 101.94877,87.67665 C 101.77787,87.74315 82.407865,95.27315 82.236965,95.33955 C 82.068565,95.26545 49.794465,81.06005 49.390465,80.88225 C 49.568965,80.47655 71.915886,30.660856 72.094286,30.255556 C 72.499186,30.433756 117.56619,50.033471 117.97119,50.211671 z " + style="fill:url(#linearGradient1565);stroke-width:0.41061199" + id="path911" + sodipodi:nodetypes="ccccccccccccc" /> + <path + d="M 114.93487,55.21335 L 116.97387,50.58035 L 72.706865,31.09645 L 60.858565,57.90845 C 64.132665,61.57825 68.260965,64.64475 73.107465,66.77795 C 88.441765,73.52725 106.16287,67.84765 114.93487,55.21335 z " + style="fill:url(#linearGradient1562);stroke-width:0.41061199" + id="path922" /> + <path + d="M 87.769465,80.38775 L 81.307665,95.06875 L 82.043865,95.39285 L 101.93607,87.65985 L 102.31827,86.79135 L 87.769465,80.38775 L 87.769465,80.38775 z " + style="fill:url(#radialGradient1559);fill-opacity:0.1;stroke-width:0.41061199" + id="path1083" /> + <path + d="M 82.231565,95.58225 L 102.11667,87.85205 L 88.308965,81.77465 L 82.231565,95.58225 z " + style="fill:white;stroke-width:0.41061199" + id="path1085" /> + <path + d="M 83.529365,95.22615 L 101.02927,88.42385 L 88.877565,83.07535 L 83.529365,95.22615 z " + style="fill:url(#linearGradient1555);stroke-width:0.41061199" + id="path1094" /> + <path + d="M 88.399365,81.56935 C 88.285565,81.51925 88.153765,81.57055 88.103665,81.68425 L 82.026265,95.49195 C 81.989565,95.57525 82.006365,95.67185 82.069465,95.73785 C 82.132065,95.80265 82.228565,95.82405 82.312965,95.79165 L 102.19797,88.06135 C 102.28297,88.02765 102.33927,87.94755 102.34127,87.85725 C 102.34317,87.76595 102.29037,87.68345 102.20697,87.64675 L 88.399365,81.56935 L 88.399365,81.56935 z M 101.53067,87.83915 C 100.59667,88.20155 83.588465,94.81345 82.654965,95.17705 C 83.054265,94.26995 88.255065,82.45385 88.423865,82.07025 C 88.807465,82.23905 100.62357,87.43995 101.53067,87.83915 z " + style="fill:url(#linearGradient1552);stroke-width:0.41061199" + id="path1103" /> + <path + d="M 95.8682,100.911 L 43.3098,77.7775 L 66.4433,25.219 L 119.002,48.3526 L 95.8682,100.911 z " + style="fill:none;stroke-width:0.41061199" + id="path1116" /> + <path + d="M 114.57498,55.753188 L 116.97387,50.58035 L 72.706865,31.09645 L 70.395696,36.494893 L 114.57498,55.753188 z " + style="fill:url(#linearGradient1548);stroke-width:0.41061199" + id="path1074" + sodipodi:nodetypes="ccccc" /> + </g> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="doc2" + style="display:inline"> + <path + d="M 30.734049,99.873243 L 67.198549,96.762262 L 47.140149,45.525387 L 23.234749,47.205498 L 17.377349,61.943745 L 30.734049,99.873243 L 30.734049,99.873243 z " + style="fill:white;stroke-width:0.42452899" + id="path964" /> + <path + style="fill:url(#linearGradient3030);stroke-width:0.42452899" + d="M 23.649726,48.389827 C 21.920559,52.577327 20.191393,56.764827 18.462226,60.952327 C 22.837226,73.389827 27.212226,85.827327 31.587226,98.264827 C 42.837226,97.31691 54.087226,96.368994 65.337226,95.421077 C 58.972643,79.202327 52.73948,63.246419 46.374897,47.027669 C 38.843647,47.569336 31.180976,47.84816 23.649726,48.389827 z " + id="path965" + sodipodi:nodetypes="cccccc" /> + <path + style="fill:url(#linearGradient3026);stroke-width:0.42452899" + d="M 23.055976,46.764827 C 21.86327,49.017426 20.984815,51.87252 19.910846,54.354274 C 18.934716,56.883981 17.828346,59.437629 16.930976,61.952327 C 21.443591,74.743167 25.938665,87.540603 30.462226,100.32733 C 42.902305,99.337595 55.337614,98.221562 67.774726,97.171077 C 60.988143,79.829436 54.215509,62.482279 47.430976,45.139827 C 39.305179,45.656353 31.181056,46.227354 23.055976,46.764827 z M 56.712226,71.296077 C 60.014309,79.650244 63.316393,88.00441 66.618476,96.358577 C 54.743476,97.358577 42.868476,98.358577 30.993476,99.358577 C 26.608059,86.900244 22.222643,74.44191 17.837226,61.983577 C 19.733059,57.202327 21.628893,52.421077 23.524726,47.639827 C 31.264309,47.118994 39.003893,46.59816 46.743476,46.077327 C 50.066393,54.483577 53.389309,62.889827 56.712226,71.296077 z " + id="path966" /> + <path + d="M 29.689949,93.961943 L 31.323249,98.600129 L 65.707849,95.666622 L 54.890649,68.362174 C 51.050949,66.764416 48.506749,66.462887 44.742249,66.784055 C 32.831149,67.800251 26.907749,79.949759 29.689949,93.961943 z " + style="fill:url(#linearGradient11517);stroke-width:0.42452899" + id="path967" /> + <path + d="M 29.190249,61.928875 L 24.014549,47.231279 L 23.442749,47.280012 L 17.583249,62.023592 L 17.889349,62.892944 L 29.190249,61.928875 L 29.190249,61.928875 z " + style="fill:url(#radialGradient11514);fill-opacity:0.1;stroke-width:0.42452899" + id="path974" /> + <path + d="M 28.951428,61.803143 C 28.951428,61.803143 23.587896,47.532561 23.562896,47.59479 L 17.639785,62.135905 C 17.615085,62.199428 25.213167,63.099806 28.951428,61.803143 z " + style="fill:#656565;fill-opacity:0.22674417;stroke:none;stroke-width:0.42452899;stroke-opacity:1;display:inline" + id="path11530" + sodipodi:nodetypes="cccc" /> + <path + d="M 23.234749,47.205498 L 17.377349,61.943745 L 28.102549,61.028731 L 23.234749,47.205498 z " + style="fill:white;stroke-width:0.42452899" + id="path975" /> + <path + d="M 22.89757,49.360776 L 18.268055,61.147787 L 27.049849,60.211103 L 22.89757,49.360776 z " + style="fill:url(#linearGradient11510);stroke-width:0.42452899" + id="path976" + sodipodi:nodetypes="cccc" /> + <path + d="M 28.174949,61.23425 C 28.263249,61.226734 28.302149,61.128945 28.262049,61.015073 L 23.394249,47.19184 C 23.364849,47.108436 23.299949,47.036913 23.228649,47.00992 C 23.158349,46.983735 23.095649,47.007334 23.070649,47.069563 L 17.213249,61.80781 C 17.188549,61.871333 17.205849,61.960474 17.257349,62.034826 C 17.309449,62.109986 17.384949,62.154759 17.449749,62.149263 L 28.174949,61.23425 L 28.174949,61.23425 z M 17.689949,61.705334 C 17.965449,61.013699 22.844028,48.801919 23.118628,48.109477 C 23.832691,51.251701 27.735549,60.452827 27.870749,60.83679 C 27.572849,60.862167 18.394549,61.645206 17.689949,61.705334 z " + style="fill:url(#linearGradient11507);stroke-width:0.42452899" + id="path977" + sodipodi:nodetypes="cccsccsccccccc" /> + </g> + <g + inkscape:groupmode="layer" + id="layer6" + inkscape:label="doc4" + style="display:inline"> + <g + id="g122" + transform="matrix(1,0,0,-1,2.15166,126.9994)"> + <path + id="path950" + style="fill:white;fill-rule:evenodd;stroke-width:0.78354502" + d="M 81.9334,42.7224 L 45.124,52.8879 L 36.8284,21.0747 L 84.5101,21.5391 L 81.9334,42.7224 z " /> + <path + id="path952" + style="fill:url(#defitem14);fill-rule:evenodd;stroke-width:0.64687002" + d="M 79.2746,41.2165 L 47.5188,49.3388 L 41.2753,24.3463 L 81.4159,23.7233 L 79.2746,41.2165 z " /> + <path + id="path954" + style="fill:#a3a3a3;fill-rule:evenodd;stroke-width:0.78354502" + d="M 82.5076,43.2046 L 82.0973,43.3157 L 45.2806,53.4831 L 44.6994,53.6572 L 44.5205,53.032 L 36.2085,21.2375 L 36.0106,20.4366 L 36.8354,20.4584 L 84.5161,20.9162 L 85.2286,20.9279 L 85.1242,21.606 L 82.5526,42.7996 L 82.5076,43.2046 z M 81.3585,42.24 L 83.805,22.149 L 37.6372,21.7051 L 45.5772,52.1174 L 81.3585,42.24 z " /> + </g> + </g> + <g + inkscape:groupmode="layer" + id="layer3" + inkscape:label="doc3" + style="display:inline" /> + <g + inkscape:groupmode="layer" + id="layer10" + inkscape:label="TRANSPARENCY" + style="display:inline"> + <path + inkscape:export-ydpi="100.00000" + inkscape:export-xdpi="100.00000" + d="M 6.988867,61.258951 C 7.320324,73.578073 16.113507,93.08394 19.189136,99.587404 C 27.836048,106.61708 41.667432,109.62166 63.264576,109.62166 C 81.695283,109.46261 101.74761,106.66669 108.64916,99.388576 C 113.83708,87.31344 118.55438,75.353928 119.11689,60.330963 C 116.33302,69.003229 89.637486,76.174609 63.131886,75.785209 C 33.937886,74.783209 14.079709,68.945853 6.988867,61.258951 z " + style="opacity:1;fill:#f0c515;fill-opacity:0.81976743;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round" + id="path930" + sodipodi:nodetypes="ccccccc" /> + </g> + <g + inkscape:groupmode="layer" + id="layer5" + inkscape:label="basket front" + style="opacity:1;display:inline"> + <path + inkscape:export-ydpi="100.00000" + inkscape:export-xdpi="100.00000" + d="M 7.6459739,61.127529 C 7.9774302,73.446655 15.850665,95.449523 20.371927,101.55872 C 27.717835,106.35811 41.536011,109.8845 63.133155,109.8845 C 81.563862,109.72545 100.69623,108.37516 107.59779,101.09705 C 114.09992,90.730388 119.08007,74.69682 118.98547,60.593805 C 117.59354,64.929939 118.40466,60.742734 116.64483,60.67967 C 113.57078,71.130304 76.743303,73.479378 63.52615,73.419625 C 36.694398,73.298323 14.736815,68.814431 7.6459739,61.127529 z " + style="fill:url(#linearGradient2098);fill-opacity:0.51412395;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round" + id="path11403" + sodipodi:nodetypes="ccccccsc" /> + <path + d="M 119.00496,63.665871 C 118.17271,76.984027 112.06841,95.356885 106.18108,102.13524 C 104.58457,103.98386 96.922778,104.53098 95.205533,105.24865 C 104.18331,100.22463 111.98125,80.371588 112.23876,69.164963 C 112.23876,69.164963 115.58009,69.442345 119.00496,63.665871 z " + style="opacity:0.6;fill:url(#linearGradient2095);fill-rule:evenodd;stroke-width:1.25" + id="path11405" + sodipodi:nodetypes="ccccc" /> + <path + d="M 7.0634382,60.382968 C 7.2017415,61.960641 7.7677564,62.876101 8.593538,63.929957 C 9.4192798,64.983813 10.968773,66.339214 12.410181,67.229428 C 15.292949,69.009855 19.366796,70.581626 24.360969,71.886936 C 34.349418,74.497659 48.073636,76.07235 63.216937,76.07235 C 78.360239,76.07235 92.052623,74.497659 102.04147,71.886936 C 107.03505,70.581626 111.1096,69.009855 113.99182,67.229428 C 115.43342,66.339214 116.57669,65.356555 117.40262,64.3027 C 118.22854,63.248944 118.77915,62.056924 118.77915,60.778098 C 118.77915,60.589683 118.67605,60.427753 118.65402,60.243165 L 116.80795,60.243165 C 116.83999,60.415266 117.02721,60.609119 117.02721,60.778098 C 117.02721,61.471337 116.78292,62.206367 116.15123,63.012494 C 115.51952,63.818721 114.50539,64.679228 113.17891,65.498546 C 110.52595,67.137184 106.59957,68.622954 101.69729,69.904298 C 91.893244,72.466884 78.254521,74.089812 63.216937,74.089812 C 48.179154,74.089812 34.572066,72.466985 24.767725,69.904298 C 19.865553,68.622954 16.097771,67.026699 13.44456,65.388061 C 12.117979,64.568743 10.914412,63.818721 10.282807,63.012494 C 9.6511019,62.206367 8.8665807,60.898652 8.8665807,60.205514 L 7.0634382,60.382968 z " + style="fill:white;fill-rule:evenodd;stroke-width:1.45019996" + id="path11409" + sodipodi:nodetypes="ccccccccccccccccccccc" /> + <path + sodipodi:nodetypes="ccccccs" + id="path11367" + style="fill:url(#linearGradient2985);fill-opacity:1.0;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round" + d="M 24.436088,108.19439 C 23.558637,107.06221 22.556379,107.42834 18.613742,98.885951 C 19.640411,102.06034 29.932164,109.58181 62.443823,109.76755 C 86.586906,109.52993 104.85948,105.47111 109.26532,98.166018 C 108.3557,102.24234 105.1149,106.24656 103.3557,108.43116 C 99.930725,112.1583 83.163965,117.33864 62.394165,117.14224 C 39.463465,116.92714 26.775251,111.21265 24.436088,108.19439 z " + inkscape:export-xdpi="99.533745" + inkscape:export-ydpi="99.533745" /> + <path + sodipodi:nodetypes="ccscscscs" + id="path11397" + style="fill:#2a2a2a;fill-opacity:1;fill-rule:evenodd;stroke-width:1.25;stroke-linejoin:round;display:inline" + d="M 24.173246,108.72007 C 23.295795,107.58789 20.453639,103.87997 18.745164,99.674478 C 21.295528,103.36441 24.820285,107.21193 25.976677,108.39952 C 30.116118,112.65065 49.732497,115.51826 62.39599,115.49565 C 74.467531,115.37684 92.309836,113.13452 101.58486,108.25986 C 102.43889,107.811 106.20816,103.9213 109.52817,97.903178 C 107.43575,103.8194 105.20412,106.32333 103.3557,108.43116 C 100.01994,112.23507 83.163965,117.33864 62.394165,117.14224 C 39.463465,116.92714 26.512409,111.73833 24.173246,108.72007 z " /> + </g> + <g + inkscape:groupmode="layer" + id="layer4" + inkscape:label="handle" + style="display:inline"> + <path + d="M 90.412225,75.074998 L 83.869695,78.408702 C 84.287509,81.532121 91.225538,80.858337 92.520586,78.66015 C 93.37382,77.16685 91.009725,75.353798 90.412225,75.074998 z " + style="fill:#5e2f2f;fill-opacity:0.40116278;fill-rule:evenodd;stroke-width:1.25" + id="path979" + sodipodi:nodetypes="cccc" /> + <path + style="fill:#9e6d0b;fill-opacity:0.99402511;fill-rule:evenodd;stroke-linejoin:round" + d="M 55.600365,4.2286501 C 46.555805,4.6648134 38.621297,10.968448 34.845508,18.925864 C 30.627366,27.557007 29.570776,37.052891 30.569936,46.514006 C 33.965622,45.983058 35.450201,46.101593 37.754151,45.64683 C 37.215739,36.930584 37.329198,27.484594 42.719352,19.668329 C 46.052815,15.025873 51.921024,12.08957 58.068491,11.995407 C 64.035973,11.904002 71.220947,14.90597 74.9474,20.602008 C 81.252176,29.241389 82.847011,40.444765 83.25823,50.912215 C 83.58309,58.887605 83.137947,66.499753 83.1349,74.47865 C 81.526323,76.029108 82.080949,79.061835 84.3224,79.72865 C 86.888457,80.492033 90.652955,80.437097 92.319115,78.10365 C 92.884989,76.657556 91.907512,75.06215 90.694115,74.3224 C 90.786588,58.747558 91.81875,42.677916 86.725365,27.6974 C 83.781943,19.463585 78.686135,11.375003 70.621134,7.3851827 C 66.015081,5.0030974 60.776981,3.9793908 55.600365,4.2286501 z " + id="path870" + sodipodi:nodetypes="ccccczcccsccccc" /> + <path + d="M 57.256665,5.1661501 C 48.670365,5.1661501 41.474065,9.7091501 37.006665,17.10315 C 32.636836,24.336477 30.700474,34.778398 31.52484,46.326651 C 34.991077,45.646486 32.219354,46.367006 34.335932,45.645687 C 32.102093,27.985998 39.474413,11.964531 52.052649,9.6694341 C 62.188971,7.8199028 69.448565,12.63015 73.412865,16.57215 C 77.377165,20.51515 80.177265,26.09015 82.069065,32.50985 C 85.478765,44.07905 84.457765,63.67585 84.498465,74.73995 C 79.311565,81.36215 97.014265,79.98095 89.692465,74.69035 C 89.708565,63.20005 90.837165,44.98075 86.756665,31.13485 C 84.716365,24.21215 81.625365,17.85215 76.850365,13.10315 C 72.075365,8.3551501 65.552065,5.3371501 57.287865,5.1661501 C 57.277465,5.1661501 57.267065,5.1661501 57.256665,5.1661501 z " + style="fill:#fa0;fill-rule:evenodd;stroke-width:4.875;stroke-linejoin:round" + id="path873" + sodipodi:nodetypes="csccsssccsssc" /> + <path + d="M 89.047337,75.256906 C 89.366037,76.345606 89.03835,77.588436 87.00695,78.358536 C 88.71975,78.517836 90.356738,77.679956 90.46909,77.317737 C 90.803853,76.238473 89.644837,75.535706 89.047337,75.256906 z " + style="fill:url(#linearGradient10600);fill-rule:evenodd;stroke-width:1.25" + id="path875" /> + <path + d="M 84.947065,78.11115 C 84.733765,77.08995 84.863909,75.914435 86.083609,74.901135 C 84.976909,75.110135 84.331936,76.265935 84.304636,76.686835 C 84.281636,77.007335 84.547165,77.84965 84.947065,78.11115 z " + style="fill:url(#linearGradient10597);fill-opacity:1;fill-rule:evenodd;stroke-width:1.25" + id="path877" /> + <path + d="M 89.194165,74.49875 C 89.694965,62.64135 89.644965,36.76295 85.536565,29.37485 C 88.625165,38.78795 88.613065,60.36405 87.747065,74.56275 L 89.194165,74.49875 z " + style="fill:url(#radialGradient10591);fill-rule:evenodd;stroke-width:1.25" + id="path883" /> + <path + d="M 83.035665,27.34615 C 79.813165,18.55015 75.111665,8.2861501 62.855665,6.3561501 C 52.480165,4.7461501 47.587865,5.4951501 39.360965,14.27815 C 46.478665,8.6631501 53.324765,5.6301501 62.139065,7.5821501 C 73.800165,10.21015 78.049665,18.98415 81.315765,27.92615 C 85.537565,39.27855 85.673265,62.87015 85.197865,74.88135 C 86.950465,58.47065 86.798465,37.26645 83.035665,27.34615 z " + style="fill:url(#radialGradient10588);fill-rule:evenodd;stroke-width:1.25" + id="path885" /> + </g> +</svg> diff --git a/src/debugwindow.cpp b/src/debugwindow.cpp new file mode 100644 index 0000000..7df259e --- /dev/null +++ b/src/debugwindow.cpp @@ -0,0 +1,73 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qlayout.h> +#include <qtextbrowser.h> +#include <qstring.h> +#include <qevent.h> +#include <klocale.h> + +#include "global.h" +#include "debugwindow.h" + +DebugWindow::DebugWindow(QWidget *parent, const char *name ) + : QWidget(parent, name != 0 ? name : "DebugWindow") +{ + Global::debugWindow = this; + setCaption(i18n("Debug Window")); + + layout = new QVBoxLayout(this); + textBrowser = new QTextBrowser(this); + + textBrowser->setWordWrap(QTextBrowser::NoWrap); + + layout->addWidget(textBrowser); + textBrowser->show(); +} + +DebugWindow::~DebugWindow() +{ + delete textBrowser; + delete layout; +} + +void DebugWindow::postMessage(const QString msg) +{ + textBrowser->append(msg); +} + +DebugWindow& DebugWindow::operator<<(const QString msg) +{ + textBrowser->append(msg); + return *this; +} + +void DebugWindow::insertHLine() +{ + textBrowser->append("<hr>"); +} + +void DebugWindow::closeEvent(QCloseEvent *event) +{ + Global::debugWindow = 0L; + QWidget::closeEvent(event); +} + +#include "debugwindow.moc" diff --git a/src/debugwindow.h b/src/debugwindow.h new file mode 100644 index 0000000..c078aec --- /dev/null +++ b/src/debugwindow.h @@ -0,0 +1,54 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef DEBUGWINDOW_H +#define DEBUGWINDOW_H + +#include <qwidget.h> + +class QVBoxLayout; +class QTextBrowser; +class QString; +class QCloseEvent; + +/**A simple window that display text through debuging messages. + *@author S�bastien Lao�t + */ + +class DebugWindow : public QWidget { + Q_OBJECT + public: + /** Construtor and destructor */ + DebugWindow(QWidget *parent = 0, const char *name = 0); + ~DebugWindow(); + /** Methods to post a message to the debug window */ + void postMessage(const QString msg); + DebugWindow& operator<<(const QString msg); + void insertHLine(); + protected: + virtual void closeEvent(QCloseEvent *event); + private: + QVBoxLayout *layout; + QTextBrowser *textBrowser; +}; + +#define DEBUG_WIN if (Global::debugWindow) *Global::debugWindow + +#endif // DEBUGWINDOW_H diff --git a/src/exporterdialog.cpp b/src/exporterdialog.cpp new file mode 100644 index 0000000..569baf7 --- /dev/null +++ b/src/exporterdialog.cpp @@ -0,0 +1,148 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kurlrequester.h> +#include <klineedit.h> +#include <kfiledialog.h> +#include <qcheckbox.h> +#include <qdir.h> +#include <qhbox.h> +#include <qvbox.h> +#include <qlayout.h> +#include <qlabel.h> +#include <klocale.h> +#include <kconfig.h> + +#include "exporterdialog.h" +#include "basket.h" + +ExporterDialog::ExporterDialog(Basket *basket, QWidget *parent, const char *name) + : KDialogBase(parent, name, /*modal=*/true, i18n("Export Basket to HTML"), + KDialogBase::Ok | KDialogBase::Cancel, KDialogBase::Ok, /*separator=*/true), + m_basket(basket) +{ + QVBox *page = makeVBoxMainWidget(); + + QWidget *wid = new QWidget(page); + QHBoxLayout *hLay = new QHBoxLayout(wid, /*margin=*/0, KDialogBase::spacingHint()); + m_url = new KURLRequester("", wid); + m_url->setCaption(i18n("HTML Page Filename")); + m_url->setFilter("text/html"); + m_url->fileDialog()->setOperationMode(KFileDialog::Saving); + hLay->addWidget( new QLabel(m_url, i18n("&Filename:"), wid) ); + hLay->addWidget( m_url ); + + m_embedLinkedFiles = new QCheckBox(i18n("&Embed linked local files"), page); + m_embedLinkedFolders = new QCheckBox(i18n("Embed &linked local folders"), page); + m_erasePreviousFiles = new QCheckBox(i18n("Erase &previous files in target folder"), page); + m_formatForImpression = new QCheckBox(i18n("For&mat for impression"), page); + m_formatForImpression->hide(); + + load(); + m_url->lineEdit()->setFocus(); + + showTile(true); + // Add a stretch at the bottom: + // Duplicated code from AddBasketWizard::addStretch(QWidget *parent): + (new QWidget(page))->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + // Double the width, because the filename should be visible + QSize size(sizeHint()); + resize(QSize(size.width() * 2, size.height())); +/* +========================== ++ [00000000000 ] Progress bar! ++ newBasket -> name folder as the basket +*/ +} + +ExporterDialog::~ExporterDialog() +{ +} + +void ExporterDialog::show() +{ + KDialogBase::show(); + + QString lineEditText = m_url->lineEdit()->text(); + int selectionStart = lineEditText.findRev("/") + 1; + m_url->lineEdit()->setSelection(selectionStart, lineEditText.length() - selectionStart - QString(".html").length()); +} + +void ExporterDialog::load() +{ + KConfig *config = KGlobal::config(); + config->setGroup("HTML Export"); + + QString folder = config->readEntry("lastFolder", QDir::homeDirPath()) + "/"; + QString url = folder + QString(m_basket->basketName()).replace("/", "_") + ".html"; + m_url->setURL(url); + + m_embedLinkedFiles->setChecked( config->readBoolEntry("embedLinkedFiles", true) ); + m_embedLinkedFolders->setChecked( config->readBoolEntry("embedLinkedFolders", false) ); + m_erasePreviousFiles->setChecked( config->readBoolEntry("erasePreviousFiles", true) ); + m_formatForImpression->setChecked( config->readBoolEntry("formatForImpression", false) ); +} + +void ExporterDialog::save() +{ + KConfig *config = KGlobal::config(); + config->setGroup("HTML Export"); + + QString folder = KURL(m_url->url()).directory(); + config->writeEntry( "lastFolder", folder ); + config->writeEntry( "embedLinkedFiles", m_embedLinkedFiles->isChecked() ); + config->writeEntry( "embedLinkedFolders", m_embedLinkedFolders->isChecked() ); + config->writeEntry( "erasePreviousFiles", m_erasePreviousFiles->isChecked() ); + config->writeEntry( "formatForImpression", m_formatForImpression->isChecked() ); +} + +void ExporterDialog::slotOk() +{ + save(); + KDialogBase::slotOk(); +} + +QString ExporterDialog::filePath() +{ + return m_url->url(); +} + +bool ExporterDialog::embedLinkedFiles() +{ + return m_embedLinkedFiles->isChecked(); +} + +bool ExporterDialog::embedLinkedFolders() +{ + return m_embedLinkedFolders->isChecked(); +} + +bool ExporterDialog::erasePreviousFiles() +{ + return m_erasePreviousFiles->isChecked(); +} + +bool ExporterDialog::formatForImpression() +{ + return m_formatForImpression->isChecked(); +} + +#include "exporterdialog.moc" diff --git a/src/exporterdialog.h b/src/exporterdialog.h new file mode 100644 index 0000000..af2e0cd --- /dev/null +++ b/src/exporterdialog.h @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef EXPORTERDIALOG_H +#define EXPORTERDIALOG_H + +#include <kdialogbase.h> + +class KURLRequester; +class QCheckBox; +class QString; + +class Basket; + +/** + * @author S�bastien Lao�t + */ +class ExporterDialog : public KDialogBase +{ + Q_OBJECT + public: + ExporterDialog(Basket *basket, QWidget *parent = 0, const char *name = 0); + ~ExporterDialog(); + QString filePath(); + bool embedLinkedFiles(); + bool embedLinkedFolders(); + bool erasePreviousFiles(); + bool formatForImpression(); + void show(); + protected slots: + void slotOk(); + void load(); + void save(); + private: + Basket *m_basket; + KURLRequester *m_url; + QCheckBox *m_embedLinkedFiles; + QCheckBox *m_embedLinkedFolders; + QCheckBox *m_erasePreviousFiles; + QCheckBox *m_formatForImpression; +}; + +#endif // EXPORTERDIALOG_H diff --git a/src/filter.cpp b/src/filter.cpp new file mode 100644 index 0000000..57aa4ae --- /dev/null +++ b/src/filter.cpp @@ -0,0 +1,307 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qlayout.h> +//#include <ktoolbarbutton.h> +#include <qtoolbutton.h> +#include <qlabel.h> +//#include <qcombobox.h> +//#include <klineedit.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kglobalsettings.h> +#include <kapplication.h> +#include <kiconloader.h> +#include <qpixmap.h> +#include <qimage.h> +#include <qpainter.h> +#include <qbitmap.h> +#include <kdialogbase.h> + +#include "filter.h" +//#include "settings.h" +#include "global.h" +#include "bnpview.h" +#include "tools.h" +#include "tag.h" +#include "focusedwidgets.h" + +/** FilterBar */ + +FilterBar::FilterBar(QWidget *parent, const char *name) + : QWidget(parent, name)/*, m_blinkTimer(this), m_blinkedTimes(0)*/ +{ + QHBoxLayout *hBox = new QHBoxLayout(this, /*margin*/0, /*spacing*/0); + + // Create every widgets: + QIconSet resetIconSet = kapp->iconLoader()->loadIconSet("locationbar_erase", KIcon::Toolbar); + QIconSet inAllIconSet = kapp->iconLoader()->loadIconSet("find", KIcon::Toolbar); + + + m_resetButton = new QToolButton(this); + m_resetButton->setIconSet(resetIconSet); + m_resetButton->setTextLabel(i18n("Reset Filter"));//, /*groupText=*/"", this, SLOT(reset()), 0); + m_resetButton->setAutoRaise(true); + //new KToolBarButton("locationbar_erase", /*id=*/1230, this, /*name=*/0, i18n("Reset Filter")); + m_lineEdit = new FocusedLineEdit(this); + QLabel *label = new QLabel(m_lineEdit, i18n("&Filter: "), this); + m_tagsBox = new FocusedComboBox(this); + QLabel *label2 = new QLabel(m_tagsBox, i18n("T&ag: "), this); + m_inAllBasketsButton = new QToolButton(this); + m_inAllBasketsButton->setIconSet(inAllIconSet); + m_inAllBasketsButton->setTextLabel(i18n("Filter all Baskets"));//, /*groupText=*/"", this, SLOT(inAllBaskets()), 0); + m_inAllBasketsButton->setAutoRaise(true); + + // Configure the Reset button: + m_resetButton->setEnabled(false); + + // Configure the Tags combobox: + repopulateTagsComnbo(); + + // Configure the Serach in all Baskets button: + m_inAllBasketsButton->setToggleButton(true); +// m_inAllBasketsButton->setOn(true); +// Global::bnpView->toggleFilterAllBaskets(true); + +// m_lineEdit->setMaximumWidth(150); + + // Layout all those widgets: +// hBox->addStretch(); + hBox->addWidget(m_resetButton); + hBox->addSpacing(KDialogBase::spacingHint()); + hBox->addWidget(label); + hBox->addWidget(m_lineEdit); + hBox->addSpacing(KDialogBase::spacingHint()); + hBox->addWidget(label2); + hBox->addWidget(m_tagsBox); + hBox->addSpacing(KDialogBase::spacingHint()); + hBox->addWidget(m_inAllBasketsButton); + + m_data = new FilterData(); // TODO: Not a pointer! and return a const & !! + +// connect( &m_blinkTimer, SIGNAL(timeout()), this, SLOT(blinkBar()) ); + connect( m_resetButton, SIGNAL(clicked()), this, SLOT(reset()) ); + connect( m_lineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(textChanged(const QString&)) ); + connect( m_tagsBox, SIGNAL(activated(int)), this, SLOT(tagChanged(int)) ); + +// connect( m_inAllBasketsButton, SIGNAL(clicked()), this, SLOT(inAllBaskets()) ); + connect( m_inAllBasketsButton, SIGNAL(toggled(bool)), Global::bnpView, SLOT(toggleFilterAllBaskets(bool)) ); + + connect( m_lineEdit, SIGNAL(escapePressed()), this, SIGNAL(escapePressed()) ); + connect( m_lineEdit, SIGNAL(returnPressed()), this, SIGNAL(returnPressed()) ); + connect( m_tagsBox, SIGNAL(escapePressed()), this, SIGNAL(escapePressed()) ); + connect( m_tagsBox, SIGNAL(returnPressed2()), this, SIGNAL(returnPressed()) ); +} + +FilterBar::~FilterBar() +{ +} + +void FilterBar::setFilterAll(bool filterAll) +{ + m_inAllBasketsButton->setOn(filterAll); +} + +void FilterBar::setFilterData(const FilterData &data) +{ + m_lineEdit->setText(data.string); + + int index = 0; + switch (data.tagFilterType) { + default: + case FilterData::DontCareTagsFilter: index = 0; break; + case FilterData::NotTaggedFilter: index = 1; break; + case FilterData::TaggedFilter: index = 2; break; + case FilterData::TagFilter: filterTag(data.tag); return; + case FilterData::StateFilter: filterState(data.state); return; + } + + if (m_tagsBox->currentItem() != index) { + m_tagsBox->setCurrentItem(index); + tagChanged(index); + } +} + +void FilterBar::repopulateTagsComnbo() +{ + static const int ICON_SIZE = 16; + + m_tagsBox->clear(); + m_tagsMap.clear(); + m_statesMap.clear(); + + m_tagsBox->insertItem("", 0); + m_tagsBox->insertItem(i18n("(Not tagged)"), 1); + m_tagsBox->insertItem(i18n("(Tagged)"), 2); + + int index = 3; + Tag *tag; + State *state; + QString icon; + QString text; + QPixmap emblem; + for (Tag::List::iterator it = Tag::all.begin(); it != Tag::all.end(); ++it) { + tag = *it; + state = tag->states().first(); + // Insert the tag in the combo-box: + if (tag->countStates() > 1) { + text = tag->name(); + icon = ""; + } else { + text = state->name(); + icon = state->emblem(); + } + emblem = kapp->iconLoader()->loadIcon(icon, KIcon::Desktop, ICON_SIZE, KIcon::DefaultState, 0L, /*canReturnNull=*/true); + m_tagsBox->insertItem(emblem, text, index); + // Update the mapping: + m_tagsMap.insert(index, tag); + ++index; + // Insert sub-states, if needed: + if (tag->countStates() > 1) { + for (State::List::iterator it2 = tag->states().begin(); it2 != tag->states().end(); ++it2) { + state = *it2; + // Insert the state: + text = state->name(); + icon = state->emblem(); + emblem = kapp->iconLoader()->loadIcon(icon, KIcon::Desktop, ICON_SIZE, KIcon::DefaultState, 0L, /*canReturnNull=*/true); + // Indent the emblem to show the hierarchy relation: + if (!emblem.isNull()) + emblem = Tools::indentPixmap(emblem, /*depth=*/1, /*deltaX=*/2 * ICON_SIZE / 3); + m_tagsBox->insertItem(emblem, text, index); + // Update the mapping: + m_statesMap.insert(index, state); + ++index; + } + } + } +} + +void FilterBar::reset() +{ + m_lineEdit->setText(""); // m_data->isFiltering will be set to false; + if (m_tagsBox->currentItem() != 0) { + m_tagsBox->setCurrentItem(0); + tagChanged(0); + } +} + +void FilterBar::filterTag(Tag *tag) +{ + int index = 0; + + for (QMap<int, Tag*>::Iterator it = m_tagsMap.begin(); it != m_tagsMap.end(); ++it) + if (it.data() == tag) { + index = it.key(); + break; + } + if (index <= 0) + return; + + if (m_tagsBox->currentItem() != index) { + m_tagsBox->setCurrentItem(index); + tagChanged(index); + } +} + +void FilterBar::filterState(State *state) +{ + int index = 0; + + for (QMap<int, State*>::Iterator it = m_statesMap.begin(); it != m_statesMap.end(); ++it) + if (it.data() == state) { + index = it.key(); + break; + } + if (index <= 0) + return; + + if (m_tagsBox->currentItem() != index) { + m_tagsBox->setCurrentItem(index); + tagChanged(index); + } +} + +void FilterBar::inAllBaskets() +{ + // TODO! +} + +void FilterBar::setEditFocus() +{ + m_lineEdit->setFocus(); +} + +bool FilterBar::hasEditFocus() +{ + return m_lineEdit->hasFocus(); +} + +const FilterData& FilterBar::filterData() +{ + return *m_data; +} + +void FilterBar::textChanged(const QString &text) +{ + m_data->string = text; + m_data->isFiltering = (!m_data->string.isEmpty() || m_data->tagFilterType != FilterData::DontCareTagsFilter); + m_resetButton->setEnabled(m_data->isFiltering); + emit newFilter(*m_data); +} + +void FilterBar::tagChanged(int index) +{ + m_data->tag = 0; + m_data->state = 0; + switch (index) { + case 0: + m_data->tagFilterType = FilterData::DontCareTagsFilter; + break; + case 1: + m_data->tagFilterType = FilterData::NotTaggedFilter; + break; + case 2: + m_data->tagFilterType = FilterData::TaggedFilter; + break; + default: + // Try to find if we are filtering a tag: + QMapIterator<int, Tag*> it = m_tagsMap.find(index); + if (it != m_tagsMap.end()) { + m_data->tagFilterType = FilterData::TagFilter; + m_data->tag = *it; + } else { + // If not, try to find if we are filtering a state: + QMapIterator<int, State*> it2 = m_statesMap.find(index); + if (it2 != m_statesMap.end()) { + m_data->tagFilterType = FilterData::StateFilter; + m_data->state = *it2; + } else { + // If not (should never happens), do as if the tags filter was reseted: + m_data->tagFilterType = FilterData::DontCareTagsFilter; + } + } + break; + } + m_data->isFiltering = (!m_data->string.isEmpty() || m_data->tagFilterType != FilterData::DontCareTagsFilter); + m_resetButton->setEnabled(m_data->isFiltering); + emit newFilter(*m_data); +} + +#include "filter.moc" diff --git a/src/filter.h b/src/filter.h new file mode 100644 index 0000000..56bea49 --- /dev/null +++ b/src/filter.h @@ -0,0 +1,93 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef FILTER_H +#define FILTER_H + +#include <qwidget.h> +#include <qmap.h> + +#include "focusedwidgets.h" + +class QToolButton; + +class Tag; +class State; + +/** The structure that contain all filter terms + * @author S�bastien Lao�t + */ +class FilterData +{ + public: + // Useful Enum for tagFilterType: + enum TagFilterType { DontCareTagsFilter = 0, NotTaggedFilter, TaggedFilter, TagFilter, StateFilter }; + // Constructor and Destructor: + FilterData() { isFiltering = false; tagFilterType = DontCareTagsFilter; tag = 0; state = 0; } + ~FilterData() {} + // Filter data: + QString string; + int tagFilterType; + Tag *tag; + State *state; + bool isFiltering; +}; + +/** A QWidget that allow user to enter terms to filter in a Basket. + * @author S�bastien Lao�t + */ +class FilterBar : public QWidget +{ + Q_OBJECT + public: + FilterBar(QWidget *parent = 0, const char *name = 0); + ~FilterBar(); + const FilterData& filterData(); + signals: + void newFilter(const FilterData &data); + void escapePressed(); + void returnPressed(); + public slots: + void repopulateTagsComnbo(); + void reset(); + void inAllBaskets(); + void setEditFocus(); + void filterTag(Tag *tag); + void filterState(State *state); + void setFilterAll(bool filterAll); + void setFilterData(const FilterData &data); + public: + bool hasEditFocus(); + KLineEdit* lineEdit() { return m_lineEdit; } + private slots: + void textChanged(const QString &text); + void tagChanged(int index); + private: + FilterData *m_data; + FocusedLineEdit *m_lineEdit; + QToolButton *m_resetButton; + FocusedComboBox *m_tagsBox; + QToolButton *m_inAllBasketsButton; + + QMap<int, Tag*> m_tagsMap; + QMap<int, State*> m_statesMap; +}; + +#endif // FILTER_H diff --git a/src/focusedwidgets.cpp b/src/focusedwidgets.cpp new file mode 100644 index 0000000..05c8b84 --- /dev/null +++ b/src/focusedwidgets.cpp @@ -0,0 +1,295 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qpopupmenu.h> +#include <klocale.h> + +#include <iostream> + +#include "focusedwidgets.h" +#include "bnpview.h" +#include "global.h" +#include "basket.h" + +#ifdef KeyPress +#undef KeyPress +#endif +#include <qevent.h> + +/** class FocusedTextEdit */ + +FocusedTextEdit::FocusedTextEdit(bool disableUpdatesOnKeyPress, QWidget *parent, const char *name) + : KTextEdit(parent, name), + m_disableUpdatesOnKeyPress(disableUpdatesOnKeyPress) +{ + setWFlags(Qt::WNoAutoErase); // Does not work, we still need the disableUpdatesOnKeyPress hack! +} + +FocusedTextEdit::~FocusedTextEdit() +{ +} + +/** + * Thanks to [email protected], author of TuxCards + * Code copied from tuxcards-1.2/src/gui/editor/editor.cpp + * + *** + * Override the regular paste() methode, so that lines are + * not separated by each other with an blank line. + */ +void FocusedTextEdit::paste() +{ + adaptClipboardText(QClipboard::Selection); + adaptClipboardText(QClipboard::Clipboard); + + // If we paste a application/x-qrichtext content starting with a "-" or a "*", + // then auto-bulletting will crash. + // So we insert a space to be sure what we paste will not trigger the auto-bulleting. + +// enum AutoFormatting { AutoNone = 0, AutoBulletList = 0x00000001, AutoAll = 0xffffffff } +// uint oldAutoFormating = autoFormatting(); +// setAutoFormatting(AutoNone); + + QClipboard *clipboard = QApplication::clipboard(); + int paragraph; + int index; + getCursorPosition(¶graph, &index); + + bool preventAutoBullet = (index == 0) && + (clipboard->data(QClipboard::Selection)->provides("application/x-qrichtext") || + clipboard->data(QClipboard::Clipboard)->provides("application/x-qrichtext") ); + + if (preventAutoBullet) + insert(" "); + + KTextEdit::paste(); + + if (preventAutoBullet) { + int paragraph2; + int index2; + getCursorPosition(¶graph2, &index2); + setSelection(paragraph, index, paragraph, index + 1); + removeSelectedText(); + if (paragraph == paragraph2) // We removed one character in that paragraph, so we should move the cursor back to old position... minus one character + index2--; + setCursorPosition(paragraph2, index2); + } + + +// setAutoFormatting(oldAutoFormating); +} + +/** + * Thanks to [email protected], author of TuxCards + * Code copied from tuxcards-1.2/src/gui/editor/editor.cpp + * + *** + * Auxiliar method that takes the text from the clipboard - using the + * specified 'mode' -, replaces all '\n' within that text and writes + * it back to the clipboard. + */ +void FocusedTextEdit::adaptClipboardText(QClipboard::Mode mode) +{ + QClipboard *clipboard = QApplication::clipboard(); + if (!clipboard) + return; + + if ( (textFormat() == Qt::RichText) && (!clipboard->data(mode)->provides("application/x-qrichtext")) ) { + QString text = clipboard->text(mode); + if (text) { + text = text.replace("\n", QChar(0x2028)); + clipboard->setText(text, mode); + } + } +} + + +QTextCursor* FocusedTextEdit::textCursor() const +{ + return KTextEdit::textCursor(); +} + + +void FocusedTextEdit::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) { + emit escapePressed(); + return; + // In RichTextFormat mode, [Return] create a new paragraphe. + // To keep consistency with TextFormat mode (new line on [Return]), + // we redirect [Return] to simulate [Ctrl+Return] (create a new line in both modes). + // Create new paragraphes still possible in RichTextFormat mode with [Shift+Enter]. + } else if (event->key() == Qt::Key_Return && event->state() == 0) + event = new QKeyEvent(QEvent::KeyPress, event->key(), event->ascii(), Qt::ControlButton, + event->text(), event->isAutoRepeat(), event->count() ); + else if (event->key() == Qt::Key_Return && event->state() & Qt::ControlButton) + event = new QKeyEvent(QEvent::KeyPress, event->key(), event->ascii(), Qt::ShiftButton, + event->text(), event->isAutoRepeat(), event->count() ); + + if (m_disableUpdatesOnKeyPress) + setUpdatesEnabled(false); + KTextEdit::keyPressEvent(event); + // Workarround (for ensuring the cursor to be visible): signal not emited when pressing those keys: + if (event->key() == Qt::Key_Home || event->key() == Qt::Key_End || event->key() == Qt::Key_PageUp || event->key() == Qt::Key_PageDown) { + int para; + int index; + getCursorPosition(¶, &index); + emit cursorPositionChanged(para, index); + } + if (m_disableUpdatesOnKeyPress) { + setUpdatesEnabled(true); + if (text().isEmpty()) + ;// emit textChanged(); // TODO: DOESN'T WORK: the editor is not resized down to only one line of text + else + ensureCursorVisible(); + updateContents(); + } +} + +void FocusedTextEdit::wheelEvent(QWheelEvent *event) +{ + if (event->delta() > 0 && contentsY() > 0) { + KTextEdit::wheelEvent(event); + return; + } else if (event->delta() < 0 && contentsY() + visibleHeight() < contentsHeight()) { + KTextEdit::wheelEvent(event); + return; + } else + Global::bnpView->currentBasket()->wheelEvent(event); +} + +void FocusedTextEdit::enterEvent(QEvent *event) +{ + emit mouseEntered(); + KTextEdit::enterEvent(event); +} + +QPopupMenu* FocusedTextEdit::createPopupMenu(const QPoint &pos) +{ + QPopupMenu *menu = KTextEdit::createPopupMenu(pos); + + int index = 0; + int id = 0; + while (true) { + id = menu->idAt(index); + if (id == -1) + break; + // Disable Spell Check for rich text editors, because it doesn't work anyway: + if (textFormat() == Qt::RichText && (menu->text(id) == i18n("Auto Spell Check") || menu->text(id) == i18n("Check Spelling..."))) + menu->setItemEnabled(id, false); + // Always enable tabulations!: + if (menu->text(id) == i18n("Allow Tabulations")) + menu->setItemEnabled(id, false); + index++; + } + + // And return the menu: + return menu; +} + +/** class FocusedColorCombo: */ + +FocusedColorCombo::FocusedColorCombo(QWidget *parent, const char *name) + : KColorCombo(parent, name) +{ +} + +FocusedColorCombo::~FocusedColorCombo() +{ +} + +void FocusedColorCombo::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) + emit escapePressed(); + else if (event->key() == Qt::Key_Return) + emit returnPressed2(); + else + KColorCombo::keyPressEvent(event); +} + +/** class FocusedFontCombo: */ + +FocusedFontCombo::FocusedFontCombo(QWidget *parent, const char *name) + : KFontCombo(parent, name) +{ +} + +FocusedFontCombo::~FocusedFontCombo() +{ +} + +void FocusedFontCombo::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) + emit escapePressed(); + else if (event->key() == Qt::Key_Return) + emit returnPressed2(); + else + KFontCombo::keyPressEvent(event); +} + +/** class FocusedComboBox: */ + +FocusedComboBox::FocusedComboBox(QWidget *parent, const char *name) + : KComboBox(parent, name) +{ +} + +FocusedComboBox::~FocusedComboBox() +{ +} + +void FocusedComboBox::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) + emit escapePressed(); + else if (event->key() == Qt::Key_Return) + emit returnPressed2(); + else + KComboBox::keyPressEvent(event); +} + +/** class FocusedLineEdit: */ + +FocusedLineEdit::FocusedLineEdit(QWidget *parent, const char *name) + : KLineEdit(parent, name) +{ +} + +FocusedLineEdit::~FocusedLineEdit() +{ +} + +void FocusedLineEdit::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) + emit escapePressed(); + else + KLineEdit::keyPressEvent(event); +} + +void FocusedLineEdit::enterEvent(QEvent *event) +{ + emit mouseEntered(); + KLineEdit::enterEvent(event); +} + +#include "focusedwidgets.moc" diff --git a/src/focusedwidgets.h b/src/focusedwidgets.h new file mode 100644 index 0000000..4d1bfcb --- /dev/null +++ b/src/focusedwidgets.h @@ -0,0 +1,110 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef FOCUSEDWIDGETS_H +#define FOCUSEDWIDGETS_H + +#include <ktextedit.h> +#include <kcolorcombo.h> +#include <kfontcombo.h> +#include <kcombobox.h> +#include <klineedit.h> +#include <kapplication.h> +#include <qclipboard.h> + +class FocusedTextEdit : public KTextEdit +{ + Q_OBJECT + public: + FocusedTextEdit(bool disableUpdatesOnKeyPress, QWidget *parent = 0, const char *name = 0); + ~FocusedTextEdit(); + void paste(); + QTextCursor* textCursor() const; + protected: + void adaptClipboardText(QClipboard::Mode mode); + void keyPressEvent(QKeyEvent *event); + void wheelEvent(QWheelEvent *event); + void enterEvent(QEvent *event); + QPopupMenu* createPopupMenu(const QPoint &pos); + signals: + void escapePressed(); + void mouseEntered(); + private: + bool m_disableUpdatesOnKeyPress; +}; + +// TODO: Rename to EscapableKColorCombo +class FocusedColorCombo : public KColorCombo +{ + Q_OBJECT + public: + FocusedColorCombo(QWidget *parent = 0, const char *name = 0); + ~FocusedColorCombo(); + protected: + void keyPressEvent(QKeyEvent *event); + signals: + void escapePressed(); + void returnPressed2(); +}; + +// TODO: Rename to EscapableKFontCombo +class FocusedFontCombo : public KFontCombo +{ + Q_OBJECT + public: + FocusedFontCombo(QWidget *parent = 0, const char *name = 0); + ~FocusedFontCombo(); + protected: + void keyPressEvent(QKeyEvent *event); + signals: + void escapePressed(); + void returnPressed2(); +}; + +// TODO: Rename to EscapableKComboBox +class FocusedComboBox : public KComboBox +{ + Q_OBJECT + public: + FocusedComboBox(QWidget *parent = 0, const char *name = 0); + ~FocusedComboBox(); + protected: + void keyPressEvent(QKeyEvent *event); + signals: + void escapePressed(); + void returnPressed2(); +}; + +// TODO: Rename to EscapableKLineEdit +class FocusedLineEdit : public KLineEdit +{ + Q_OBJECT + public: + FocusedLineEdit(QWidget *parent = 0, const char *name = 0); + ~FocusedLineEdit(); + protected: + void keyPressEvent(QKeyEvent *event); + void enterEvent(QEvent *event); + signals: + void escapePressed(); + void mouseEntered(); +}; + +#endif // FOCUSEDWIDGETS_H diff --git a/src/formatimporter.cpp b/src/formatimporter.cpp new file mode 100644 index 0000000..c98ad38 --- /dev/null +++ b/src/formatimporter.cpp @@ -0,0 +1,307 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qstring.h> +#include <qstringlist.h> +#include <qdir.h> +#include <qfileinfo.h> +#include <qdom.h> +#include <kglobalsettings.h> +#include <kmessagebox.h> +#include <klocale.h> +#include <kapplication.h> + +#include <iostream> + +#include "formatimporter.h" +#include "notecontent.h" +#include "notefactory.h" +#include "bnpview.h" +#include "basket.h" +#include "global.h" +#include "xmlwork.h" +#include "tools.h" + +bool FormatImporter::shouldImportBaskets() +{ + // We should import if the application have not successfully loaded any basket... + if (Global::bnpView->firstListViewItem()) + return false; + + // ... And there is at least one folder in the save folder, with a ".basket" file inside that folder. + QDir dir(Global::savesFolder(), QString::null, QDir::Name | QDir::IgnoreCase, QDir::Dirs | QDir::NoSymLinks); + QStringList list = dir.entryList(); + for (QStringList::Iterator it = list.begin(); it != list.end(); ++it) + if (*it != "." && *it != ".." && dir.exists(Global::savesFolder() + *it + "/.basket")) + return true; + + return false; +} + +void FormatImporter::copyFolder(const QString &folder, const QString &newFolder) +{ + copyFinished = false; + KIO::CopyJob *copyJob = KIO::copyAs(KURL(folder), KURL(newFolder), /*showProgressInfo=*/false); + connect( copyJob, SIGNAL(result(KIO::Job*)), this, SLOT(slotCopyingDone(KIO::Job*)) ); + while (!copyFinished) + kapp->processEvents(); +} + +void FormatImporter::moveFolder(const QString &folder, const QString &newFolder) +{ + copyFinished = false; + KIO::CopyJob *copyJob = KIO::moveAs(KURL(folder), KURL(newFolder), /*showProgressInfo=*/false); + connect( copyJob, SIGNAL(result(KIO::Job*)), this, SLOT(slotCopyingDone(KIO::Job*)) ); + while (!copyFinished) + kapp->processEvents(); +} + +void FormatImporter::slotCopyingDone(KIO::Job *) +{ +// std::cout << "Copy finished of " + from.path() + " to " + to.path() << std::endl; + copyFinished = true; +} + +void FormatImporter::importBaskets() +{ + std::cout << "Import Baskets: Preparing..." << std::endl; + + // Some preliminary preparations (create the destination folders and the basket tree file): + QDir dirPrep; + dirPrep.mkdir(Global::savesFolder()); + dirPrep.mkdir(Global::basketsFolder()); + QDomDocument document("basketTree"); + QDomElement root = document.createElement("basketTree"); + document.appendChild(root); + + // First up, establish a list of every baskets, ensure the old order (if any), and count them. + QStringList baskets; + + // Read the 0.5.0 baskets order: + QDomDocument *doc = XMLWork::openFile("container", Global::savesFolder() + "container.baskets"); + if (doc != 0) { + QDomElement docElem = doc->documentElement(); + QDomElement basketsElem = XMLWork::getElement(docElem, "baskets"); + QDomNode n = basketsElem.firstChild(); + while (!n.isNull()) { + QDomElement e = n.toElement(); + if ((!e.isNull()) && e.tagName() == "basket") + baskets.append(e.text()); + n = n.nextSibling(); + } + } + + // Then load the baskets that weren't loaded (import < 0.5.0 ones): + QDir dir(Global::savesFolder(), QString::null, QDir::Name | QDir::IgnoreCase, QDir::Dirs | QDir::NoSymLinks); + QStringList list = dir.entryList(); + if (list.count() > 2) // Pass "." and ".." + for (QStringList::Iterator it = list.begin(); it != list.end(); ++it) // For each folder + if (*it != "." && *it != ".." && dir.exists(Global::savesFolder() + *it + "/.basket")) // If it can be a basket folder + if ( baskets.find((*it) + "/") == baskets.end() && + baskets.find(*it) == baskets.end() ) // And if it is not already in the imported baskets list + baskets.append(*it); + + std::cout << "Import Baskets: Found " << baskets.count() << " baskets to import." << std::endl; + + // Import every baskets: + int i = 0; + for (QStringList::iterator it = baskets.begin(); it != baskets.end(); ++it) { + ++i; + std::cout << "Import Baskets: Importing basket " << i << " of " << baskets.count() << "..." << std::endl; + + // Move the folder to the new repository (normal basket) or copy the folder (mirorred folder): + QString folderName = *it; + if (folderName.startsWith("/")) { // It was a folder mirror: + KMessageBox::information(0, i18n("<p>Folder mirroring is not possible anymore (see <a href='http://basket.kde.org/'>basket.kde.org</a> for more information).</p>" + "<p>The folder <b>%1</b> has been copied for the basket needs. You can either delete this folder or delete the basket, or use both. But remember that " + "modifying one will not modify the other anymore as they are now separate entities.</p>").arg(folderName), i18n("Folder Mirror Import"), + "", KMessageBox::AllowLink); + // Also modify folderName to be only the folder name and not the full path anymore: + QString newFolderName = folderName; + if (newFolderName.endsWith("/")) + newFolderName = newFolderName.left(newFolderName.length() - 1); + newFolderName = newFolderName.mid(newFolderName.findRev('/') + 1); + newFolderName = Tools::fileNameForNewFile(newFolderName, Global::basketsFolder()); + FormatImporter f; + f.copyFolder(folderName, Global::basketsFolder() + newFolderName); + folderName = newFolderName; + } else + dir.rename(Global::savesFolder() + folderName, Global::basketsFolder() + folderName); // Move the folder + + // Import the basket structure file and get the properties (to add them in the tree basket-properties cache): + QDomElement properties = importBasket(folderName); + + // Add it to the XML document: + QDomElement basketElement = document.createElement("basket"); + root.appendChild(basketElement); + basketElement.setAttribute("folderName", folderName); + basketElement.appendChild(properties); + } + + // Finalize (write to disk and delete now useless files): + std::cout << "Import Baskets: Finalizing..." << std::endl; + + QFile file(Global::basketsFolder() + "baskets.xml"); + if (file.open(IO_WriteOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); + QString xml = document.toString(); + stream << "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"; + stream << xml; + file.close(); + } + + Tools::deleteRecursively(Global::savesFolder() + ".tmp"); + dir.remove(Global::savesFolder() + "container.baskets"); + + std::cout << "Import Baskets: Finished." << std::endl; +} + +QDomElement FormatImporter::importBasket(const QString &folderName) +{ + // Load the XML file: + QDomDocument *document = XMLWork::openFile("basket", Global::basketsFolder() + folderName + "/.basket"); + if (!document) { + std::cout << "Import Baskets: Failed to read the basket file!" << std::endl; + return QDomElement(); + } + QDomElement docElem = document->documentElement(); + + // Import properties (change <background color=""> to <appearance backgroundColor="">, and figure out if is a checklist or not): + QDomElement properties = XMLWork::getElement(docElem, "properties"); + QDomElement background = XMLWork::getElement(properties, "background"); + QColor backgroundColor = QColor(background.attribute("color")); + if (backgroundColor.isValid() && (backgroundColor != KGlobalSettings::baseColor())) { // Use the default color if it was already that color: + QDomElement appearance = document->createElement("appearance"); + appearance.setAttribute("backgroundColor", backgroundColor.name()); + properties.appendChild(appearance); + } + QDomElement disposition = document->createElement("disposition"); + disposition.setAttribute("mindMap", "false"); + disposition.setAttribute("columnCount", "1"); + disposition.setAttribute("free", "false"); + bool isCheckList = XMLWork::trueOrFalse( XMLWork::getElementText(properties, "showCheckBoxes", false) ); + + // Insert all notes in a group (column): 1/ rename "items" to "group", 2/ add "notes" to root, 3/ move "group" into "notes" + QDomElement column = XMLWork::getElement(docElem, "items"); + column.setTagName("group"); + QDomElement notes = document->createElement("notes"); + notes.appendChild(column); + docElem.appendChild(notes); + + // Import notes from older representations: + QDomNode n = column.firstChild(); + while ( ! n.isNull() ) { + QDomElement e = n.toElement(); + if (!e.isNull()) { + e.setTagName("note"); + QDomElement content = XMLWork::getElement(e, "content"); + // Add Check tag: + if (isCheckList) { + bool isChecked = XMLWork::trueOrFalse(e.attribute("checked", "false")); + XMLWork::addElement(*document, e, "tags", (isChecked ? "todo_done" : "todo_unchecked")); + } + // Import annotations as folded groups: + QDomElement parentE = column; + QString annotations = XMLWork::getElementText(e, "annotations", ""); + if (!annotations.isEmpty()) { + QDomElement annotGroup = document->createElement("group"); + column.insertBefore(annotGroup, e); + annotGroup.setAttribute("folded", "true"); + annotGroup.appendChild(e); + parentE = annotGroup; + // Create the text note and add it to the DOM tree: + QDomElement annotNote = document->createElement("note"); + annotNote.setAttribute("type", "text"); + annotGroup.appendChild(annotNote); + QString annotFileName = Tools::fileNameForNewFile("annotations1.txt", Basket::fullPathForFolderName(folderName)); + QString annotFullPath = Basket::fullPathForFolderName(folderName) + "/" + annotFileName; + QFile file(annotFullPath); + if (file.open(IO_WriteOnly)) { + QTextStream stream(&file); + stream << annotations; + file.close(); + } + XMLWork::addElement(*document, annotNote, "content", annotFileName); + n = annotGroup; + } + // Import Launchers from 0.3.x, 0.4.0 and 0.5.0-alphas: + QString runCommand = e.attribute("runcommand"); // Keep compatibility with 0.4.0 and 0.5.0-alphas versions + runCommand = XMLWork::getElementText(e, "action", runCommand); // Keep compatibility with 0.3.x versions + if ( ! runCommand.isEmpty() ) { // An import should be done + // Prepare the launcher note: + QString title = content.attribute("title", ""); + QString icon = content.attribute("icon", ""); + if (title.isEmpty()) title = runCommand; + if (icon.isEmpty()) icon = NoteFactory::iconForCommand(runCommand); + // Import the launcher note: + // Adapted version of "QString launcherName = NoteFactory::createNoteLauncherFile(runCommand, title, icon, this)": + QString launcherContent = QString( + "[Desktop Entry]\n" + "Exec=%1\n" + "Name=%2\n" + "Icon=%3\n" + "Encoding=UTF-8\n" + "Type=Application\n").arg(runCommand, title, icon.isEmpty() ? QString("exec") : icon); + QString launcherFileName = Tools::fileNameForNewFile("launcher.desktop", Global::basketsFolder() + folderName /*+ "/"*/); + QString launcherFullPath = Global::basketsFolder() + folderName /*+ "/"*/ + launcherFileName; + QFile file(launcherFullPath); + if (file.open(IO_WriteOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); + stream << launcherContent; + file.close(); + } + // Add the element to the DOM: + QDomElement launcherElem = document->createElement("note"); + parentE.insertBefore(launcherElem, e); + launcherElem.setAttribute("type", "launcher"); + XMLWork::addElement(*document, launcherElem, "content", launcherFileName); + } + // Import unknown ns to 0.6.0: + if (e.attribute("type") == "unknow") + e.setAttribute("type", "unknown"); + // Import links from version < 0.5.0: + if (!content.attribute("autotitle").isEmpty() && content.attribute("autoTitle").isEmpty()) + content.setAttribute("autoTitle", content.attribute("autotitle")); + if (!content.attribute("autoicon").isEmpty() && content.attribute("autoIcon").isEmpty()) + content.setAttribute("autoIcon", content.attribute("autoicon")); + } + n = n.nextSibling(); + } + + // Save the resulting XML file: + QFile file(Global::basketsFolder() + folderName + "/.basket"); + if (file.open(IO_WriteOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); +// QString xml = document->toString(); +// stream << "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"; +// stream << xml; + stream << document->toString(); // Document is ALREADY using UTF-8 + file.close(); + } else + std::cout << "Import Baskets: Failed to save the basket file!" << std::endl; + + // Return the newly created properties (to put in the basket tree): + return properties; +} + +#include "formatimporter.moc" diff --git a/src/formatimporter.h b/src/formatimporter.h new file mode 100644 index 0000000..18c8af8 --- /dev/null +++ b/src/formatimporter.h @@ -0,0 +1,51 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef FORMATIMPORTER_H +#define FORMATIMPORTER_H + +#include <qobject.h> +//#include <qwidget.h> +#include <qdom.h> + +namespace KIO { + class Job; +} + +/** + * @author S�astien Laot + */ +class FormatImporter : QObject +{ + Q_OBJECT + public: + static bool shouldImportBaskets(); + static void importBaskets(); + static QDomElement importBasket(const QString &folderName); + + void copyFolder(const QString &folder, const QString &newFolder); + void moveFolder(const QString &folder, const QString &newFolder); + private slots: + void slotCopyingDone(KIO::Job*); + private: + bool copyFinished; +}; + +#endif // FORMATIMPORTER_H diff --git a/src/global.cpp b/src/global.cpp new file mode 100644 index 0000000..7645248 --- /dev/null +++ b/src/global.cpp @@ -0,0 +1,100 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kglobal.h> +#include <kstandarddirs.h> +#include <qstring.h> +#include <kaction.h> +#include <kapplication.h> +#include <kmainwindow.h> +#include <qdir.h> +#include <kdebug.h> +#include <kconfig.h> + +#include "global.h" +#include "bnpview.h" +#include "settings.h" + +/** Define initial values for global variables : */ + +QString Global::s_customSavesFolder = ""; +LikeBack *Global::likeBack = 0L; +DebugWindow *Global::debugWindow = 0L; +BackgroundManager *Global::backgroundManager = 0L; +SystemTray *Global::systemTray = 0L; +BNPView *Global::bnpView = 0L; +KGlobalAccel *Global::globalAccel = 0L; +KConfig *Global::basketConfig = 0L; +AboutData Global::basketAbout; + +void Global::setCustomSavesFolder(const QString &folder) +{ + s_customSavesFolder = folder; +} + +#include <iostream> +QString Global::savesFolder() +{ + static QString *folder = 0L; // Memorize the folder to do not have to re-compute it each time it's needed + + if (folder == 0L) { // Initialize it if not yet done + if (!s_customSavesFolder.isEmpty()) { // Passed by command line (for development & debug purpose) + QDir dir; + dir.mkdir(s_customSavesFolder); + folder = new QString(s_customSavesFolder.endsWith("/") ? s_customSavesFolder : s_customSavesFolder + "/"); + } else if (!Settings::dataFolder().isEmpty()) { // Set by config option (in Basket -> Backup & Restore) + QDir dir; + dir.mkdir(s_customSavesFolder); + folder = new QString(Settings::dataFolder().endsWith("/") ? Settings::dataFolder() : Settings::dataFolder() + "/"); + } else { // The default path (should be that for most computers) + folder = new QString(KGlobal::dirs()->saveLocation("data", "basket/")); + } + } + + return *folder; +} + +QString Global::basketsFolder() { return savesFolder() + "baskets/"; } +QString Global::backgroundsFolder() { return savesFolder() + "backgrounds/"; } +QString Global::templatesFolder() { return savesFolder() + "templates/"; } +QString Global::tempCutFolder() { return savesFolder() + "temp-cut/"; } + +QString Global::openNoteIcon() // FIXME: Now an edit icon +{ + return Global::bnpView->m_actEditNote->icon(); +} + +KMainWindow* Global::mainWindow() +{ + QWidget* res = kapp->mainWidget(); + + if(res && res->inherits("KMainWindow")) + { + return static_cast<KMainWindow*>(res); + } + return 0; +} + +KConfig* Global::config() +{ + if(!Global::basketConfig) + Global::basketConfig = KSharedConfig::openConfig("basketrc"); + return Global::basketConfig; +} diff --git a/src/global.h b/src/global.h new file mode 100644 index 0000000..6337358 --- /dev/null +++ b/src/global.h @@ -0,0 +1,71 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef GLOBAL_H +#define GLOBAL_H + +#include <qstring.h> +#include "aboutdata.h" + +class LikeBack; +class DebugWindow; +class BackgroundManager; +class SystemTray; +class BNPView; +class KGlobalAccel; +class KMainWindow; +class KAboutData; + +/** Handle all global variables of the application. + * This file only declare classes : developer should include + * the .h files of variables he use. + * @author S�astien Laot + */ +class Global +{ + private: + static QString s_customSavesFolder; + public: + // Global Variables: + static LikeBack *likeBack; + static DebugWindow *debugWindow; + static BackgroundManager *backgroundManager; + static SystemTray *systemTray; + static BNPView *bnpView; + static KGlobalAccel *globalAccel; + static KConfig *basketConfig; + static AboutData basketAbout; + + // Application Folders: + static void setCustomSavesFolder(const QString &folder); + static QString savesFolder(); /// << @return e.g. "/home/username/.kde/share/apps/basket/". + static QString basketsFolder(); /// << @return e.g. "/home/username/.kde/share/apps/basket/baskets/". + static QString backgroundsFolder(); /// << @return e.g. "/home/username/.kde/share/apps/basket/backgrounds/". + static QString templatesFolder(); /// << @return e.g. "/home/username/.kde/share/apps/basket/templates/". + static QString tempCutFolder(); /// << @return e.g. "/home/username/.kde/share/apps/basket/temp-cut/". (was ".tmp/") + + // Various Things: + static QString openNoteIcon(); /// << @return the icon used for the "Open" action on notes. + static KMainWindow* mainWindow(); + static KConfig* config(); + static KAboutData* about() { return &basketAbout; }; +}; + +#endif // GLOBAL_H diff --git a/src/htmlexporter.cpp b/src/htmlexporter.cpp new file mode 100644 index 0000000..9e41d35 --- /dev/null +++ b/src/htmlexporter.cpp @@ -0,0 +1,560 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "htmlexporter.h" +#include "bnpview.h" +#include "basketlistview.h" +#include "basket.h" +#include "note.h" +#include "tools.h" +#include "config.h" +#include "tag.h" + +#include <kapplication.h> +#include <kglobal.h> +#include <kconfig.h> +#include <kiconloader.h> +#include <kfiledialog.h> +#include <kmessagebox.h> +#include <qdir.h> +#include <qfile.h> +#include <qpainter.h> +#include <kglobalsettings.h> +#include <kprogress.h> + +HTMLExporter::HTMLExporter(Basket *basket) +{ + QDir dir; + + // Compute a default file name & path: + KConfig *config = KGlobal::config(); + config->setGroup("Export to HTML"); + QString folder = config->readEntry("lastFolder", QDir::homeDirPath()) + "/"; + QString url = folder + QString(basket->basketName()).replace("/", "_") + ".html"; + + // Ask a file name & path to the user: + QString filter = "*.html *.htm|" + i18n("HTML Documents") + "\n*|" + i18n("All Files"); + QString destination = url; + for (bool askAgain = true; askAgain; ) { + // Ask: + destination = KFileDialog::getSaveFileName(destination, filter, 0, i18n("Export to HTML")); + // User canceled? + if (destination.isEmpty()) + return; + // File already existing? Ask for overriding: + if (dir.exists(destination)) { + int result = KMessageBox::questionYesNoCancel( + 0, + "<qt>" + i18n("The file <b>%1</b> already exists. Do you really want to override it?") + .arg(KURL(destination).fileName()), + i18n("Override File?"), + KGuiItem(i18n("&Override"), "filesave") + ); + if (result == KMessageBox::Cancel) + return; + else if (result == KMessageBox::Yes) + askAgain = false; + } else + askAgain = false; + } + + // Create the progress dialog that will always be shown during the export: + KProgressDialog dialog(0, 0, i18n("Export to HTML"), i18n("Exporting to HTML. Please wait..."), /*Not modal, for password dialogs!*/false); + dialog.showCancelButton(false); + dialog.setAutoClose(true); + dialog.show(); + progress = dialog.progressBar(); + + // Remember the last folder used for HTML exporation: + config->writeEntry("lastFolder", KURL(destination).directory()); + config->sync(); + + prepareExport(basket, destination); + exportBasket(basket, /*isSubBasket*/false); + + progress->advance(1); // Finishing finished +} + +HTMLExporter::~HTMLExporter() +{ +} + +void HTMLExporter::prepareExport(Basket *basket, const QString &fullPath) +{ + progress->setTotalSteps(/*Preparation:*/1 + /*Finishing:*/1 + /*Basket:*/1 + /*SubBaskets:*/Global::bnpView->basketCount(Global::bnpView->listViewItemForBasket(basket))); + progress->setValue(0); + kapp->processEvents(); + + // Remember the file path choosen by the user: + filePath = fullPath; + fileName = KURL(fullPath).fileName(); + exportedBasket = basket; + + BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); + withBasketTree = (item->firstChild() != 0); + + // Create and empty the files folder: + QString filesFolderPath = i18n("HTML export folder (files)", "%1_files").arg(filePath) + "/"; // eg.: "/home/seb/foo.html_files/" + Tools::deleteRecursively(filesFolderPath); + QDir dir; + dir.mkdir(filesFolderPath); + + // Create sub-folders: + iconsFolderPath = filesFolderPath + i18n("HTML export folder (icons)", "icons") + "/"; // eg.: "/home/seb/foo.html_files/icons/" + imagesFolderPath = filesFolderPath + i18n("HTML export folder (images)", "images") + "/"; // eg.: "/home/seb/foo.html_files/images/" + basketsFolderPath = filesFolderPath + i18n("HTML export folder (baskets)", "baskets") + "/"; // eg.: "/home/seb/foo.html_files/baskets/" + dir.mkdir(iconsFolderPath); + dir.mkdir(imagesFolderPath); + dir.mkdir(basketsFolderPath); + + progress->advance(1); // Preparation finished +} + +#include <iostream> + +void HTMLExporter::exportBasket(Basket *basket, bool isSubBasket) +{ + if (!basket->isLoaded()) { + basket->load(); + } + + // Compute the absolute & relative paths for this basket: + filesFolderPath = i18n("HTML export folder (files)", "%1_files").arg(filePath) + "/"; + if (isSubBasket) { + basketFilePath = basketsFolderPath + basket->folderName().left(basket->folderName().length() - 1) + ".html"; + filesFolderName = "../"; + dataFolderName = basket->folderName().left(basket->folderName().length() - 1) + "-" + i18n("HTML export folder (data)", "data") + "/"; + dataFolderPath = basketsFolderPath + dataFolderName; + basketsFolderName = ""; + } else { + basketFilePath = filePath; + filesFolderName = i18n("HTML export folder (files)", "%1_files").arg(KURL(filePath).fileName()) + "/"; + dataFolderName = filesFolderName + i18n("HTML export folder (data)", "data") + "/"; + dataFolderPath = filesFolderPath + i18n("HTML export folder (data)", "data") + "/"; + basketsFolderName = filesFolderName + i18n("HTML export folder (baskets)", "baskets") + "/"; + } + iconsFolderName = (isSubBasket ? "../" : filesFolderName) + i18n("HTML export folder (icons)", "icons") + "/"; // eg.: "foo.html_files/icons/" or "../icons/" + imagesFolderName = (isSubBasket ? "../" : filesFolderName) + i18n("HTML export folder (images)", "images") + "/"; // eg.: "foo.html_files/images/" or "../images/" + + std::cout << "Exporting ================================================" << std::endl; + std::cout << " filePath:" << filePath << std::endl; + std::cout << " basketFilePath:" << basketFilePath << std::endl; + std::cout << " filesFolderPath:" << filesFolderPath << std::endl; + std::cout << " filesFolderName:" << filesFolderName << std::endl; + std::cout << " iconsFolderPath:" << iconsFolderPath << std::endl; + std::cout << " iconsFolderName:" << iconsFolderName << std::endl; + std::cout << " imagesFolderPath:" << imagesFolderPath << std::endl; + std::cout << " imagesFolderName:" << imagesFolderName << std::endl; + std::cout << " dataFolderPath:" << dataFolderPath << std::endl; + std::cout << " dataFolderName:" << dataFolderName << std::endl; + std::cout << " basketsFolderPath:" << basketsFolderPath << std::endl; + std::cout << " basketsFolderName:" << basketsFolderName << std::endl; + + // Create the data folder for this basket: + QDir dir; + dir.mkdir(dataFolderPath); + + backgroundColorName = basket->backgroundColor().name().lower().mid(1); + + // Generate basket icons: + QString basketIcon16 = iconsFolderName + copyIcon(basket->icon(), 16); + QString basketIcon32 = iconsFolderName + copyIcon(basket->icon(), 32); + + // Generate the [+] image for groups: + QPixmap expandGroup(Note::EXPANDER_WIDTH, Note::EXPANDER_HEIGHT); + expandGroup.fill(basket->backgroundColor()); + QPainter painter(&expandGroup); + Note::drawExpander(&painter, 0, 0, basket->backgroundColor(), /*expand=*/true, basket); + painter.end(); + expandGroup.save(imagesFolderPath + "expand_group_" + backgroundColorName + ".png", "PNG"); + + // Generate the [-] image for groups: + QPixmap foldGroup(Note::EXPANDER_WIDTH, Note::EXPANDER_HEIGHT); + foldGroup.fill(basket->backgroundColor()); + painter.begin(&foldGroup); + Note::drawExpander(&painter, 0, 0, basket->backgroundColor(), /*expand=*/false, basket); + painter.end(); + foldGroup.save(imagesFolderPath + "fold_group_" + backgroundColorName + ".png", "PNG"); + + // Open the file to write: + QFile file(basketFilePath); + if (!file.open(IO_WriteOnly)) + return; + stream.setDevice(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); + + // Compute the colors to draw dragient for notes: + QColor topBgColor; + QColor bottomBgColor; + Note::getGradientColors(basket->backgroundColor(), &topBgColor, &bottomBgColor); + // Compute the gradient image for notes: + QString gradientImageFileName = Basket::saveGradientBackground(basket->backgroundColor(), basket->QScrollView::font(), imagesFolderPath); + + // Output the header: + QString borderColor = Tools::mixColor(basket->backgroundColor(), basket->textColor()).name(); + stream << + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n" + "<html>\n" + " <head>\n" + " <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n" + " <meta name=\"Generator\" content=\"" << kapp->aboutData()->programName() << " " << VERSION << " http://basket.kde.org/\">\n" + " <style type=\"text/css\">\n" +// " @media print {\n" +// " span.printable { display: inline; }\n" +// " }\n" + " body { margin: 10px; font: 11px sans-serif; }\n" // TODO: Use user font + " h1 { text-align: center; }\n" + " img { border: none; vertical-align: middle; }\n"; + if (withBasketTree) { + stream << + " .tree { margin: 0; padding: 1px 0 1px 1px; width: 150px; _width: 149px; overflow: hidden; float: left; }\n" + " .tree ul { margin: 0 0 0 10px; padding: 0; }\n" + " .tree li { padding: 0; margin: 0; list-style: none; }\n" + " .tree a { display: block; padding: 1px; height: 16px; text-decoration: none;\n" + " white-space: nowrap; word-wrap: normal; text-wrap: suppress; color: black; }\n" + " .tree span { -moz-border-radius: 6px; display: block; float: left;\n" + " line-height: 16px; height: 16px; vertical-align: middle; padding: 0 1px; }\n" + " .tree img { vertical-align: top; padding-right: 1px; }\n" + " .tree .current { background-color: " << KGlobalSettings::highlightColor().name() << "; " + "-moz-border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px; color: " << KGlobalSettings::highlightedTextColor().name() << "; }\n" + " .basketSurrounder { margin-left: 152px; _margin: 0; _float: right; }\n"; + } + stream << + " .basket { background-color: " << basket->backgroundColor().name() << "; border: solid " << borderColor << " 1px; " + "font: " << Tools::cssFontDefinition(basket->QScrollView::font()) << "; color: " << basket->textColor().name() << "; padding: 1px; width: 100%; }\n" + " table.basket { border-collapse: collapse; }\n" + " .basket * { padding: 0; margin: 0; }\n" + " .basket table { width: 100%; border-spacing: 0; _border-collapse: collapse; }\n" + " .column { vertical-align: top; }\n" + " .columnHandle { width: " << Note::RESIZER_WIDTH << "px; background: transparent url('" << imagesFolderName << "column_handle_" << backgroundColorName << ".png') repeat-y; }\n" + " .group { margin: 0; padding: 0; border-collapse: collapse; width: 100% }\n" + " .groupHandle { margin: 0; width: " << Note::GROUP_WIDTH << "px; text-align: center; }\n" + " .note { padding: 1px 2px; background: " << bottomBgColor.name() << " url('" << imagesFolderName << gradientImageFileName << "')" + " repeat-x; border-top: solid " << topBgColor.name() << + " 1px; border-bottom: solid " << Tools::mixColor(topBgColor, bottomBgColor).name() << + " 1px; width: 100%; }\n" + " .tags { width: 1px; white-space: nowrap; }\n" + " .tags img { padding-right: 2px; }\n" + << LinkLook::soundLook->toCSS("sound", basket->textColor()) + << LinkLook::fileLook->toCSS("file", basket->textColor()) + << LinkLook::localLinkLook->toCSS("local", basket->textColor()) + << LinkLook::networkLinkLook->toCSS("network", basket->textColor()) + << LinkLook::launcherLook->toCSS("launcher", basket->textColor()) + << + " .unknown { margin: 1px 2px; border: 1px solid " << borderColor << "; -moz-border-radius: 4px; }\n"; + QValueList<State*> states = basket->usedStates(); + QString statesCss; + for (State::List::Iterator it = states.begin(); it != states.end(); ++it) + statesCss += (*it)->toCSS(imagesFolderPath, imagesFolderName, basket->QScrollView::font()); + stream << + statesCss << + " .credits { text-align: right; margin: 3px 0 0 0; _margin-top: -17px; font-size: 80%; color: " << borderColor << "; }\n" + " </style>\n" + " <title>" << Tools::textToHTMLWithoutP(basket->basketName()) << "</title>\n" + " <link rel=\"shortcut icon\" type=\"image/png\" href=\"" << basketIcon16 << "\">\n"; + // Create the column handle image: + QPixmap columnHandle(Note::RESIZER_WIDTH, 50); + painter.begin(&columnHandle); + Note::drawInactiveResizer(&painter, 0, 0, columnHandle.height(), basket->backgroundColor(), /*column=*/true); + painter.end(); + columnHandle.save(imagesFolderPath + "column_handle_" + backgroundColorName + ".png", "PNG"); + + stream << + " </head>\n" + " <body>\n" + " <h1><img src=\"" << basketIcon32 << "\" width=\"32\" height=\"32\" alt=\"\"> " << Tools::textToHTMLWithoutP(basket->basketName()) << "</h1>\n"; + + if (withBasketTree) + writeBasketTree(basket); + + // If filtering, only export filtered notes, inform to the user: + // TODO: Filtering tags too!! + // TODO: Make sure only filtered notes are exported! +// if (decoration()->filterData().isFiltering) +// stream << +// " <p>" << i18n("Notes matching the filter "%1":").arg(Tools::textToHTMLWithoutP(decoration()->filterData().string)) << "</p>\n"; + + stream << + " <div class=\"basketSurrounder\">\n"; + + if (basket->isColumnsLayout()) + stream << + " <table class=\"basket\">\n" + " <tr>\n"; + else + stream << + " <div class=\"basket\" style=\"position: relative; height: " << basket->contentsHeight() << "px; width: " << basket->contentsWidth() << "px; min-width: 100%;\">\n"; + + for (Note *note = basket->firstNote(); note; note = note->next()) + exportNote(note, /*indent=*/(basket->isFreeLayout() ? 4 : 5)); + + // Output the footer: + if (basket->isColumnsLayout()) + stream << + " </tr>\n" + " </table>\n"; + else + stream << + " </div>\n"; + stream << QString( + " </div>\n" + " <p class=\"credits\">%1</p>\n").arg( + i18n("Made with %1, a KDE tool to take notes and keep information at hand.") + .arg("<a href=\"http://basket.kde.org/\">%1</a> %2") + .arg(kapp->aboutData()->programName(), VERSION)); + + // Copy a transparent GIF image in the folder, needed for the JavaScript hack: + QString gifFileName = "spacer.gif"; + QFile transGIF(imagesFolderPath + gifFileName); + if (!transGIF.exists() && transGIF.open(IO_WriteOnly)) { + QDataStream streamGIF(&transGIF); + // This is a 1px*1px transparent GIF image: + const uchar blankGIF[] = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x0a, 0x00, 0x0a, 0x00, + 0x80, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x21, + 0xfe, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, + 0x77, 0x69, 0x74, 0x68, 0x20, 0x54, 0x68, 0x65, 0x20, 0x47, + 0x49, 0x4d, 0x50, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x0a, 0x00, + 0x01, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x0a, + 0x00, 0x00, 0x02, 0x08, 0x8c, 0x8f, 0xa9, 0xcb, 0xed, 0x0f, + 0x63, 0x2b, 0x00, 0x3b }; + streamGIF.writeRawBytes((const char*)blankGIF, (unsigned int)74); + transGIF.close(); + } + stream << + " <!--[if lt IE 7]>\n" + " <script>\n" + " function fixPng(img) {\n" + " if (!img.style.filter) {\n" + " img.style.filter = \"progid:DXImageTransform.Microsoft.AlphaImageLoader(src='\" + img.src + \"')\";\n" + " img.src = \"" << imagesFolderName << gifFileName << "\";\n" + " }\n" + " }\n" + " for (i = document.images.length - 1; i >= 0; i -= 1) {\n" + " var img = document.images[i];\n" + " if (img.src.substr(img.src.length - 4) == \".png\")\n" + " if (img.complete)\n" + " fixPng(img);\n" + " else\n" + " img.attachEvent(\"onload\", function() { fixPng(window.event.srcElement); });\n" + " }\n" + " </script>\n" + " <![endif]-->\n" + " </body>\n" + "</html>\n"; + + file.close(); + stream.unsetDevice(); + progress->advance(1); // Basket exportation finished + + // Recursively export child baskets: + BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); + if (item->firstChild()) { + for (BasketListViewItem *child = (BasketListViewItem*) item->firstChild(); child; child = (BasketListViewItem*) child->nextSibling()) { + exportBasket(child->basket(), /*isSubBasket=*/true); + } + } +} + +void HTMLExporter::exportNote(Note *note, int indent) +{ + QString spaces; + + if (note->isColumn()) { + QString width = ""; + if (false/*TODO: DEBUG AND REENABLE: hasResizer()*/) { + // As we cannot be precise in CSS (say eg. "width: 50%-40px;"), + // we output a percentage that is approximatively correct. + // For instance, we compute the currently used percentage of width in the basket + // and try make make it the same on a 1024*768 display in a Web browser: + int availableSpaceForColumnsInThisBasket = note->basket()->contentsWidth() - (note->basket()->columnsCount() - 1) * Note::RESIZER_WIDTH; + int availableSpaceForColumnsInBrowser = 1024 /* typical screen width */ + - 25 /* window border and scrollbar width */ + - 2 * 5 /* page margin */ + - (note->basket()->columnsCount() - 1) * Note::RESIZER_WIDTH; + if (availableSpaceForColumnsInThisBasket <= 0) + availableSpaceForColumnsInThisBasket = 1; + int widthValue = (int)(availableSpaceForColumnsInBrowser * (double) note->groupWidth() / availableSpaceForColumnsInThisBasket); + if (widthValue <= 0) + widthValue = 1; + if (widthValue > 100) + widthValue = 100; + width = QString(" width=\"%1%\"").arg(QString::number(widthValue)); + } + stream << spaces.fill(' ', indent) << "<td class=\"column\"" << width << ">\n"; + + // Export child notes: + for (Note *child = note->firstChild(); child; child = child->next()) { + stream << spaces.fill(' ', indent + 1); + exportNote(child, indent + 1); + stream << '\n'; + } + + stream << spaces.fill(' ', indent) << "</td>\n"; + if (note->hasResizer()) + stream << spaces.fill(' ', indent) << "<td class=\"columnHandle\"></td>\n"; + return; + } + + QString freeStyle; + if (note->isFree()) + freeStyle = " style=\"position: absolute; left: " + QString::number(note->x()) + "px; top: " + QString::number(note->y()) + "px; width: " + QString::number(note->groupWidth()) + "px\""; + + if (note->isGroup()) { + stream << '\n' << spaces.fill(' ', indent) << "<table" << freeStyle << ">\n"; // Note content is expected to be on the same HTML line, but NOT groups + int i = 0; + for (Note *child = note->firstChild(); child; child = child->next()) { + stream << spaces.fill(' ', indent); + if (i == 0) + stream << " <tr><td class=\"groupHandle\"><img src=\"" << imagesFolderName << (note->isFolded() ? "expand_group_" : "fold_group_") << backgroundColorName << ".png" + << "\" width=\"" << Note::EXPANDER_WIDTH << "\" height=\"" << Note::EXPANDER_HEIGHT << "\"></td>\n"; + else if (i == 1) + stream << " <tr><td class=\"freeSpace\" rowspan=\"" << note->countDirectChilds() << "\"></td>\n"; + else + stream << " <tr>\n"; + stream << spaces.fill(' ', indent) << " <td>"; + exportNote(child, indent + 3); + stream << "</td>\n" + << spaces.fill(' ', indent) << " </tr>\n"; + ++i; + } + stream << '\n' << spaces.fill(' ', indent) << "</table>\n" /*<< spaces.fill(' ', indent - 1)*/; + } else { + // Additionnal class for the content (link, netword, color...): + QString additionnalClasses = note->content()->cssClass(); + if (!additionnalClasses.isEmpty()) + additionnalClasses = " " + additionnalClasses; + // Assign the style of each associted tags: + for (State::List::Iterator it = note->states().begin(); it != note->states().end(); ++it) + additionnalClasses += " tag_" + (*it)->id(); + //stream << spaces.fill(' ', indent); + stream << "<table class=\"note" << additionnalClasses << "\"" << freeStyle << "><tr>"; + if (note->emblemsCount() > 0) { + stream << "<td class=\"tags\"><nobr>"; + for (State::List::Iterator it = note->states().begin(); it != note->states().end(); ++it) + if (!(*it)->emblem().isEmpty()) { + int emblemSize = 16; + QString iconFileName = copyIcon((*it)->emblem(), emblemSize); + stream << "<img src=\"" << iconsFolderName << iconFileName + << "\" width=\"" << emblemSize << "\" height=\"" << emblemSize + << "\" alt=\"" << (*it)->textEquivalent() << "\" title=\"" << (*it)->fullName() << "\">"; + } + stream << "</nobr></td>"; + } + stream << "<td>"; + note->content()->exportToHTML(this, indent); + stream << "</td></tr></table>"; + } +} + +void HTMLExporter::writeBasketTree(Basket *currentBasket) +{ + stream << " <ul class=\"tree\">\n"; + writeBasketTree(currentBasket, exportedBasket, 3); + stream << " </ul>\n"; +} + +void HTMLExporter::writeBasketTree(Basket *currentBasket, Basket *basket, int indent) +{ + // Compute variable HTML code: + QString spaces; + QString cssClass = (basket == currentBasket ? " class=\"current\"" : ""); + QString link = "#"; + if (currentBasket != basket) { + if (currentBasket == exportedBasket) { + link = basketsFolderName + basket->folderName().left(basket->folderName().length() - 1) + ".html"; + } else if (basket == exportedBasket) { + link = "../../" + fileName; + } else { + link = basket->folderName().left(basket->folderName().length() - 1) + ".html"; + } + } + QString spanStyle = ""; + if (basket->backgroundColorSetting().isValid() || basket->textColorSetting().isValid()) { + spanStyle = " style=\"background-color: " + basket->backgroundColor().name() + "; color: " + basket->textColor().name() + "\""; + } + + // Write the basket tree line: + stream << + spaces.fill(' ', indent) << "<li><a" << cssClass << " href=\"" << link << "\">" + "<span" << spanStyle << " title=\"" << Tools::textToHTMLWithoutP(basket->basketName()) << "\">" + "<img src=\"" << iconsFolderName << copyIcon(basket->icon(), 16) << "\" width=\"16\" height=\"16\" alt=\"\">" << Tools::textToHTMLWithoutP(basket->basketName()) << "</span></a>"; + + // Write the sub-baskets lines & end the current one: + BasketListViewItem *item = Global::bnpView->listViewItemForBasket(basket); + if (item->firstChild() != 0) { + stream << + "\n" << + spaces.fill(' ', indent) << " <ul>\n"; + for (BasketListViewItem *child = (BasketListViewItem*) item->firstChild(); child; child = (BasketListViewItem*) child->nextSibling()) + writeBasketTree(currentBasket, child->basket(), indent + 2); + stream << + spaces.fill(' ', indent) << " </ul>\n" << + spaces.fill(' ', indent) << "</li>\n"; + } else { + stream << "</li>\n"; + } +} + +/** Save an icon to a folder. + * If an icon with the same name already exist in the destination, + * it is assumed the icon is already copied, so no action is took. + * It is optimized so that you can have an empty folder receiving the icons + * and call copyIcon() each time you encounter one during export process. + */ +QString HTMLExporter::copyIcon(const QString &iconName, int size) +{ + if (iconName.isEmpty()) + return ""; + + // Sometimes icon can be "favicons/www.kde.org", we replace the '/' with a '_' + QString fileName = iconName; // QString::replace() isn't const, so I must copy the string before + fileName = "ico" + QString::number(size) + "_" + fileName.replace("/", "_") + ".png"; + QString fullPath = iconsFolderPath + fileName; + if (!QFile::exists(fullPath)) + DesktopIcon(iconName, size).save(fullPath, "PNG"); + return fileName; +} + +/** Done: Sometimes we can call two times copyFile() with the same srcPath and dataFolderPath + * (eg. when exporting basket to HTML with two links to same filename + * (but not necesary same path, as in "/home/foo.txt" and "/foo.txt") ) + * The first copy isn't yet started, so the dest file isn't created and this method + * returns the same filename !!!!!!!!!!!!!!!!!!!! + */ +QString HTMLExporter::copyFile(const QString &srcPath, bool createIt) +{ + QString fileName = Tools::fileNameForNewFile(KURL(srcPath).fileName(), dataFolderPath); + QString fullPath = dataFolderPath + fileName; + + if (createIt) { + // We create the file to be sure another very near call to copyFile() willn't choose the same name: + QFile file(KURL(fullPath).path()); + if (file.open(IO_WriteOnly)) + file.close(); + // And then we copy the file AND overwriting the file we juste created: + new KIO::FileCopyJob( + KURL(srcPath), KURL(fullPath), 0666, /*move=*/false, + /*overwrite=*/true, /*resume=*/true, /*showProgress=*/false ); + } else + /*KIO::CopyJob *copyJob = */KIO::copy(KURL(srcPath), KURL(fullPath)); // Do it as before + + return fileName; +} diff --git a/src/htmlexporter.h b/src/htmlexporter.h new file mode 100644 index 0000000..5b75ab6 --- /dev/null +++ b/src/htmlexporter.h @@ -0,0 +1,79 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef HTMLEXPORTER_H +#define HTMLEXPORTER_H + +#include <qstring.h> +#include <qtextstream.h> + +class KProgress; + +class Basket; +class Note; + +/** + * @author Sébastien Laoût <[email protected]> + */ +class HTMLExporter +{ + public: + HTMLExporter(Basket *basket); + ~HTMLExporter(); + private: + void prepareExport(Basket *basket, const QString &fullPath); + void exportBasket(Basket *basket, bool isSubBasket); + void exportNote(Note *note, int indent); + void writeBasketTree(Basket *currentBasket); + void writeBasketTree(Basket *currentBasket, Basket *basket, int indent); + + public: + QString copyIcon(const QString &iconName, int size); + QString copyFile(const QString &srcPath, bool createIt); + + public: + // Absolute path of the file name the user choosen: + QString filePath; // eg.: "/home/seb/foo.html" + QString fileName; // eg.: "foo.html" + + // Absolute & relative paths for the current basket to be exported: + QString basketFilePath; // eg.: "/home/seb/foo.html" or "/home/seb/foo.html_files/baskets/basketN.html" + QString filesFolderPath; // eg.: "/home/seb/foo.html_files/" + QString filesFolderName; // eg.: "foo.html_files/" or "../" + QString iconsFolderPath; // eg.: "/home/seb/foo.html_files/icons/" + QString iconsFolderName; // eg.: "foo.html_files/icons/" or "../icons/" + QString imagesFolderPath; // eg.: "/home/seb/foo.html_files/images/" + QString imagesFolderName; // eg.: "foo.html_files/images/" or "../images/" + QString dataFolderPath; // eg.: "/home/seb/foo.html_files/data/" or "/home/seb/foo.html_files/baskets/basketN-data/" + QString dataFolderName; // eg.: "foo.html_files/data/" or "basketN-data/" + QString basketsFolderPath; // eg.: "/home/seb/foo.html_files/baskets/" + QString basketsFolderName; // eg.: "foo.html_files/baskets/" or "" + + // Various properties of the currently exporting basket: + QString backgroundColorName; + + // Variables used by every export methods: + QTextStream stream; + Basket *exportedBasket; + bool withBasketTree; + KProgress *progress; +}; + +#endif // HTMLEXPORTER_H diff --git a/src/kcm_basket.cpp b/src/kcm_basket.cpp new file mode 100644 index 0000000..5c1efa6 --- /dev/null +++ b/src/kcm_basket.cpp @@ -0,0 +1,72 @@ +/*************************************************************************** + * Copyright (C) 2006 by Petri Damsten * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +// This must be first +#include <config.h> +#include "settings.h" +#include <kcmodule.h> + +//---------------------------- +// KCM stuff +//---------------------------- +extern "C" +{ + KDE_EXPORT KCModule *create_basket_config_general(QWidget *parent, const char *) + { + GeneralPage *page = new GeneralPage(parent, "kcmbasket_config_general"); + return page; + } +} + +extern "C" +{ + KDE_EXPORT KCModule *create_basket_config_baskets(QWidget *parent, const char *) + { + BasketsPage *page = new BasketsPage(parent, "kcmbasket_config_baskets"); + return page; + } +} + +extern "C" +{ + KDE_EXPORT KCModule *create_basket_config_new_notes(QWidget *parent, const char *) + { + NewNotesPage *page = new NewNotesPage(parent, "kcmbasket_config_new_notes"); + return page; + } +} + +extern "C" +{ + KDE_EXPORT KCModule *create_basket_config_notes_appearance(QWidget *parent, const char *) + { + NotesAppearancePage *page = new NotesAppearancePage(parent, "kcmbasket_config_notes_appearance"); + return page; + } +} + +extern "C" +{ + KDE_EXPORT KCModule *create_basket_config_apps(QWidget *parent, const char *) + { + ApplicationsPage *page = new ApplicationsPage(parent, "kcmbasket_config_apps"); + return page; + } +} diff --git a/src/kcolorcombo2.cpp b/src/kcolorcombo2.cpp new file mode 100644 index 0000000..b5ac1e3 --- /dev/null +++ b/src/kcolorcombo2.cpp @@ -0,0 +1,769 @@ +/*************************************************************************** + * Copyright (C) 2005 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qapplication.h> +#include <qpixmap.h> +#include <qbitmap.h> +#include <qpainter.h> +#include <qlistbox.h> +#include <kglobalsettings.h> +#include <klocale.h> +#include <kcolordialog.h> +#include <qclipboard.h> +#include <kstdaccel.h> +#include <kcolordrag.h> + +#include "kcolorcombo2.h" + +//#include "qeffects.h" + +//#define DEBUG_COLOR_ARRAY +//#define OUTPUT_GIMP_PALETTE + +#ifdef DEBUG_COLOR_ARRAY + #include <iostream> + #include <iomanip> +#endif +#ifdef OUTPUT_GIMP_PALETTE + #include <iostream> + #include <iomanip> +#endif + +/** class KColorPopup: */ + +const int KColorPopup::MARGIN = 1; +const int KColorPopup::FRAME_WIDTH = 1; + + +KColorPopup::KColorPopup(KColorCombo2 *parent) + : QWidget(/*parent=*/0, /*name=*/0, WType_Popup | WNoAutoErase), + m_selector(parent) +{ + hide(); + setMouseTracking(true); + //resize(20, 20); + //setWFlags(Qt::WNoAutoErase); +} + +KColorPopup::~KColorPopup() +{ +} + +#include <qcursor.h> + +void KColorPopup::relayout() // FIXME: relayout should NOT redraw the pixmap! +{ + int columnCount = m_selector->columnCount(); + int rowCount = m_selector->rowCount(); + int colorHeight = m_selector->colorRectHeight(); + int colorWidth = m_selector->colorRectWidthForHeight(colorHeight); + bool haveDefault = m_selector->defaultColor().isValid(); + + int width = 2 + MARGIN + (colorWidth + MARGIN) * columnCount; + int height = 2 + MARGIN + (colorHeight + MARGIN) * rowCount + (colorHeight + MARGIN); + + resize(width, height); + + // Initialize the pixmap: + m_pixmap.resize(width, height); + QPainter painter(&m_pixmap); + painter.fillRect(0, 0, width, height, KGlobalSettings::baseColor()); + painter.setPen(KGlobalSettings::textColor()); + painter.drawRect(0, 0, width, height); + + // Needed to draw: + int x, y; + QRect selectionRect; + + // Draw the color array: + for (int i = 0; i < columnCount; ++i) { + for (int j = 0; j < rowCount; ++j) { + x = 1 + MARGIN + (colorWidth + MARGIN) * i; + y = 1 + MARGIN + (colorHeight + MARGIN) * j; + if (i == m_selectedColumn && j == m_selectedRow) { + selectionRect = QRect(x - 2, y - 2, colorWidth + 4, colorHeight + 4); + painter.fillRect(selectionRect, KGlobalSettings::highlightColor()); + } + m_selector->drawColorRect(painter, x, y, m_selector->colorAt(i, j), /*isDefault=*/false, colorWidth, colorHeight); + } + } + + m_columnOther = (haveDefault ? columnCount / 2 : 0); // "(Default)" is allowed, paint "Other..." on the right + int defaultCellWidth = (colorWidth + MARGIN) * m_columnOther; + int otherCellWidth = (colorWidth + MARGIN) * (columnCount - m_columnOther); + + // Draw the "(Default)" and "Other..." colors: + y = height - (colorHeight + MARGIN) - 1; + QColor textColor; + if (m_selector->defaultColor().isValid()) { + x = 1 + MARGIN; + if (m_selectedColumn < m_columnOther && rowCount == m_selectedRow) { + selectionRect = QRect(x - 2, y - 2, defaultCellWidth, colorHeight + 4); + painter.fillRect(selectionRect, KGlobalSettings::highlightColor()); + textColor = KGlobalSettings::highlightedTextColor(); + } else + textColor = KGlobalSettings::textColor(); + m_selector->drawColorRect(painter, x, y, m_selector->defaultColor(), /*isDefault=*/true, colorWidth, colorHeight); + painter.setFont(m_selector->font()); + painter.setPen(textColor); + painter.drawText(x + 2 + colorWidth, y, /*width=*/5000, colorHeight, AlignLeft | AlignVCenter | DontClip, i18n("(Default)")); + } + x = 1 + MARGIN + m_columnOther * (colorWidth + MARGIN); + if (m_selectedColumn >= m_columnOther && rowCount == m_selectedRow) { + selectionRect = QRect(x - 2, y - 2, otherCellWidth, colorHeight + 4); + painter.fillRect(selectionRect, KGlobalSettings::highlightColor()); + textColor = KGlobalSettings::highlightedTextColor(); + } else + textColor = KGlobalSettings::textColor(); + m_selector->drawColorRect(painter, x, y, m_otherColor, /*isDefault=*/false, colorWidth, colorHeight); + painter.setFont(m_selector->font()); + painter.setPen(textColor); + painter.drawText(x + 2 + colorWidth, y, /*width=*/5000, colorHeight, AlignLeft | AlignVCenter | DontClip, i18n("Other...")); + +// QPoint pos = mapFromGlobal(QCursor::pos()); +// painter.drawRect(pos.x(), pos.y(), 5000, 5000); +} + +void KColorPopup::updateCell(int column, int row) +{ + int colorHeight = m_selector->colorRectHeight(); + int colorWidth = m_selector->colorRectWidthForHeight(colorHeight); + + int x = 1 + MARGIN + - 2 + column * (colorWidth + MARGIN); + int y = 1 + MARGIN + - 2 + row * (colorHeight + MARGIN); + int width = colorWidth + MARGIN; + int height = colorHeight + MARGIN; + + if (row == m_selector->rowCount()) { + if (m_selectedColumn < m_columnOther) // The "(Default)" cell: + width = (colorWidth + MARGIN) * m_columnOther; + else // The "Other..." cell: + width = (colorWidth + MARGIN) * (m_selector->columnCount() - m_columnOther); + } + + update(x, y, width, height); +} + +void KColorPopup::doSelection() +{ + m_otherColor = QColor(); + + // If the selected color is not the default one, try to find it in the array: + if (m_selector->color().isValid()) { + bool isInArray = false; + for (int column = 0; column < m_selector->columnCount(); ++column) + for (int row = 0; row < m_selector->rowCount(); ++row) + if (m_selector->color() == m_selector->colorAt(column, row)) { + m_selectedColumn = column; + m_selectedRow = row; + isInArray = true; + } + // If not found in array, it's another one: + if (!isInArray) { + m_selectedColumn = m_columnOther; + m_selectedRow = m_selector->rowCount(); + m_otherColor = m_selector->color(); + } + // If it's the default one: + } else { + m_selectedColumn = 0; + m_selectedRow = m_selector->rowCount(); + } +} + +void KColorPopup::validate() +{ + hide(); + close(); + + if (m_selectedRow != m_selector->rowCount()) // A normal row: + m_selector->setColor(m_selector->colorAt(m_selectedColumn, m_selectedRow)); + else if (m_selectedColumn < m_columnOther) // The default color: + m_selector->setColor(QColor()); + else { // The user want to choose one: + QColor color = m_selector->effectiveColor(); + if (KColorDialog::getColor(color, this) == QDialog::Accepted) + m_selector->setColor(color); + } +} + +void KColorPopup::mousePressEvent(QMouseEvent *event) +{ + int x = event->pos().x(); + int y = event->pos().y(); + if (x < 0 || y < 0 || x >= width() || y >= height()) { + hide(); + close(); + } else + validate(); + + event->accept(); +} + +void KColorPopup::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.drawPixmap(0, 0, m_pixmap); + painter.setPen(Qt::black); + painter.drawRect(event->rect()); +} + +void KColorPopup::mouseMoveEvent(QMouseEvent *event) +{ + int x = event->pos().x(); + int y = event->pos().y(); + if (x < FRAME_WIDTH + 2 || y < FRAME_WIDTH + 2 || x > width() - 2 - 2*FRAME_WIDTH || y > height() - 2 - 2*FRAME_WIDTH) + return; + + int colorHeight = m_selector->colorRectHeight(); + int colorWidth = m_selector->colorRectWidthForHeight(colorHeight); + +// int oldSelectedColumn = m_selectedColumn; +// int oldSelectedRow = m_selectedRow; + m_selectedColumn = (x - FRAME_WIDTH - MARGIN + 2) / (colorWidth + MARGIN); + m_selectedRow = (y - FRAME_WIDTH - MARGIN + 2) / (colorHeight + MARGIN); + + relayout(); + update(); +} + +void KColorPopup::keyPressEvent(QKeyEvent *event) +{ + int column = m_selectedColumn; + int row = m_selectedRow; + int columnCount = m_selector->columnCount(); + int rowCount = m_selector->rowCount(); + + switch (event->key()) { + case Qt::Key_Right: + if (m_selectedRow != rowCount) // A normal row: + column = (column + 1) % columnCount; + else { + // The last row, if there are two choices, switch. Else, do nothing: + if (m_selector->defaultColor().isValid()) + column = (m_selectedColumn < m_columnOther ? m_columnOther : 0); + } + break; + case Qt::Key_Left: + if (m_selectedRow != rowCount) { // A normal row: + column = (column - 1); + if (column < 0) + column = columnCount - 1; + } else { + // The last row, if there are two choices, switch. Else, do nothing: + if (m_selector->defaultColor().isValid()) + column = (m_selectedColumn < m_columnOther ? m_columnOther : 0); + } + break; + case Qt::Key_Up: row = (row - 1); if (row < 0) row = rowCount; break; + case Qt::Key_Down: row = (row + 1) % (rowCount+1); break; + case Qt::Key_PageDown: row += 10; if (row > rowCount) row = rowCount; break; + case Qt::Key_PageUp: row -= 10; if (row < 0) row = 0; break; + case Qt::Key_Home: row = 0; column = 0; break; + case Qt::Key_End: row = rowCount; column = columnCount - 1; break; + case Qt::Key_Return: + validate(); + break; + default: + QWidget::keyPressEvent(event); + } + + if (row != m_selectedRow || column != m_selectedColumn) { + m_selectedRow = row; + m_selectedColumn = column; + relayout(); + update(); + } +} + +/** Helper function: */ + +QColor Tool_mixColors(const QColor &color1, const QColor &color2) +{ + QColor mixedColor; + mixedColor.setRgb( (color1.red() + color2.red()) / 2, + (color1.green() + color2.green()) / 2, + (color1.blue() + color2.blue()) / 2 ); + return mixedColor; +} + +/** class KColorCombo2Private */ + +class KColorCombo2::KColorCombo2Private +{ +}; + +/** class KColorCombo2: */ + +/* All code for the popup management (including the constructor, popup() and eventFilter()) + * has been copied from the KDateEdit widget (in libkdepim). + * + * Some other piece of code comes from KColorButton (in libkdeui) to enable color drag, drop, copy and paste. + */ + +KColorCombo2::KColorCombo2(const QColor &color, const QColor &defaultColor, QWidget *parent, const char *name) + : QComboBox(/*editable=*/false, parent, name), + m_color(color), m_defaultColor(defaultColor) +{ + init(); +} + +KColorCombo2::KColorCombo2(const QColor &color, QWidget *parent, const char *name) + : QComboBox(/*editable=*/false, parent, name), + m_color(color), m_defaultColor() +{ + init(); +} + +void KColorCombo2::init() +{ + m_discardNextMousePress = false; + m_colorArray = 0; + d = new KColorCombo2Private(); + + setDefaultColor(m_defaultColor); + insertItem("", /*index=*/0); + updateComboBox(); // It need an item of index 0 to exists, so we created it. + setAcceptDrops(true); + + m_popup = new KColorPopup(this); + m_popup->installEventFilter(this); + + // By default, the array is filled with setRainbowPreset(). + // But we allocate it on demand (the later as possible) to avoid performances issues if the developer set another array. + // However, to keep columnCount() rowCount() const, we define theme here: + m_columnCount = 13; + m_rowCount = 9; +} + +KColorCombo2::~KColorCombo2() +{ + deleteColorArray(); +} + +void KColorCombo2::setColor(const QColor &color) +{ + // Do nothing if the color should be set to the default one and there is no such default color allowed: + if (!color.isValid() && !m_defaultColor.isValid()) { + // kdebug << this::FUNCTION << "Trying to assign the default color (an invalid one) whereas no such default color is allowed"; + return; + } + + if (m_color != color) { + m_color = color; + updateComboBox(); + emit changed(color); + } +} + +QColor KColorCombo2::color() const +{ + return m_color; +} + +QColor KColorCombo2::effectiveColor() const +{ + if (m_color.isValid()) + return m_color; + else + return m_defaultColor; +} + +void KColorCombo2::setRainbowPreset(int colorColumnCount, int lightRowCount, int darkRowCount, bool withGray) +{ + // At least one row and one column: + if (colorColumnCount < 1 - (withGray ? 1 : 0)) + colorColumnCount = 1 - (withGray ? 1 : 0); + if (lightRowCount < 0) + lightRowCount = 0; + if (darkRowCount < 0) + darkRowCount = 0; + + // Create the array: + int columnCount = colorColumnCount + (withGray ? 1 : 0); + int rowCount = lightRowCount + 1 + darkRowCount; + newColorArray(columnCount, rowCount); + + // Fill the array: + for (int i = 0; i < colorColumnCount; ++i) { + int hue = i * 360 / colorColumnCount; + // With light colors: + for (int j = 1; j <= lightRowCount; ++j) { // Start to 1 because we don't want a row full of white! + int saturation = j * 255 / (lightRowCount + 1); + setColorAt(i, j - 1, QColor(hue, saturation, 255, QColor::Hsv)); + } + // With pure colors: + setColorAt(i, lightRowCount, QColor(hue, 255, 255, QColor::Hsv)); + // With dark colors: + for (int j = 1; j <= darkRowCount; ++j) { + int value = 255 - j * 255 / (darkRowCount + 1); + setColorAt(i, lightRowCount + j, QColor(hue, 255, value, QColor::Hsv)); + } + } + + // Fill the gray column: + if (withGray) { + for (int i = 0; i < rowCount; ++i) { + int gray = ( rowCount == 1 ? 128 : 255 - (i * 255 / (rowCount - 1)) ); + setColorAt(columnCount-1, i, QColor(gray, gray, gray)); + } + } + +#ifdef DEBUG_COLOR_ARRAY + std::cout << "KColorCombo2::setColorPreset" << std::endl; + for (int j = 0; j < rowCount; ++j) { + for (int i = 0; i < columnCount; ++i) { + int h, s, v; + m_colorArray[i][j].getHsv(h, s, v); + std::cout << "(" << std::setw(3) << h << "," << std::setw(3) << s << "," << std::setw(3) << v << ") "; + //std::cout << colorArray[i][j].name() << " "; + } + std::cout << std::endl; + } +#endif +#ifdef OUTPUT_GIMP_PALETTE + std::cout << "GIMP Palette" << std::endl; + for (int j = 0; j < rowCount; ++j) { + for (int i = 0; i < columnCount; ++i) { + std::cout << std::setw(3) << m_colorArray[i][j].red() << ", " << std::setw(3) << m_colorArray[i][j].green() << ", " << std::setw(3) << m_colorArray[i][j].blue() << std::endl; + } + } +#endif +} + +int KColorCombo2::columnCount() const +{ + return m_columnCount; +} + +int KColorCombo2::rowCount() const +{ + return m_rowCount; +} + +QColor KColorCombo2::colorAt(int column, int row)/* const*/ +{ + if (!m_colorArray) + setRainbowPreset(); + + if (column < 0 || row < 0 || column >= m_columnCount || row >= m_rowCount) + return QColor(); + + return m_colorArray[column][row]; +} + +QColor KColorCombo2::defaultColor() const +{ + return m_defaultColor; +} + +void KColorCombo2::newColorArray(int columnCount, int rowCount) +{ + if (columnCount <= 0 || rowCount <= 0) { + // kdebug << this::FUNCTION << "Trying to create an empty new color array (with %d columns and %d rows)"; + return; + } + + // Delete any previous array (if any): + deleteColorArray(); + + // Create a new array of the wanted dimentions: + m_columnCount = columnCount; + m_rowCount = rowCount; + m_colorArray = new QColor* [columnCount]; + for (int i = 0; i < columnCount; ++i) + m_colorArray[i] = new QColor[rowCount]; + + m_popup->relayout(); +} + +void KColorCombo2::setColorAt(int column, int row, const QColor &color) +{ + if (!m_colorArray) + setRainbowPreset(); + + if (column < 0 || row < 0 || column >= m_columnCount || row >= m_rowCount) { + // kdebug << this::FUNCTION << "Trying to set a color at an invalid index (at column %d and row %d, whereas the array have %d columns and %d rows)"; + return; + } + + m_colorArray[column][row] = color; +} + +void KColorCombo2::setDefaultColor(const QColor &color) +{ + m_defaultColor = color; + if (!m_defaultColor.isValid() && !m_color.isValid()) + m_color = Qt::white; // FIXME: Use the first one. +} + +QPixmap KColorCombo2::colorRectPixmap(const QColor &color, bool isDefault, int width, int height) +{ + // Prepare to draw: + QPixmap pixmap(width, height); + QBitmap mask(width, height); + QPainter painter(&pixmap); + QPainter maskPainter(&mask); + + // Draw pixmap: + drawColorRect(painter, 0, 0, color, isDefault, width, height); + + // Draw mask (make the four corners transparent): + maskPainter.fillRect(0, 0, width, height, Qt::color1); // opaque + maskPainter.setPen(Qt::color0); // transparent + maskPainter.drawPoint(0, 0); + maskPainter.drawPoint(0, height - 1); + maskPainter.drawPoint(width - 1, height - 1); + maskPainter.drawPoint(width - 1, 0); + + // Finish: + painter.end(); + maskPainter.end(); + pixmap.setMask(mask); + return pixmap; +} + +void KColorCombo2::drawColorRect(QPainter &painter, int x, int y, const QColor &color, bool isDefault, int width, int height) +{ + // Fill: + if (color.isValid()) + painter.fillRect(x /*+ 1*/, y /*+ 1*/, width /*- 2*/, height /*- 2*/, color); + else { + // If it's an invalid color, it's for the "Other..." entry: draw a rainbow. + // If it wasn't for the "Other..." entry, the programmer made a fault, so (s)he will be informed about that visually. + for (int i = 0; i < width-2; ++i) { + int hue = i * 360 / (width-2); + for (int j = 0; j < height-2; ++j) { + int saturation = 255 - (j * 255 / (height-2)); + painter.setPen( QColor(hue, saturation, /*value=*/255, QColor::Hsv) ); + painter.drawPoint(x + i + 1, y + j + 1); + } + } + } + + // Stroke: + int dontCare, value; + color.getHsv(/*hue:*/dontCare, /*saturation:*/dontCare, value); + QColor stroke = (color.isValid() ? color.dark(125) : KGlobalSettings::textColor()); + painter.setPen(/*color);//*/stroke); + painter.drawLine(x + 1, y, x + width - 2, y); + painter.drawLine(x, y + 1, x, y + height - 2); + painter.drawLine(x + 1, y + height - 1, x + width - 2, y + height - 1); + painter.drawLine(x + width - 1, y + 1, x + width - 1, y + height - 2); + + // Round corners: + QColor antialiasing; + if (color.isValid()) { + antialiasing = Tool_mixColors(color, stroke); + painter.setPen(antialiasing); + painter.drawPoint(x + 1, y + 1); + painter.drawPoint(x + 1, y + height - 2); + painter.drawPoint(x + width - 2, y + height - 2); + painter.drawPoint(x + width - 2, y + 1); + } else { + // The two top corners: + antialiasing = Tool_mixColors(Qt::red, stroke); + painter.setPen(antialiasing); + painter.drawPoint(x + 1, y + 1); + painter.drawPoint(x + width - 2, y + 1); + // The two bottom ones: + antialiasing = Tool_mixColors(Qt::white, stroke); + painter.setPen(antialiasing); + painter.drawPoint(x + 1, y + height - 2); + painter.drawPoint(x + width - 2, y + height - 2); + } + + // Mark default color: + if (isDefault) { + painter.setPen(stroke); + painter.drawLine(x + 1, y + height - 2, x + width - 2, y + 1); + } +} + +int KColorCombo2::colorRectHeight() const +{ + return (fontMetrics().boundingRect(i18n("(Default)")).height() + 2)*3/2; +} + +int KColorCombo2::colorRectWidthForHeight(int height) const +{ + return height * 14 / 10; // 1.4 times the height, like A4 papers. +} + +void KColorCombo2::deleteColorArray() +{ + if (m_colorArray) { + for (int i = 0; i < m_columnCount; ++i) + delete[] m_colorArray[i]; + delete[] m_colorArray; + m_colorArray = 0; + } +} + +void KColorCombo2::updateComboBox() +{ + int height = colorRectHeight()*2/3; // fontMetrics().boundingRect(i18n("(Default)")).height() + 2 + QPixmap pixmap = colorRectPixmap(effectiveColor(), !m_color.isValid(), colorRectWidthForHeight(height), height); // TODO: isDefaultColorSelected() + changeItem(pixmap, (m_color.isValid() ? "" : i18n("(Default)")), /*index=*/0); +} + +void KColorCombo2::popup() +{ + if (!m_colorArray) + setRainbowPreset(); + + // Compute where to show the popup: + QRect desk = KGlobalSettings::desktopGeometry(this); + + QPoint popupPoint = mapToGlobal(QPoint(0, 0)); + + int popupHeight = m_popup->sizeHint().height(); + if (popupPoint.y() + height() + popupHeight > desk.bottom()) + popupPoint.setY(popupPoint.y() - popupHeight); + else + popupPoint.setY(popupPoint.y() + height()); + + int popupWidth = m_popup->sizeHint().width(); + if (popupPoint.x() + popupWidth > desk.right()) + popupPoint.setX(desk.right() - popupWidth); + + if (popupPoint.x() < desk.left()) + popupPoint.setX(desk.left()); + + if (popupPoint.y() < desk.top()) + popupPoint.setY(desk.top()); + + // Configure the popup: + m_popup->move(popupPoint); + //m_popup->setColor(m_color); + m_popup->doSelection(); + m_popup->relayout(); // FIXME: In aboutToShow() ? +#if 0 +//#ifndef QT_NO_EFFECTS + if (QApplication::isEffectEnabled(UI_AnimateCombo)) { + if (m_popup->y() < mapToGlobal(QPoint(0,0)).y()) + qScrollEffect(m_popup, QEffects::UpScroll); + else + qScrollEffect(m_popup); + } else +#endif + m_popup->show(); + + // The combo box is now shown pressed. Make it show not pressed again + // by causing its (invisible) list box to emit a 'selected' signal. + // Simulate an Enter to unpress it: + QListBox *lb = listBox(); + if (lb) { + lb->setCurrentItem(0); + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Enter, 0, 0); + QApplication::postEvent(lb, keyEvent); + } +} + +bool KColorCombo2::eventFilter(QObject */*object*/, QEvent *event) +{ + QMouseEvent *mouseEvent; + + switch (event->type()) { + case QEvent::MouseButtonDblClick: + case QEvent::MouseButtonPress: + mouseEvent = (QMouseEvent*)event; + if ( !m_popup->rect().contains(mouseEvent->pos()) ) { + QPoint globalPos = m_popup->mapToGlobal(mouseEvent->pos()); + if (QApplication::widgetAt(globalPos, /*child=*/true) == this) { + // The popup is being closed by a click on the KColorCombo2 widget. + // Avoid popping it up again immediately: + m_discardNextMousePress = true; + } + } + break; + default: + break; + } + + // Don't stop the event being handled further: + return false; +} + +void KColorCombo2::mousePressEvent(QMouseEvent *event) +{ + m_dragStartPos = event->pos(); + + if (event->button() == Qt::LeftButton && m_discardNextMousePress) + m_discardNextMousePress = false; + else + QComboBox::mousePressEvent(event); +} + +void KColorCombo2::mouseMoveEvent(QMouseEvent *event) +{ + if( (event->state() & Qt::LeftButton) && + (event->pos() - m_dragStartPos).manhattanLength() > KGlobalSettings::dndEventDelay() ) { + // Drag color object: + KColorDrag *colorDrag = new KColorDrag(effectiveColor(), this); + // Replace the drag pixmap with our own rounded one, at the same position and dimetions: + QPixmap pixmap = colorDrag->pixmap(); + pixmap = colorRectPixmap(effectiveColor(), /*isDefault=*/false, pixmap.width(), pixmap.height()); + colorDrag->setPixmap(pixmap, colorDrag->pixmapHotSpot()); + colorDrag->dragCopy(); + //setDown(false); + } +} + +void KColorCombo2::dragEnterEvent(QDragEnterEvent *event) +{ + event->accept(isEnabled() && KColorDrag::canDecode(event)); +} + +void KColorCombo2::dropEvent(QDropEvent *event) +{ + QColor color; + if (KColorDrag::decode(event, color)) + setColor(color); +} + +void KColorCombo2::keyPressEvent(QKeyEvent *event) +{ + KKey key(event); + + if (KStdAccel::copy().contains(key)) { + QMimeSource *mime = new KColorDrag(effectiveColor()); + QApplication::clipboard()->setData(mime, QClipboard::Clipboard); + } else if (KStdAccel::paste().contains(key)) { + QColor color; + KColorDrag::decode(QApplication::clipboard()->data(QClipboard::Clipboard), color); + setColor(color); + } else + QComboBox::keyPressEvent(event); +} + +void KColorCombo2::fontChange(const QFont &oldFont) +{ + // Since the color-rectangle is the same height of the text, we should resize it if the font change: + updateComboBox(); + QComboBox::fontChange(oldFont); // To update geometry. +} + +void KColorCombo2::virtual_hook(int /*id*/, void */*data*/) +{ + /* KBASE::virtual_hook(id, data); */ +} + +#include "kcolorcombo2.moc" diff --git a/src/kcolorcombo2.h b/src/kcolorcombo2.h new file mode 100644 index 0000000..ec264fc --- /dev/null +++ b/src/kcolorcombo2.h @@ -0,0 +1,343 @@ +/*************************************************************************** + * Copyright (C) 2005 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef KCOLORCOMBO2_H +#define KCOLORCOMBO2_H + +#include <qcombobox.h> +#include <qcolor.h> +#include <qpixmap.h> + +class KColorPopup; + +/** + * @short A combobox to display or allow user selection of a color in a user-friendly way. + * + * A combobox widget that popup an array of colors for the user to easily pick a common color.\n + * He/she can use the popup to quickly pick a reasonable color or open a color chooser dialog for a more precise choice.\n + * The user can also choose a default color (the standard background color, text color, etc... it's to the programmer to make sense of this property).\n + * \n + * The user is also offered some facilities: like KColorButton he/she can copy a color or paste it + * (with standard keyboard shortcuts, usually Ctrl+C and Ctrl+V), and he/she can drag or drop colors. + * + * @par Quick usage: + * Just create a new KColorCombo2() with the initial color and eventually an allowed default color + * (eg. KGlobalSettings::baseColor() for a background color, KGlobalSettings::textColor()...).\n + * You will be noticed of the color the user selects with the signal changed(), or you can use color() to get the color at any moment.\n + * Note that they can return an invalid color (see QColor::isValid()) if the user chosen the default color (if he can choose that).\n + * It's then easy to save in settings, but if you want the real color (even for the default), you can get it with effectiveColor(). + * + * @par Notes about default color: + * If you set a default color using Qt or KDE standard colors, the user can change them in the KDE Control Center, + * but this widget willn't be update and will still show the old one.\n + * To be noticed of such color change and then update the widget with the new standard color, you can use one of those two methods: + * @code + * void QWidgetDerivate::paletteChange(const QPalette &oldPalette) { // QWidgetDerivate is a parent or near custom widget + * theComboBox->setDefaultColor(theNewDefaultColor); + * QWidget::paletteChange(oldPalette); + * } + * @endcode + * or connect the signal KApplication::kdisplayPaletteChanged() to a slot that will set the default color of this widget. + * + * @par Advanced usage: + * By default, the combobox show a well balanced rainbow, OK for most usages, and you don't need to do anything for it to work.\n + * You however can set your own color array by calling newColorArray() with the number of columns and rows. + * Then, setColorAt() several times to fill the array.\n + * This allow the most flexibility. But if you just want a rainbow with more or less colors, setRainbowPreset() is what you want.\n + * If you worry about performance issues of creating a combobox with the default color array and then allocating another color array by yourself, + * note that the default color array is not allocated in the constructor, but as soon as it is demanded (on first popup if no array has been + * set before, or on first call of any accessors: colorAt(), columnCount(), setColorAt()...). + * Finally, colorRectPixmap() and drawColorRect() allow to draw the color rounded-rectangle in other places for a consistent look. + * + * @see KGlobalSettings Use one of the static functions to get KDE standard colors for default values. + * @see KColorButton The same, but without the rainbow popup or the choice of a default color. + * @see KColorDialog The dialog that is shown when the user click the "Other..." entry. + * @author S�bastien Lao�t <[email protected]> + * + * @image html commoncolorselector.png "Common Color Selector ComboBox" + */ +class KColorCombo2 : public QComboBox +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor) + Q_PROPERTY(QColor defaultColor READ defaultColor WRITE setDefaultColor) + + public slots: + /** + * Change the selected color.\n + * If the popup is open, it will not reflect the change. FIXME: Should it? + * @param color The new selected color. Can be invalid to select the default one.\n + * If @p color is invalid and no default color is allowed, the function will keep the old one. + */ + void setColor(const QColor &color); + + /** + * Change the default color. + * @param color The color to return if the user choose the default one. If it is not valid, the user willn't be allowed to choose a default one. + * @see defaultColor() to get it. + */ + void setDefaultColor(const QColor &color); + + signals: + /** + * Emitted when the color of the widget is changed, either with setColor() or via user selection. + * @see color() to know the content of @p newColor. + */ + void changed(const QColor &newColor); + + public: + /** + * Constructs a color combobox with parent @p parent called @p name. + * @param color The initial selected color. If it is not valid, the default one will then be selected.\n + * But if @p color is invalid and there is no default color, the result is undefined. + * @param defaultColor The color to return if the user choose the default one. If it is not valid, the user willn't be allowed to choose a default one. + */ + KColorCombo2(const QColor &color, const QColor &defaultColor, QWidget *parent = 0, const char *name = 0); + + /** + * Constructs a color combobox with parent @p parent called @p name.\n + * The user is not allowed to choose a default color, unless you call setDefaultColor() later. + * @param color The initial selected color. If it is invalid, the result is undefined. + */ + KColorCombo2(const QColor &color, QWidget *parent = 0L, const char *name = 0L); + + /** + * Destroys the combobox. + */ + virtual ~KColorCombo2(); + + /** + * Get the color chosen by the user.\n + * Can be invalid, if the user chosen the default one.\n + * Ideal to store it in settings for later recall. + * @see effectiveColor() if you want the color to be always valid. + */ + QColor color() const; + + /** + * Return the color chosen by the user.\n + * If the user chosen the default color, the default one is then returned, so the returned color is always valid.\n + * Ideal to directly use to draw. + * @see color() if you want to be notified of a default color choice. + */ + QColor effectiveColor() const; + + /** + * Returns the default color or an invalid color if no default color is set (if the user isn't allowed to choose a default color). + * @see setDefaultColor() to change it. + */ + QColor defaultColor() const; + + /** + * Allocate a new color array of the specified dimention.\n + * The new array will have invalid colors: you should then assign them one by one.\n + * If one or both of the dimentions are negative or null, this function do nothing (both dimentions are always ensured to be at least equal to 1). + * @param columnCount The number of columns of the array. + * @param rowCount The number of rows of the array. + * @see setColorAt() to set all colors once the array have been created. + */ + void newColorArray(int columnCount, int rowCount); + + /** + * Get the number of columns in the array that the user can see to choose. + * @see rowCount() for the number of rows, and colorAt() to get a color from the array. + */ + int columnCount() const; + + /** + * Get the number of rows in the array that the user can see to choose. + * @see columnCount() for the number of columns, and colorAt() to get a color from the array. + */ + int rowCount() const; + + /** + * Set a color in the array at position (column,row).\n + * If one or both of the indexes are out of range, this function do nothing.\n + * @p column and @p row start from 0 to columnCount()-1 and columnRow()-1. + * + * @param column The x coordinate of the color to set or change. + * @param row The y coordinate of the color to set or change. + * @param color The color to assign at this position. + */ + void setColorAt(int column, int row, const QColor &color); + + /** + * Get a color in the array that the user can see to choose.\n + * @p column and @p row start from 0 to columnCount()-1 and columnRow()-1. + * + * @return The asked color, or an invalid color if the index is out of limit of the array. + * @see columnCount() and rowCount() to get the array dimentions. + */ + QColor colorAt(int column, int row)/* const*/; + + /** + * Fill the array of colors (that will be shown to the user in the popup that appears when he/she click the arrow) with a rainbow of different luminosity.\n + * This rainbow representation have the advantage of being natural and well structured for a human to be able to select reasonable colors.\n + * This function will allocate a color array by itself depending on the parameters (no need to call newColorArray()). + * @param colorColumnCount The number of columns. The 360 possible colors of the rainbow will be splitted to take the wanted number of colors, equaly separated. + * @param lightRowCount There is always at least 1 row of colors: the "pure" colors: pure red, pure blue, pure green, pure fushia...\n + * Additionnaly, you can add row on top: they will contain the same colors, but lighter.\n + * The parameter @p lightRowCount specify how many different lighting grades shoud be shown (from near to white, but not white, to "pure"). + * @param darkRowCount Finally, on bottom of the row of "pure colors", you can put any variety of dark colors (from "pure", to near to black, but not black).\n + * So, the number of rows is equal to @p lightRowCount + 1 + @p darkRowCount. On top are light colors, gradually going to dark ones on bottom. + * @param withGray If true, another column (so there will be @p colorColumnCount+1 columns) is added on the very-right of the popup + * to show different gray values, matching the brightness of the sibling colors. + * + * The most acceptable parameters: + * @li The default values are good to have the 7 colors of the rainbow + colors between them, and light/dark colors are well distinct. + * @li If the color is a background color, you can set @p darkRowCount to 0, so only light colors are shown. + * @li The inverse is true for text color choice: you can set @p lightRowCount to 0. + * @li But be careful: some advanced users prefer white text on dark background, so you eg. can set @p lightRowCount to a big value and + * @p darkRowCount to a small one for a fewer choice of dark colors, but at least some ones. + */ + void setRainbowPreset(int colorColumnCount = 12, int lightRowCount = 4, int darkRowCount = 4, bool withGray = true); + //void setHsvPreset(QColor hue[], QColor saturation[], QColor value[], bool withGray = true); + + /** + * Returns a pixmap of a colored rounded-rectangle. The four corners are transparent.\n + * Useful if you want to set such a rectangle as an icon for a menu entry, or for drag and drop operation... + * @param color The color of the rectangle. If the color is invalid, a rainbow is then drawn (like for the "Other..." entry in the popup). + * @param isDefault True if @p color is the default one and should then be draw with a diagonal line. + * @param width The width of the rectangle pixmap to return. + * @param height The height of the rectangle pixmap to return. + * + * @see drawColorRect() if you need to draw it directly: it's faster. + */ + static QPixmap colorRectPixmap(const QColor &color, bool isDefault, int width, int height); + + /** + * Draw an image of a colored rounded-rectangle.\n + * This is like colorRectPixmap() but significantly faster because there is nothing to copy, and no transparency mask to create and apply. + * @param painter The painter where to draw the image. + * @param x The x coordinate on the @p painter where to draw the rectangle. + * @param y The y coordinate on the @p painter where to draw the rectangle. + * @param color The color of the rectangle. If the color is invalid, a rainbow is then drawn (like for the "Other..." entry in the popup). + * @param isDefault True if @p color is the default one and should then be draw with a diagonal line. + * @param width The width of the rectangle pixmap to return. + * @param height The height of the rectangle pixmap to return. + * + * @see colorRectPixmap() to get a transparent pixmap of the rectangle. + */ + static void drawColorRect(QPainter &painter, int x, int y, const QColor &color, bool isDefault, int width, int height); + + /** + * Get the height of a color rectangle for this combobox.\n + * This is equal to the text height, regarding to the current font of this combobox. + */ + int colorRectHeight() const; + + /** + * Get the width of a color rectangle, depending of the @p height of it.\n + * It typically return 1.4 * @p height for decent rectangle proportions. + */ + int colorRectWidthForHeight(int height) const; + + protected: + virtual void popup(); + virtual void mousePressEvent(QMouseEvent *event); + virtual void mouseMoveEvent(QMouseEvent *event); + virtual bool eventFilter(QObject *object, QEvent *event); + virtual void dragEnterEvent(QDragEnterEvent *event); + virtual void dropEvent(QDropEvent *event); + virtual void keyPressEvent(QKeyEvent *event); + virtual void fontChange(const QFont &oldFont); + + private: + /** + * Initialization routine common to every constructors.\n + * Constructors just have to initialize the QComboBox, m_color and m_defaultColor + * and this function do the rest to complete the creation of this widget. + */ + void init(); + + /** + * Free up all memory allocated for the color array.\n + * But only if an array have previously been allocated, of course. + */ + void deleteColorArray(); + + /** + * Update the only item of the combobox to mirror the new selected color.\n + * Mainly called on init() and setColor(). + */ + void updateComboBox(); + + KColorPopup *m_popup; + QColor m_color; + QColor m_defaultColor; + bool m_discardNextMousePress; + QColor **m_colorArray; + int m_columnCount; + int m_rowCount; + QPoint m_dragStartPos; + + protected: + /** + * Keep place for future improvements without having to break binary compatibility.\n + * Does nothing for the moment. + */ + virtual void virtual_hook(int id, void *data); + + private: + /** + * Keep place for future improvements without having to break binary compatibility. + */ + class KColorCombo2Private; + + KColorCombo2Private *d; +}; + + + +// TODO: setColorArray(QColor **, int, int) and use signals/slots ?? + +class KColorPopup : public QWidget +{ + Q_OBJECT + public: + KColorPopup(KColorCombo2 *parent); + ~KColorPopup(); + void relayout(); // updateGeometry() ?? + protected: + void paintEvent(QPaintEvent */*event*/); + void mouseMoveEvent(QMouseEvent *event); + void mousePressEvent(QMouseEvent *event); + void keyPressEvent(QKeyEvent *event); + void doSelection(); + void validate(); + void updateCell(int column, int row); + + friend class KColorCombo2; + + private: + KColorCombo2 *m_selector; + QPixmap m_pixmap; + int m_selectedRow; + int m_selectedColumn; + int m_columnOther; + QColor m_otherColor; + + static const int MARGIN; + static const int FRAME_WIDTH; +}; + + + +#endif // KCOLORCOMBO2_H diff --git a/src/keyboard.cpp b/src/keyboard.cpp new file mode 100644 index 0000000..a7871b3 --- /dev/null +++ b/src/keyboard.cpp @@ -0,0 +1,129 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kapplication.h> // it define Q_WS_X11 + +#include "keyboard.h" + +/* This file contain modified code from klistbox.cpp + */ + +// ShiftMask, ControlMask and Mod1Mask are defined in <X11/X.h> +#if defined Q_WS_X11 && ! defined K_WS_QTONLY +#include <X11/Xlib.h> // schroder +#endif + +void Keyboard::pressedKeys(bool &shiftPressed, bool &controlPressed) +{ +#if defined Q_WS_X11 && ! defined K_WS_QTONLY + Window root; + Window child; + int root_x, root_y, win_x, win_y; + uint keybstate; + XQueryPointer( qt_xdisplay(), qt_xrootwin(), &root, &child, + &root_x, &root_y, &win_x, &win_y, &keybstate ); + + shiftPressed = keybstate & ShiftMask; + controlPressed = keybstate & ControlMask; +#endif +} + +/** Same code as pressedKeys(...) but for shift key only + */ +bool Keyboard::shiftPressed() +{ +#if defined Q_WS_X11 && ! defined K_WS_QTONLY + Window root; + Window child; + int root_x, root_y, win_x, win_y; + uint keybstate; + XQueryPointer( qt_xdisplay(), qt_xrootwin(), &root, &child, + &root_x, &root_y, &win_x, &win_y, &keybstate ); + + return (keybstate & ShiftMask) != 0; +#else + return false; +#endif +} + +/** Same code as pressedKeys(...) but for control key only + */ +bool Keyboard::controlPressed() +{ +#if defined Q_WS_X11 && ! defined K_WS_QTONLY + Window root; + Window child; + int root_x, root_y, win_x, win_y; + uint keybstate; + XQueryPointer( qt_xdisplay(), qt_xrootwin(), &root, &child, + &root_x, &root_y, &win_x, &win_y, &keybstate ); + + return (keybstate & ControlMask) != 0; +#else + return false; +#endif +} + +/** Return if Alt key is pressed + */ +bool Keyboard::altPressed() +{ +#if defined Q_WS_X11 && ! defined K_WS_QTONLY + Window root; + Window child; + int root_x, root_y, win_x, win_y; + uint keybstate; + XQueryPointer( qt_xdisplay(), qt_xrootwin(), &root, &child, + &root_x, &root_y, &win_x, &win_y, &keybstate ); + + return (keybstate & Mod1Mask) != 0; +#else + return false; +#endif +} + +/******************* + * What does KDE 3.1 and later: + * FIXME: Use this function in KDE 4 + * (I wasn't knowing it by creating this class) + + / * + * Returns the currently pressed keyboard modifiers (e.g. shift, control, etc.) + * Usually you simply want to test for those in key events, in which case + * QKeyEvent::state() does the job (or QKeyEvent::key() to + * notice when a modifier is pressed alone). + * But it can be useful to query for the status of the modifiers at another moment + * (e.g. some KDE apps do that upon a drop event). + * @return the keyboard modifiers + * @since 3.1 + / +uint KApplication::keyboardModifiers() +{ + Window root; + Window child; + int root_x, root_y, win_x, win_y; + uint keybstate; + XQueryPointer( qt_xdisplay(), qt_xrootwin(), &root, &child, + &root_x, &root_y, &win_x, &win_y, &keybstate ); + return keybstate & 0x00ff; +} + + * + *******************/ diff --git a/src/keyboard.h b/src/keyboard.h new file mode 100644 index 0000000..2be6297 --- /dev/null +++ b/src/keyboard.h @@ -0,0 +1,37 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef KEYBOARD_H +#define KEYBOARD_H + +/** + * Methods to know if Shift and / or Ctrl keys are pressed. + * Depend on X, but neither Qt not KDE provide this class / methods. + * @author S�bastien Lao�t + */ +namespace Keyboard +{ + void pressedKeys(bool &shiftPressed, bool &controlPressed); + bool shiftPressed(); + bool controlPressed(); + bool altPressed(); +} + +#endif // KEYBOARD_H diff --git a/src/kgpgme.cpp b/src/kgpgme.cpp new file mode 100644 index 0000000..4aaeeeb --- /dev/null +++ b/src/kgpgme.cpp @@ -0,0 +1,443 @@ +/*************************************************************************** + * Copyright (C) 2006 by Petri Damsten * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "kgpgme.h" + +#ifdef HAVE_LIBGPGME + +#include <kapplication.h> +#include <kmessagebox.h> +#include <kpassdlg.h> +#include <kiconloader.h> +#include <klistview.h> +#include <kdebug.h> +#include <qcheckbox.h> +#include <qlayout.h> +#include <qlabel.h> +#include <klocale.h> +#include <locale.h> +#include <errno.h> +#include <stdlib.h> +#include <unistd.h> + +// KGpgSelKey class based on class in KGpg with the same name + +class KGpgSelKey : public KDialogBase +{ + private: + KListView* keysListpr; + + public: + + KGpgSelKey(QWidget *parent, const char *name, QString preselected, + const KGpgMe& gpg): + KDialogBase( parent, name, true,i18n("Private Key List"),Ok | Cancel) { + QString keyname; + QVBoxLayout* vbox; + QWidget* page = new QWidget(this); + QLabel* labeltxt; + KIconLoader* loader = KGlobal::iconLoader(); + QPixmap keyPair = loader->loadIcon("kgpg_key2", KIcon::Small, 20); + + setMinimumSize(350,100); + keysListpr = new KListView(page); + keysListpr->setRootIsDecorated(true); + keysListpr->addColumn(i18n("Name")); + keysListpr->addColumn(i18n("Email")); + keysListpr->addColumn(i18n("ID")); + keysListpr->setShowSortIndicator(true); + keysListpr->setFullWidth(true); + keysListpr->setAllColumnsShowFocus(true); + + labeltxt = new QLabel(i18n("Choose a secret key:"),page); + vbox = new QVBoxLayout(page); + + KGpgKeyList list = gpg.keys(true); + + for(KGpgKeyList::iterator it = list.begin(); it != list.end(); ++it) { + QString name = gpg.checkForUtf8((*it).name); + KListViewItem *item = new + KListViewItem(keysListpr, name, (*it).email, (*it).id); + item->setPixmap(0,keyPair); + if(preselected == (*it).id) { + keysListpr->setSelected(item, true); + keysListpr->setCurrentItem(item); + } + } + if(!keysListpr->selectedItem()) { + keysListpr->setSelected(keysListpr->firstChild(), true); + keysListpr->setCurrentItem(keysListpr->firstChild()); + } + vbox->addWidget(labeltxt); + vbox->addWidget(keysListpr); + setMainWidget(page); + }; + + QString key() { + QListViewItem* item = keysListpr->selectedItem(); + + if(item) + return item->text(2); + return ""; + } +}; + +KGpgMe::KGpgMe() : m_ctx(0), m_useGnuPGAgent(true) +{ + init(GPGME_PROTOCOL_OpenPGP); + if(gpgme_new(&m_ctx)) { + m_ctx = 0; + } + else { + gpgme_set_armor(m_ctx, 1); + setPassphraseCb(); + } +} + +KGpgMe::~KGpgMe() +{ + if(m_ctx) + gpgme_release(m_ctx); + clearCache(); +} + +void KGpgMe::clearCache() +{ + if(m_cache.size() > 0) + { + m_cache.fill('\0'); + m_cache.truncate(0); + } +} + +void KGpgMe::init(gpgme_protocol_t proto) +{ + gpgme_error_t err; + + gpgme_check_version(NULL); + setlocale(LC_ALL, ""); + gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL)); + gpgme_set_locale(NULL, LC_MESSAGES, setlocale(LC_MESSAGES, NULL)); + + err = gpgme_engine_check_version(proto); + if(err) { + KMessageBox::error(kapp->activeWindow(), QString("%1: %2") + .arg(gpgme_strsource(err)).arg(gpgme_strerror(err))); + } +} + +QString KGpgMe::checkForUtf8(QString txt) +{ + // code borrowed from KGpg which borrowed it from gpa + const char *s; + + // Make sure the encoding is UTF-8. + // Test structure suggested by Werner Koch + if(txt.isEmpty()) + return QString::null; + + for(s = txt.ascii(); *s && !(*s & 0x80); s++) + ; + if (*s && !strchr (txt.ascii(), 0xc3) && (txt.find("\\x")==-1)) + return txt; + + // The string is not in UTF-8 + //if (strchr (txt.ascii(), 0xc3)) return (txt+" +++"); + if (txt.find("\\x")==-1) + return QString::fromUtf8(txt.ascii()); + // if (!strchr (txt.ascii(), 0xc3) || (txt.find("\\x")!=-1)) { + for(int idx = 0 ; (idx = txt.find( "\\x", idx )) >= 0 ; ++idx) { + char str[2] = "x"; + str[0] = (char)QString(txt.mid(idx + 2, 2)).toShort(0, 16); + txt.replace(idx, 4, str); + } + if (!strchr (txt.ascii(), 0xc3)) + return QString::fromUtf8(txt.ascii()); + else + return QString::fromUtf8(QString::fromUtf8(txt.ascii()).ascii()); + // perform Utf8 twice, or some keys display badly + return txt; +} + +QString KGpgMe::selectKey(QString previous) +{ + KGpgSelKey dlg(kapp->activeWindow(), "", previous, *this); + + if(dlg.exec()) + return dlg.key(); + return ""; +} + +// Rest of the code is mainly based in gpgme examples + +KGpgKeyList KGpgMe::keys(bool privateKeys /* = false */) const +{ + KGpgKeyList keys; + gpgme_error_t err = 0, err2 = 0; + gpgme_key_t key = 0; + gpgme_keylist_result_t result = 0; + + if(m_ctx) { + err = gpgme_op_keylist_start(m_ctx, NULL, privateKeys); + if(!err) { + while(!(err = gpgme_op_keylist_next(m_ctx, &key))) { + KGpgKey gpgkey; + + if(!key->subkeys) + continue; + gpgkey.id = key->subkeys->keyid; + if(key->uids) { + gpgkey.name = key->uids->name; + gpgkey.email = key->uids->email; + } + keys.append(gpgkey); + gpgme_key_unref(key); + } + + if (gpg_err_code (err) == GPG_ERR_EOF) + err = 0; + err2 = gpgme_op_keylist_end(m_ctx); + if(!err) + err = err2; + } + } + + if(err) { + KMessageBox::error(kapp->activeWindow(), QString("%1: %2") + .arg(gpgme_strsource(err)).arg(gpgme_strerror(err))); + } + else { + result = gpgme_op_keylist_result(m_ctx); + if (result->truncated) { + KMessageBox::error(kapp->activeWindow(), + i18n("Key listing unexpectedly truncated.")); + } + } + return keys; +} + +bool KGpgMe::encrypt(const QByteArray& inBuffer, Q_ULONG length, + QByteArray* outBuffer, QString keyid /* = QString::null */) +{ + gpgme_error_t err = 0; + gpgme_data_t in = 0, out = 0; + gpgme_key_t keys[2] = { NULL, NULL }; + gpgme_key_t* key = NULL; + gpgme_encrypt_result_t result = 0; + + outBuffer->resize(0); + if(m_ctx) { + err = gpgme_data_new_from_mem(&in, inBuffer.data(), length, 1); + if(!err) { + err = gpgme_data_new(&out); + if(!err) { + if(keyid.isNull()) { + key = NULL; + } + else { + err = gpgme_get_key(m_ctx, keyid.ascii(), &keys[0], 0); + key = keys; + } + + if(!err) { + err = gpgme_op_encrypt(m_ctx, key, GPGME_ENCRYPT_ALWAYS_TRUST, + in, out); + if(!err) { + result = gpgme_op_encrypt_result(m_ctx); + if (result->invalid_recipients) { + KMessageBox::error(kapp->activeWindow(), QString("%1: %2") + .arg(i18n("That public key is not meant for encryption")) + .arg(result->invalid_recipients->fpr)); + } + else { + err = readToBuffer(out, outBuffer); + } + } + } + } + } + } + if(err != GPG_ERR_NO_ERROR && err != GPG_ERR_CANCELED) { + KMessageBox::error(kapp->activeWindow(), QString("%1: %2") + .arg(gpgme_strsource(err)).arg(gpgme_strerror(err))); + } + if(err != GPG_ERR_NO_ERROR) + clearCache(); + if(keys[0]) + gpgme_key_unref(keys[0]); + if(in) + gpgme_data_release(in); + if(out) + gpgme_data_release(out); + return (err == GPG_ERR_NO_ERROR); +} + +bool KGpgMe::decrypt(const QByteArray& inBuffer, QByteArray* outBuffer) +{ + gpgme_error_t err = 0; + gpgme_data_t in = 0, out = 0; + gpgme_decrypt_result_t result = 0; + + outBuffer->resize(0); + if(m_ctx) { + err = gpgme_data_new_from_mem(&in, inBuffer.data(), inBuffer.size(), 1); + if(!err) { + err = gpgme_data_new(&out); + if(!err) { + err = gpgme_op_decrypt(m_ctx, in, out); + if(!err) { + result = gpgme_op_decrypt_result(m_ctx); + if(result->unsupported_algorithm) { + KMessageBox::error(kapp->activeWindow(), QString("%1: %2") + .arg(i18n("Unsupported algorithm")) + .arg(result->unsupported_algorithm)); + } + else { + err = readToBuffer(out, outBuffer); + } + } + } + } + } + if(err != GPG_ERR_NO_ERROR && err != GPG_ERR_CANCELED) { + KMessageBox::error(kapp->activeWindow(), QString("%1: %2") + .arg(gpgme_strsource(err)).arg(gpgme_strerror(err))); + } + if(err != GPG_ERR_NO_ERROR) + clearCache(); + if(in) + gpgme_data_release(in); + if(out) + gpgme_data_release(out); + return (err == GPG_ERR_NO_ERROR); +} + +#define BUF_SIZE (32 * 1024) + +gpgme_error_t KGpgMe::readToBuffer(gpgme_data_t in, QByteArray* outBuffer) const +{ + int ret; + gpgme_error_t err = GPG_ERR_NO_ERROR; + + ret = gpgme_data_seek(in, 0, SEEK_SET); + if(ret) { + err = gpgme_err_code_from_errno(errno); + } + else { + char* buf = new char[BUF_SIZE + 2]; + + if(buf) { + while((ret = gpgme_data_read(in, buf, BUF_SIZE)) > 0) { + uint size = outBuffer->size(); + if(outBuffer->resize(size + ret)) + memcpy(outBuffer->data() + size, buf, ret); + } + if(ret < 0) + err = gpgme_err_code_from_errno(errno); + delete[] buf; + } + } + return err; +} + +bool KGpgMe::isGnuPGAgentAvailable() +{ + QString agent_info = getenv("GPG_AGENT_INFO"); + + if (agent_info.find(':') > 0) + return true; + return false; +} + +void KGpgMe::setPassphraseCb() +{ + bool agent = false; + QString agent_info; + + agent_info = getenv("GPG_AGENT_INFO"); + + if(m_useGnuPGAgent) + { + if (agent_info.find(':')) + agent = true; + if(agent_info.startsWith("disable:")) + setenv("GPG_AGENT_INFO", agent_info.mid(8), 1); + } + else + { + if(!agent_info.startsWith("disable:")) + setenv("GPG_AGENT_INFO", "disable:" + agent_info, 1); + } + if (agent) + gpgme_set_passphrase_cb(m_ctx, 0, 0); + else + gpgme_set_passphrase_cb(m_ctx, passphraseCb, this); +} + +gpgme_error_t KGpgMe::passphraseCb(void* hook, const char* uid_hint, + const char* passphrase_info, + int last_was_bad, int fd) +{ + KGpgMe* gpg = static_cast<KGpgMe*>(hook); + return gpg->passphrase(uid_hint, passphrase_info, last_was_bad, fd); +} + +gpgme_error_t KGpgMe::passphrase(const char* uid_hint, + const char* /*passphrase_info*/, + int last_was_bad, int fd) +{ + gpgme_error_t res = GPG_ERR_CANCELED; + QString s; + QString gpg_hint = checkForUtf8(uid_hint); + int result; + + if(last_was_bad){ + s += "<b>" + i18n("Wrong password.") + "</b><br><br>\n\n"; + clearCache(); + } + + if(!m_text.isEmpty()) + s += m_text + "<br>"; + + if(!gpg_hint.isEmpty()) + s += gpg_hint; + + if(m_cache.isEmpty()){ + QCString password; + + if(m_saving) + result = KPasswordDialog::getNewPassword(password, s); + else + result = KPasswordDialog::getPassword(password, s); + + if(result == KPasswordDialog::Accepted) + m_cache = password; + } + else + result = KPasswordDialog::Accepted; + + if(result == KPasswordDialog::Accepted) { + write(fd, m_cache.data(), m_cache.length()); + res = 0; + } + write(fd, "\n", 1); + return res; +} +#endif // HAVE_LIBGPGME diff --git a/src/kgpgme.h b/src/kgpgme.h new file mode 100644 index 0000000..5493273 --- /dev/null +++ b/src/kgpgme.h @@ -0,0 +1,85 @@ +/*************************************************************************** + * Copyright (C) 2006 by Petri Damsten * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef KGPGME_H +#define KGPGME_H + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#ifdef HAVE_LIBGPGME + +#include <gpgme.h> +#include <qstringlist.h> + +/** + @author Petri Damsten <[email protected]> +*/ + +class KGpgKey +{ + public: + QString id; + QString name; + QString email; +}; + +typedef QValueList< KGpgKey > KGpgKeyList; + +class KGpgMe +{ + public: + KGpgMe(); + ~KGpgMe(); + + QString selectKey(QString previous = QString::null); + KGpgKeyList keys(bool privateKeys = false) const; + void setText(QString text, bool saving) { m_text = text; m_saving = saving; }; + void setUseGnuPGAgent(bool use) { m_useGnuPGAgent = use; setPassphraseCb(); }; + QString text() const { return m_text; }; + bool saving() const { return m_saving; }; + void clearCache(); + + bool encrypt(const QByteArray& inBuffer, Q_ULONG length, + QByteArray* outBuffer, QString keyid = QString::null); + bool decrypt(const QByteArray& inBuffer, QByteArray* outBuffer); + + static QString checkForUtf8(QString txt); + static bool isGnuPGAgentAvailable(); + + private: + gpgme_ctx_t m_ctx; + QString m_text; + bool m_saving; + bool m_useGnuPGAgent; + QCString m_cache; + + void init(gpgme_protocol_t proto); + gpgme_error_t readToBuffer(gpgme_data_t in, QByteArray* outBuffer) const; + void setPassphraseCb(); + static gpgme_error_t passphraseCb(void *hook, const char *uid_hint, + const char *passphrase_info, + int last_was_bad, int fd); + gpgme_error_t passphrase(const char *uid_hint, + const char *passphrase_info, + int last_was_bad, int fd); +}; +#endif // HAVE_LIBGPGME +#endif // KGPGME_H diff --git a/src/kiconcanvas.cpp b/src/kiconcanvas.cpp new file mode 100644 index 0000000..13ec6ce --- /dev/null +++ b/src/kiconcanvas.cpp @@ -0,0 +1,244 @@ +/* vi: ts=8 sts=4 sw=4 + * kate: space-indent on; indent-width 4; mixedindent off; indent-mode cstyle; + * + * This file is part of the KDE project, module kfile. + * Copyright (C) 2006 Luke Sandell <[email protected]> + * (C) 2002 Carsten Pfeiffer <[email protected]> + * (C) 2000 Geert Jansen <[email protected]> + * (C) 2000 Kurt Granroth <[email protected]> + * (C) 1997 Christoph Neerfeld <[email protected]> + * + * This is free software; it comes under the GNU Library General + * Public License, version 2. See the file "COPYING.LIB" for the + * exact licensing terms. + */ + +#include "kiconcanvas.h" +#include "kicondialog.h" + +#include <config.h> + +#include <kiconviewsearchline.h> + +#include <kapplication.h> +#include <kdebug.h> +#include <klocale.h> +#include <kglobal.h> +#include <kstandarddirs.h> +#include <kiconloader.h> +#include <kimagefilepreview.h> +#include <kurldrag.h> +#include <kmultipledrag.h> + +#include <qsortedlist.h> +#include <qimage.h> +#include <qpixmap.h> +#include <qtimer.h> +#include <qfileinfo.h> +#include <qdragobject.h> +#include <cmath> +#include <math.h> +#include <algorithm> + +#ifdef HAVE_LIBART +#include <svgicons/ksvgiconengine.h> +#include <svgicons/ksvgiconpainter.h> +#endif + +class KIconCanvasItem : public QIconViewItem +{ + public: + KIconCanvasItem ( QIconView * parent, const QString & key, const QPixmap & pixmap ) + : QIconViewItem(parent) + { + setText(QFileInfo(key).baseName()); + setKey(key); + setPixmap(pixmap); + setDragEnabled(true); + setDropEnabled(false); + + } + + int compare(QIconViewItem *rhs) const + { + return QString::localeAwareCompare(text().lower(), rhs->text().lower()); + } +}; + +class KIconCanvas::KIconCanvasPrivate +{ + public: + KIconCanvasPrivate() + { + m_bLoading = false; + mSize = 0; + } + ~KIconCanvasPrivate() + { + } + bool m_bLoading; + QString mSetCurrent; + int mSize; +#ifdef HAVE_LIBART + KSvgIconEngine mSvgEngine; +#endif + bool mStrictIconSize; +}; + +/* + * KIconCanvas: Iconview for the iconloader dialog. + */ + +KIconCanvas::KIconCanvas(QWidget *parent, const char *name) + : KIconView(parent, name) +{ + d = new KIconCanvasPrivate; + mpLoader = KGlobal::iconLoader(); + mpTimer = new QTimer(this); + connect(mpTimer, SIGNAL(timeout()), SLOT(slotLoadFiles())); + connect(this, SIGNAL(currentChanged(QIconViewItem *)), + SLOT(slotCurrentChanged(QIconViewItem *))); + setAcceptDrops(false); + setShowToolTips(true); + setStrictIconSize(false); +} + +KIconCanvas::~KIconCanvas() +{ + delete mpTimer; + delete d; +} + +void KIconCanvas::setIconLoader(KIconLoader *loader) +{ + mpLoader = loader; +} + +void KIconCanvas::loadIcon(const QString &name) +{ + QImage img; + QString path = mpLoader->iconPath(name,-d->mSize); + // Use the extension as the format. Works for XPM and PNG, but not for SVG + QString ext = path.right(3).upper(); + int maxSize = std::min(d->mSize, 60); + + if (ext != "SVG" && ext != "VGZ") + img.load(path); +#ifdef HAVE_LIBART + else + if (d->mSvgEngine.load(maxSize, maxSize, path)) + img = *d->mSvgEngine.painter()->image(); +#endif + + if (img.isNull()) + return; + + // For non-KDE icons + if (d->mStrictIconSize && (img.width() != d->mSize || img.height() != d->mSize)) + return; + + if (img.width() > maxSize || img.height() > maxSize) + { + if (img.width() > img.height()) { + int height = (int) ((float(maxSize) / img.width()) * img.height()); + img = img.smoothScale(maxSize, height); + } else { + int width = (int) ((float(maxSize) / img.height()) * img.width()); + img = img.smoothScale(width, maxSize); + } + } + QPixmap pm; + pm.convertFromImage(img); + + (void) new KIconCanvasItem(this, name, pm); +} + +void KIconCanvas::loadFiles(const QStringList& files) +{ + clear(); + mFiles = files; + emit startLoading(mFiles.count()); + mpTimer->start(10, true); // #86680 + d->m_bLoading = false; +} + +void KIconCanvas::slotLoadFiles() +{ + setResizeMode(Fixed); + QApplication::setOverrideCursor(waitCursor); + + // disable updates to not trigger paint events when adding child items + setUpdatesEnabled( false ); + + d->m_bLoading = true; + int count; + QStringList::ConstIterator it; + QStringList::ConstIterator end(mFiles.end()); + for (it=mFiles.begin(), count=0; it!=end; ++it, count++) + { + loadIcon(*it); + + // Calling kapp->processEvents() makes the iconview flicker like hell + // (it's being repainted once for every new item), so we don't do this. + // Instead, we directly repaint the progress bar without going through + // the event-loop. We do that just once for every 10th item so that + // the progress bar doesn't flicker in turn. (pfeiffer) + // FIXME: Qt4 will have double buffering + if ( count % 10 == 0) { + emit progress(count); + } + if ( !d->m_bLoading ) // user clicked on a button that will load another set of icons + break; + } + + // enable updates since we have to draw the whole view now + sort(); + d->m_bLoading = false; + setUpdatesEnabled( true ); + QApplication::restoreOverrideCursor(); + emit finished(); + setResizeMode(Adjust); +} + +QString KIconCanvas::getCurrent() const +{ + return currentItem() ? currentItem()->key() : QString::null; +} + +void KIconCanvas::stopLoading() +{ + d->m_bLoading = false; +} + +void KIconCanvas::slotCurrentChanged(QIconViewItem *item) +{ + emit nameChanged((item != 0L) ? item->text() : QString::null); +} + +void KIconCanvas::setGroupOrSize( int groupOrSize ) +{ + d->mSize = ((int)groupOrSize >= 0) ? + mpLoader->currentSize((KIcon::Group)groupOrSize) : + -groupOrSize; +} + +void KIconCanvas::setStrictIconSize( bool strictIconSize ) +{ + d->mStrictIconSize = strictIconSize; +} + +QDragObject *KIconCanvas::dragObject() +{ + // We use QImageDrag rather than KURLDrag so that the user can't drag an icon out of the theme! + // TODO: support SVG? + QPixmap *pixmap = currentItem()->pixmap(); + QPoint pos = viewportToContents( viewport()->mapFromGlobal( QCursor::pos() ) ); + QPoint hot; + hot.setX(pos.x() - currentItem()->pos().x() - (currentItem()->width() - pixmap->width()) / 2); + hot.setY(pos.y() - currentItem()->pos().y() - (currentItem()->height() - pixmap->height()) / 2); + QImageDrag *drag = new QImageDrag( pixmap->convertToImage(), this ); + drag->setPixmap(*pixmap, hot); + return drag; +} + +#include "kiconcanvas.moc" diff --git a/src/kiconcanvas.h b/src/kiconcanvas.h new file mode 100644 index 0000000..fe8f58a --- /dev/null +++ b/src/kiconcanvas.h @@ -0,0 +1,89 @@ +/* vi: ts=8 sts=4 sw=4 + * kate: space-indent on; indent-width 4; mixedindent off; indent-mode cstyle; + * + * This file is part of the KDE project, module kfile. + * Copyright (C) 2006 Luke Sandell <[email protected]> + * (C) 2002 Carsten Pfeiffer <[email protected]> + * (C) 2000 Geert Jansen <[email protected]> + * (C) 2000 Kurt Granroth <[email protected]> + * (C) 1997 Christoph Neerfeld <[email protected]> + * + * This is free software; it comes under the GNU Library General + * Public License, version 2. See the file "COPYING.LIB" for the + * exact licensing terms. + */ + +#ifndef _KICONCANVAS_H_ +#define _KICONCANVAS_H_ + +#include <qstring.h> +#include <qstringlist.h> +#include <kiconview.h> + +class QTimer; +class KIconLoader; +class QDragObject; +class QIconLoader; + +/** + * Icon canvas for KIconDialog. + */ +class KIO_EXPORT KIconCanvas: public KIconView +/* NOTE: Why export this? */ +{ + Q_OBJECT + +public: + KIconCanvas(QWidget *parent=0L, const char *name=0L); + ~KIconCanvas(); + + /** + * Load icons into the canvas. + */ + void loadFiles(const QStringList& files); + + /** + * Returns the current icon. + */ + QString getCurrent() const; + + void setIconLoader(KIconLoader *loader); + + void setGroupOrSize(int groupOrSize); + + void setStrictIconSize(bool strictIconSize); + +public slots: + void stopLoading(); + +signals: + /** + * Emitted when the current icon has changed. + */ + void nameChanged(QString); + /* KDE 4: Make it const QString */ + + void startLoading(int); + void progress(int); + void finished(); + +private slots: + void slotLoadFiles(); + void slotCurrentChanged(QIconViewItem *item); + +private: + QStringList mFiles; + QTimer *mpTimer; + KIconLoader *mpLoader; + +protected: + virtual void virtual_hook( int id, void* data ); + virtual QDragObject *dragObject(); + void loadIcon(const QString &path); + +private: + class KIconCanvasPrivate; + KIconCanvasPrivate *d; +}; + +#endif diff --git a/src/kicondialog.cpp b/src/kicondialog.cpp new file mode 100644 index 0000000..e5bf262 --- /dev/null +++ b/src/kicondialog.cpp @@ -0,0 +1,559 @@ +/* vi: ts=8 sts=4 sw=4 + * kate: space-indent on; indent-width 4; mixedindent off; indent-mode cstyle; + * + * This file is part of the KDE project, module kfile. + * Copyright (C) 2006 Luke Sandell <[email protected]> + * (C) 2002 Carsten Pfeiffer <[email protected]> + * (C) 2000 Geert Jansen <[email protected]> + * (C) 2000 Kurt Granroth <[email protected]> + * (C) 1997 Christoph Neerfeld <[email protected]> + * + * This is free software; it comes under the GNU Library General + * Public License, version 2. See the file "COPYING.LIB" for the + * exact licensing terms. + */ + +#include "kicondialogui.h" +#include "kicondialog.h" +#include "kiconcanvas.h" + +#include <config.h> + +#include <kiconviewsearchline.h> + +#include <kdebug.h> +#include <kapplication.h> +#include <klocale.h> +#include <kglobal.h> +#include <kstandarddirs.h> +#include <kiconloader.h> +#include <kprogress.h> +#include <kiconview.h> +#include <kfiledialog.h> +#include <kimagefilepreview.h> +#include <kpushbutton.h> +#include <kmessagebox.h> + +#include <qstring.h> +#include <qstringlist.h> +#include <qsortedlist.h> +#include <qimage.h> +#include <qpixmap.h> +#include <qlabel.h> +#include <qcombobox.h> +#include <qtimer.h> +#include <qbuttongroup.h> +#include <qradiobutton.h> +#include <qfileinfo.h> +#include <qtoolbutton.h> +#include <qwhatsthis.h> +#include <qhbuttongroup.h> +#include <qdragobject.h> + +/* NOTE: Must be in the same order as listbox */ +enum ExtendedContext +{ + ALL = 0, + RECENT = 1, + // Action thru MimeType, subtract 1 to convert to KIcon::Context + OTHER = 7 +}; + +class KIconDialog::KIconDialogPrivate +{ + public: + KIconDialogPrivate() { + m_bStrictIconSize = true; + } + ~KIconDialogPrivate() {} + bool m_bStrictIconSize; + QString custom; + QString customLocation; + int recentMax; + QStringList recentList; + ExtendedContext extendedContext; + KIconDialogUI *ui; /* FIXME: KDE4 probably move this to the main class */ +}; + +/* + * KIconDialog: Dialog for selecting icons. Both system and user + * specified icons can be chosen. + */ + +KIconDialog::KIconDialog(QWidget *parent, const char*) + : KDialogBase(parent, "IconDialog", true, i18n("Select Icon"), Ok|Cancel, Ok) +{ + d = new KIconDialogPrivate; + mpLoader = KGlobal::iconLoader(); + init(); + resize(minimumSize()); +} + +KIconDialog::KIconDialog(KIconLoader *loader, QWidget *parent, + const char *name) + : KDialogBase(parent, name, true, i18n("Select Icon"), Ok|Cancel, Ok) +{ + d = new KIconDialogPrivate; + mpLoader = loader; + init(); +} + +void KIconDialog::init() +{ + mGroupOrSize = KIcon::Desktop; + d->extendedContext = ALL; + mType = 0; + setCustomLocation(QString::null); // Initialize mFileList + + // Read configuration + KConfig *config = KGlobal::config(); + KConfigGroupSaver saver(config, "KIconDialog"); + d->recentMax = config->readNumEntry("RecentMax", 10); + d->recentList = config->readPathListEntry("RecentIcons"); + + d->ui = new KIconDialogUI( this ); + setMainWidget(d->ui); + + d->ui->searchLine->setIconView(d->ui->iconCanvas); + d->ui->searchLine->setCaseSensitive(false); + + // Hack standard Gui item, as KDevDesigner won't let us + d->ui->browseButton->setText(i18n("&Browse...")); + + connect(d->ui->browseButton, SIGNAL(clicked()), SLOT(slotBrowse())); + connect(d->ui->listBox, SIGNAL(highlighted(int)), SLOT(slotContext(int))); + connect(d->ui->iconCanvas, SIGNAL(executed(QIconViewItem *)), SLOT(slotOk())); + connect(d->ui->iconCanvas, SIGNAL(returnPressed(QIconViewItem *)), SLOT(slotOk())); + connect(d->ui->iconCanvas, SIGNAL(startLoading(int)), SLOT(slotStartLoading(int))); + connect(d->ui->iconCanvas, SIGNAL(progress(int)), SLOT(slotProgress(int))); + connect(d->ui->iconCanvas, SIGNAL(finished()), SLOT(slotFinished())); + connect(this, SIGNAL(hidden()), d->ui->iconCanvas, SLOT(stopLoading())); + + // NOTE: this must be consistent with the IconType enum (see above) + d->ui->listBox->insertItem(i18n("(All Icons)")); + d->ui->listBox->insertItem(i18n("(Recent)")); + d->ui->listBox->insertItem(i18n("Actions")); + d->ui->listBox->insertItem(i18n("Applications")); + d->ui->listBox->insertItem(i18n("Devices")); + d->ui->listBox->insertItem(i18n("Filesystem")); + d->ui->listBox->insertItem(i18n("File Types")); + d->ui->listBox->insertItem(i18n("Miscellaneous")); +} + +KIconDialog::~KIconDialog() +{ + // Write configuration + KConfig *config = KGlobal::config(); + KConfigGroupSaver saver(config, "KIconDialog"); + config->writeEntry("RecentMax", d->recentMax, true, true); + config->writePathEntry("RecentIcons", d->recentList, ',', true, true); + + delete d; +} + +void KIconDialog::slotAcceptIcons() +{ + //FIXME: not used anymore +} + +void KIconDialog::showIcons() +{ + d->ui->iconCanvas->clear(); + QStringList filelist; + + KIcon::Context context = static_cast<KIcon::Context>(d->extendedContext - 1); + switch (d->extendedContext) + { + case RECENT: + filelist = d->recentList; + break; + case OTHER: + filelist = mFileList; + break; + case ALL: + filelist = mFileList; + context = KIcon::Any; + // ** Fallthrough to next case ** + default: + QStringList list; + if (d->m_bStrictIconSize) + list=mpLoader->queryIcons(mGroupOrSize, context); + else + list=mpLoader->queryIconsByContext(mGroupOrSize, context); + + // Remove path & extension + for ( QStringList::iterator it = list.begin(); it != list.end(); ++it) + filelist.append(QFileInfo(*it).baseName()); + } + + // Remove duplicate icons FIXME: Qt4 we can just use QSet + filelist.sort(); + QString prev; + for ( QStringList::iterator it = filelist.begin(); it != filelist.end(); ) + { + if (*it == prev) + { + it = filelist.remove(it); + } + else + { + prev = *it; + ++it; + } + } + + d->ui->iconCanvas->setGroupOrSize(mGroupOrSize); + d->ui->iconCanvas->loadFiles(filelist); +} + +void KIconDialog::setStrictIconSize(bool b) +{ + d->m_bStrictIconSize=b; +} + +bool KIconDialog::strictIconSize() const +{ + return d->m_bStrictIconSize; +} + +void KIconDialog::setIconSize( int size ) +{ + // see KIconLoader, if you think this is weird + if ( size == 0 ) + mGroupOrSize = KIcon::Desktop; // default Group + else + mGroupOrSize = -size; // yes, KIconLoader::queryIconsByContext is weird +} + +int KIconDialog::iconSize() const +{ + // 0 or any other value ==> mGroupOrSize is a group, so we return 0 + return (mGroupOrSize < 0) ? -mGroupOrSize : 0; +} + +#ifndef KDE_NO_COMPAT +QString KIconDialog::selectIcon(KIcon::Group group, KIcon::Context context, bool user) +{ + setup( group, context, false, 0, user ); + return openDialog(); +} +#endif + +void KIconDialog::setup(KIcon::Group group, KIcon::Context context, + bool strictIconSize, int iconSize, bool user ) +{ + setup(group, context, strictIconSize, iconSize, user, false, false); +} + +void KIconDialog::setup(KIcon::Group group, KIcon::Context context, + bool strictIconSize, int iconSize, bool user, + bool lockContext, bool lockBrowse ) +{ + d->m_bStrictIconSize = strictIconSize; + d->ui->iconCanvas->setStrictIconSize(strictIconSize); + mGroupOrSize = (iconSize == 0) ? group : -iconSize; + mType = user; + + d->extendedContext = static_cast<ExtendedContext>( ( context == KIcon::Any ) ? ALL : context + 1 ); + + // We cannot change layout because it is protected ;-( + // FIXME: Qt4 we will be able to inherit from both QDialog and our GUI + d->ui->listBox->setEnabled(!lockContext); + d->ui->browseButton->setEnabled(!lockBrowse); + d->ui->listBox->setHidden(lockContext && lockBrowse); + d->ui->browseButton->setHidden(lockContext && lockBrowse); + + d->ui->listBox->setCurrentItem(d->extendedContext); +} + +const QString & KIconDialog::customLocation( ) const +{ + return d->customLocation; +} + +void KIconDialog::setCustomLocation( const QString& location ) +{ + d->customLocation = location; + + if (location.isEmpty()) + { + mFileList = KGlobal::dirs()->findAllResources("appicon", QString::fromLatin1("*.png")); + } else { + mFileList = mpLoader->queryIconsByDir(location); + } +} + +QString KIconDialog::openDialog() +{ + showIcons(); + + if ( exec() == Accepted ) + { + if (!d->custom.isEmpty()) + return d->custom; + else + return d->ui->iconCanvas->getCurrent(); + } + else + { + return QString::null; + } +} + +void KIconDialog::showDialog() +{ + d->custom = QString::null; + + // Make it so minimumSize returns correct value + d->ui->filterLabel->hide(); + d->ui->searchLine->hide(); + d->ui->progressBar->show(); + + setModal(false); + show(); + + // FIXME: this should be before show() but it doesn't work ;-( + resize(minimumSize()); + + showIcons(); +} + +void KIconDialog::slotOk() +{ + QString key = !d->custom.isEmpty() ? d->custom : d->ui->iconCanvas->getCurrent(); + + // Append to list of recent icons + if (!d->recentList.contains(key)) + { + d->recentList.push_back(key); + + // Limit recent list in size + while ( (int)d->recentList.size() > d->recentMax ) + d->recentList.pop_front(); + } + + emit newIconName(key); + KDialogBase::slotOk(); +} + +QString KIconDialog::getIcon(KIcon::Group group, KIcon::Context context, + bool strictIconSize, int iconSize, bool user, + QWidget *parent, const QString &caption) +{ + KIconDialog dlg(parent, "icon dialog"); + dlg.setup( group, context, strictIconSize, iconSize, user ); + if (!caption.isNull()) + dlg.setCaption(caption); + + return dlg.openDialog(); +} + +void KIconDialog::slotBrowse() +{ + // Create a file dialog to select a PNG, XPM or SVG file, + // with the image previewer shown. + // KFileDialog::getImageOpenURL doesn't allow svg. + KFileDialog dlg(QString::null, i18n("*.png *.xpm *.svg *.svgz|Icon Files (*.png *.xpm *.svg *.svgz)"), + this, "filedialog", true); + dlg.setOperationMode( KFileDialog::Opening ); + dlg.setCaption( i18n("Open") ); + dlg.setMode( KFile::File | KFile::ExistingOnly | KFile::LocalOnly ); + KImageFilePreview *ip = new KImageFilePreview( &dlg ); + dlg.setPreviewWidget( ip ); + dlg.exec(); + + QString file = dlg.selectedFile(); + if (!file.isEmpty()) + { + d->custom = file; + if ( mType == 1 ) + setCustomLocation(QFileInfo( file ).dirPath( true )); + slotOk(); + } +} + +void KIconDialog::slotContext(int id) +{ + d->extendedContext = static_cast<ExtendedContext>(id); + showIcons(); +} + +void KIconDialog::slotStartLoading(int steps) +{ + if (steps < 10) + d->ui->progressBar->hide(); + else + { + d->ui->progressBar->setTotalSteps(steps); + d->ui->progressBar->setProgress(0); + d->ui->progressBar->show(); + d->ui->filterLabel->hide(); + d->ui->searchLine->hide(); + } +} + +void KIconDialog::slotProgress(int p) +{ + d->ui->progressBar->setProgress(p); +} + +void KIconDialog::slotFinished() +{ + d->ui->progressBar->hide(); + d->ui->filterLabel->show(); + d->ui->searchLine->show(); +} + +class KIconButton::KIconButtonPrivate +{ + public: + KIconButtonPrivate() { + m_bStrictIconSize = false; + iconSize = 0; // let KIconLoader choose the default + } + ~KIconButtonPrivate() {} + bool m_bStrictIconSize; + bool lockUser; + bool lockCustom; + int iconSize; +}; + + +/* + * KIconButton: A "choose icon" pushbutton. + */ + +KIconButton::KIconButton(QWidget *parent, const char *name) + : QPushButton(parent, name) +{ + init( KGlobal::iconLoader() ); +} + +KIconButton::KIconButton(KIconLoader *loader, + QWidget *parent, const char *name) + : QPushButton(parent, name) +{ + init( loader ); +} + +void KIconButton::init( KIconLoader *loader ) +{ + d = new KIconButtonPrivate; + mGroup = KIcon::Desktop; + mContext = KIcon::Application; + mbUser = false; + + mpLoader = loader; + mpDialog = 0L; + connect(this, SIGNAL(clicked()), SLOT(slotChangeIcon())); +} + +KIconButton::~KIconButton() +{ + delete mpDialog; + delete d; +} + +void KIconButton::setStrictIconSize(bool b) +{ + d->m_bStrictIconSize=b; +} + +bool KIconButton::strictIconSize() const +{ + return d->m_bStrictIconSize; +} + +void KIconButton::setIconSize( int size ) +{ + d->iconSize = size; +} + +int KIconButton::iconSize() const +{ + return d->iconSize; +} + +void KIconButton::setIconType(KIcon::Group group, KIcon::Context context, bool user) +{ + mGroup = group; + mContext = context; + mbUser = user; + d->lockUser = false; + d->lockCustom = false; +} + +void KIconButton::setIconType(KIcon::Group group, KIcon::Context context, bool user, bool lockUser, bool lockCustom) +{ + mGroup = group; + mContext = context; + mbUser = user; + d->lockUser = lockUser; + d->lockCustom = lockCustom; +} + +void KIconButton::setIcon(const QString& icon) +{ + mIcon = icon; + setIconSet(mpLoader->loadIconSet(mIcon, mGroup, d->iconSize)); + + if (!mpDialog) + { + mpDialog = new KIconDialog(mpLoader, this); + connect(mpDialog, SIGNAL(newIconName(const QString&)), SLOT(newIconName(const QString&))); + } +} + +const QString & KIconButton::customLocation( ) const +{ + return mpDialog ? mpDialog->customLocation() : QString::null; +} + +void KIconButton::setCustomLocation(const QString &custom) +{ + if (!mpDialog) + { + mpDialog = new KIconDialog(mpLoader, this); + connect(mpDialog, SIGNAL(newIconName(const QString&)), SLOT(newIconName(const QString&))); + } + + mpDialog->setCustomLocation(custom); +} + +void KIconButton::resetIcon() +{ + mIcon = QString::null; + setIconSet(QIconSet()); +} + +void KIconButton::slotChangeIcon() +{ + if (!mpDialog) + { + mpDialog = new KIconDialog(mpLoader, this); + connect(mpDialog, SIGNAL(newIconName(const QString&)), SLOT(newIconName(const QString&))); + } + + mpDialog->setup( mGroup, mContext, d->m_bStrictIconSize, d->iconSize, mbUser, d->lockUser, d->lockCustom ); + mpDialog->showDialog(); +} + +void KIconButton::newIconName(const QString& name) +{ + if (name.isEmpty()) + return; + + QIconSet iconset = mpLoader->loadIconSet(name, mGroup, d->iconSize); + setIconSet(iconset); + mIcon = name; + + emit iconChanged(name); +} + +void KIconCanvas::virtual_hook( int id, void* data ) +{ KIconView::virtual_hook( id, data ); } + +void KIconDialog::virtual_hook( int id, void* data ) +{ KDialogBase::virtual_hook( id, data ); } + +#include "kicondialog.moc" diff --git a/src/kicondialog.h b/src/kicondialog.h new file mode 100644 index 0000000..6b03bd4 --- /dev/null +++ b/src/kicondialog.h @@ -0,0 +1,329 @@ +/* vi: ts=8 sts=4 sw=4 + * kate: space-indent on; indent-width 4; mixedindent off; indent-mode cstyle; + * + * This file is part of the KDE project, module kfile. + * Copyright (C) 2006 Luke Sandell <[email protected]> + * (C) 2002 Carsten Pfeiffer <[email protected]> + * (C) 2000 Geert Jansen <[email protected]> + * (C) 2000 Kurt Granroth <[email protected]> + * (C) 1997 Christoph Neerfeld <[email protected]> + * + * This is free software; it comes under the GNU Library General + * Public License, version 2. See the file "COPYING.LIB" for the + * exact licensing terms. + */ + +#ifndef __KIconDialog_h__ +#define __KIconDialog_h__ + +#include <qstring.h> +#include <qstringlist.h> +#include <qpushbutton.h> + +#include <kicontheme.h> +#include <kdialogbase.h> +#include <kiconview.h> + +#include <kiconcanvas.h> // FIXME: BCI KDE 3 expects KIconCanvas to be defined in kicondialog.h + +class KIconDialogUI; + +class QComboBox; +class QTimer; +class QKeyEvent; +class QRadioButton; +class KProgress; +class KIconLoader; + +/** + * Dialog for interactive selection of icons. Use the function + * getIcon() let the user select an icon. + * + * @short An icon selection dialog. + */ +class KIO_EXPORT KIconDialog: public KDialogBase +{ + Q_OBJECT + +public: + /** + * Constructs an icon selection dialog using the global iconloader. + */ + KIconDialog(QWidget *parent=0L, const char *name=0L); + /** + * Constructs an icon selection dialog using a specific iconloader. + */ + KIconDialog(KIconLoader *loader, QWidget *parent=0, + const char *name=0); + /** + * Destructs the dialog. + */ + ~KIconDialog(); + + /** + * Sets a strict icon size policy for allowed icons. When true, + * only icons of the specified group's size in getIcon() are shown. + * When false, icons not available at the desired group's size will + * also be selectable. + */ + void setStrictIconSize(bool b); + /** + * Returns true if a strict icon size policy is set. + */ + bool strictIconSize() const; + + /** + * gets the custom icon directory + */ + const QString & customLocation() const; + + /** + * sets a custom icon directory + * @since 3.1 + */ + void setCustomLocation( const QString& location ); + + /** + * Sets the size of the icons to be shown / selected. + * @see KIcon::StdSizes + * @see iconSize + */ + void setIconSize(int size); + + /** + * Returns the iconsize set via setIconSize() or 0, if the default + * iconsize will be used. + */ + int iconSize() const; + +#ifndef KDE_NO_COMPAT + /** + * @deprecated in KDE 3.0, use the static method getIcon instead. + */ + QString selectIcon(KIcon::Group group=KIcon::Desktop, KIcon::Context + context=KIcon::Application, bool user=false); +#endif + + /** + * Allows you to set the same parameters as in the class method + * getIcon(). + * + */ + void setup( KIcon::Group group, + KIcon::Context context = KIcon::Application, + bool strictIconSize = false, int iconSize = 0, + bool user = false ); + /* FIXME: KDE4 remove -- same as next method with default arguments */ + + /** + * Allows you to set the same parameters as in the class method + * getIcon(), as well as two additional parameters to lock + * the choice between system and user dirs and to lock the custom user + * dir itself. + * + * @since 3.3 + */ + void setup( KIcon::Group group, KIcon::Context context, + bool strictIconSize, int iconSize, bool user, bool lockContext, + bool lockBrowse ); + // FIXME: KDE4 add default arguments (right now this would cause ambiguity with previous method) + + /** + * exec()utes this modal dialog and returns the name of the selected icon, + * or QString::null if the dialog was aborted. + * @returns the name of the icon, suitable for loading with KIconLoader. + * @see getIcon + */ + QString openDialog(); + + /** + * show()es this dialog and emits a newIcon(const QString&) signal when + * successful. QString::null will be emitted if the dialog was aborted. + */ + void showDialog(); + + /** + * Pops up the dialog an lets the user select an icon. + * + * @param group The icon group this icon is intended for. Providing the + * group shows the icons in the dialog with the same appearance as when + * used outside the dialog. + * @param context The initial icon context. Initially, the icons having + * this context are shown in the dialog. The user can change this. + * @param strictIconSize When true, only icons of the specified group's size + * are shown, otherwise icon not available in the desired group's size + * will also be selectable. + * @param iconSize the size of the icons -- the default of the icongroup + * if set to 0 + * @param user Begin with the "user icons" instead of "system icons". + * @param parent The parent widget of the dialog. + * @param caption The caption to use for the dialog. + * @return The name of the icon, suitable for loading with KIconLoader. + * @version New in 3.0 + */ + static QString getIcon(KIcon::Group group=KIcon::Desktop, + KIcon::Context context=KIcon::Application, + bool strictIconSize=false, int iconSize = 0, + bool user=false, QWidget *parent=0, + const QString &caption=QString::null); + +signals: + void newIconName(const QString&); + +protected slots: + void slotOk(); + +private slots: + void slotContext(int); + void slotStartLoading(int); + void slotProgress(int); + void slotFinished(); + void slotAcceptIcons(); + void slotBrowse(); +private: + void init(); + void showIcons(); + + int mGroupOrSize; + KIcon::Context mContext; + int mType; + + QStringList mFileList; + + // FIXME: the following fields are obsolete, remove in KDE4 + QComboBox *mpCombo; + QPushButton *mpBrowseBut; + QRadioButton *mpRb1, *mpRb2; + KProgress *mpProgress; + + KIconLoader *mpLoader; + + KIconCanvas *mpCanvas; // FIXME: obsolete, remove in KDE4 +protected: + virtual void virtual_hook( int id, void* data ); +private: + class KIconDialogPrivate; + KIconDialogPrivate *d; +}; + + +/** + * A pushbutton for choosing an icon. Pressing on the button will open a + * KIconDialog for the user to select an icon. The current icon will be + * displayed on the button. + * + * @see KIconDialog + * @short A push button that allows selection of an icon. + */ +class KIO_EXPORT KIconButton: public QPushButton +{ + Q_OBJECT + Q_PROPERTY( QString icon READ icon WRITE setIcon RESET resetIcon ) + Q_PROPERTY( int iconSize READ iconSize WRITE setIconSize) + Q_PROPERTY( bool strictIconSize READ strictIconSize WRITE setStrictIconSize ) + Q_PROPERTY( QString customLocation READ customLocation WRITE setCustomLocation ) + +public: + /** + * Constructs a KIconButton using the global iconloader. + */ + KIconButton(QWidget *parent=0L, const char *name=0L); + + /** + * Constructs a KIconButton using a specific KIconLoader. + */ + KIconButton(KIconLoader *loader, QWidget *parent, const char *name=0L); + /** + * Destructs the button. + */ + ~KIconButton(); + + /** + * Sets a strict icon size policy for allowed icons. When true, + * only icons of the specified group's size in setIconType are allowed, + * and only icons of that size will be shown in the icon dialog. + */ + void setStrictIconSize(bool b); + /** + * Returns true if a strict icon size policy is set. + */ + bool strictIconSize() const; + + /** + * Sets the icon group and context. Use KIcon::NoGroup if you want to + * allow icons for any group in the given context. + */ + void setIconType(KIcon::Group group, KIcon::Context context, bool user=false); + + + /** + * Same as above method, but allows you to specify whether user and custom mode should be locked. + */ + void setIconType(KIcon::Group group, KIcon::Context context, bool user, bool lockContext, bool lockBrowse); + /* FIXME: KDE4 this should replace the above method using default params */ + + /** + * sets a custom icon directory + */ + void setCustomLocation(const QString &custom); + + /** + * get the custom icon directory + */ + const QString & customLocation() const; + + /** + * Sets the button's initial icon. + */ + void setIcon(const QString& icon); + + /** + * Resets the icon (reverts to an empty button). + */ + void resetIcon(); + + /** + * Returns the name of the selected icon. + */ + QString icon() const { return mIcon; } + + /** + * Sets the size of the icon to be shown / selected. + * @see KIcon::StdSizes + * @see iconSize + */ + void setIconSize( int size ); + + /** + * Returns the iconsize set via setIconSize() or 0, if the default + * iconsize will be used. + */ + int iconSize() const; + +signals: + /** + * Emitted when the icon has changed. + */ + void iconChanged(QString icon); + /* FIXME: KDE4: Make it const QString & */ + +private slots: + void slotChangeIcon(); + void newIconName(const QString& name); + +private: + void init( KIconLoader *loader ); + + bool mbUser; + KIcon::Group mGroup; + KIcon::Context mContext; + + QString mIcon; + KIconDialog *mpDialog; + KIconLoader *mpLoader; + class KIconButtonPrivate; + KIconButtonPrivate *d; +}; + + +#endif // __KIconDialog_h__ diff --git a/src/kicondialogui.ui b/src/kicondialogui.ui new file mode 100644 index 0000000..9cd6587 --- /dev/null +++ b/src/kicondialogui.ui @@ -0,0 +1,243 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>KIconDialogUI</class> +<author>Luke Sandell</author> +<widget class="QWidget"> + <property name="name"> + <cstring>KIconDialogUI</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>474</width> + <height>340</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>3</hsizetype> + <vsizetype>3</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="caption"> + <string>KIconDialogUI</string> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="KListBox" row="0" column="0"> + <property name="name"> + <cstring>listBox</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>3</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + <widget class="KIconCanvas" row="0" column="1"> + <property name="name"> + <cstring>iconCanvas</cstring> + </property> + <property name="minimumSize"> + <size> + <width>370</width> + <height>290</height> + </size> + </property> + <property name="focusPolicy"> + <enum>WheelFocus</enum> + </property> + <property name="gridX" stdset="0"> + <number>80</number> + </property> + <property name="wordWrapIconText" stdset="0"> + <bool>false</bool> + </property> + </widget> + <widget class="QLayoutWidget" row="1" column="1"> + <property name="name"> + <cstring>layout4</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="spacing"> + <number>0</number> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout3</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>filterLabel</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Fi&lter:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>searchLine</cstring> + </property> + </widget> + <widget class="KIconViewSearchLine"> + <property name="name"> + <cstring>searchLine</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>StrongFocus</enum> + </property> + </widget> + </hbox> + </widget> + <widget class="KProgress"> + <property name="name"> + <cstring>progressBar</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </vbox> + </widget> + <widget class="KPushButton" row="1" column="0"> + <property name="name"> + <cstring>browseButton</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>&Open...</string> + </property> + <property name="accel"> + <string>Alt+O</string> + </property> + <property name="on"> + <bool>false</bool> + </property> + <property name="stdItem" stdset="0"> + <number>18</number> + </property> + </widget> + </grid> +</widget> +<customwidgets> + <customwidget> + <class>KIconViewSearchLine</class> + <header location="global">kiconviewsearchline.h</header> + <sizehint> + <width>-1</width> + <height>-1</height> + </sizehint> + <container>0</container> + <sizepolicy> + <hordata>5</hordata> + <verdata>5</verdata> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + <pixmap>image0</pixmap> + <slot access="public" specifier="">updateSearch(const QString &s)</slot> + <slot access="public" specifier="">setCaseSensitive(bool cs)</slot> + <slot access="public" specifier="">setIconView(QIconView *iv)</slot> + <slot access="public" specifier="">clear()</slot> + </customwidget> + <customwidget> + <class>KIconCanvas</class> + <header location="local">kiconcanvas.h</header> + <sizehint> + <width>-1</width> + <height>-1</height> + </sizehint> + <container>0</container> + <sizepolicy> + <hordata>5</hordata> + <verdata>5</verdata> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + <pixmap>image1</pixmap> + <signal>nameChanged(QString &)</signal> + <signal>startLoading(int)</signal> + <signal>progress(int)</signal> + <signal>finished()</signal> + <slot access="public" specifier="">nameChanged(QString name)</slot> + <slot access="public" specifier="">slot()</slot> + <slot access="public" specifier="">stopLoading()</slot> + <property type="Int">gridX</property> + <property type="Int">gridY</property> + <property type="Bool">itemsMovable</property> + <property type="Bool">wordWrapIconText</property> + <property type="Bool">autoArrange</property> + </customwidget> +</customwidgets> +<images> + <image name="image0"> + <data format="XBM.GZ" length="79">789c534e494dcbcc4b554829cdcdad8c2fcf4c29c95030e0524611cd48cd4ccf28010a1797249664262b2467241641a592324b8aa363156c15aab914146aadb90067111b1f</data> + </image> + <image name="image1"> + <data format="PNG" length="1125">89504e470d0a1a0a0000000d4948445200000016000000160806000000c4b46c3b0000042c49444154388db5954f6c14551cc73fefcd7476b65bdaae4bb78bb5502a14d404e4801c88182d1c4c2c693da847400f9c24c68b878684238660e2b1e01f12c19493012ef2478c814412d354a46017a8a564bb6da5bbedccee767776e63d0ffb073751d483bfe49799974c3eeffb7ebf37df9fd05a530b2184040cc0042420aaf9a4d0d554800f045a6b256ae0e1e1e1d6bebebe838ee31c48a7d39b5cd7fd075e251cc7617272f2ded8d8d819cff33e0316819259537aead4a9839d5dd6d1784f91f55b0a94830242088404d304292bef68a89f520802a598fecddaa04f1a876f5c250c7c0a64cdeac686e33807e23d45e6b297c8b877f1831542614550b6599835c83c2a81b6786a75134faf2f1169f12997350881d9021d0903e06de0745d3160a6d3e94dbd5b0a64dcbb94b5831d0e3375ab892b1772dcf9790528543f8dd0d367b36768153b5e31503a0f1aecb004580b44ffac58baae8b1714f0833c7638cc8dab303a320f4822ab4c7a37c69196203de3319d5ce1c4d13c733331dedc67a129a154fd128401ab0616d55a130ac3d42d93d1913940d13fd0c9ee0183685c60da01c5421bd72f7a8c8efccef9afd374267ad93d642365be0636a0d28ec7600941d9e6f23917f0e97f23ce5bef35d19ec863da0ed9059b2be70bec196c66dfa10ec0e49b338f7017258651bf95021035c595429bb0903248fe52a2b5b595dd7b4d945cc2340cdca536be389ee3f67886c5798f773fe8e0dac508c989659277a2180da4ca4ff07821058b8b251445d63d6b13ed1098a6417e39cac85197dbe31962ab9bd9f1f22a226d45366f6d0620fdb08c900d281af6110284b20085b414861d905d88f2e52739ee8cbb8022143259d3dd84691730aa2d52da441a8de0c6958068870022a41e9629ad3473fd3b8fdbe319dadb9b4924da994d2d716c7896fbe35152f78b48245d6b2da4507faf582be8eaf159b721cc837b05ae7debb1f79d08cb8b515edad942a22bc4b1c33eb3d34b1c797f06af90a72d16e2f96d9a74aa11dca8586b222d01af0fb60070f6c402d72f15d97f28c6f6d7027a5f5ce6c3233dc4e2ede496b278be4fff608cee8d3e1add806aeca51094cbb06397c1ecc328e746537c7e3ccdb5cb1136bf60635882d4d41c6ec6836ab37efa214f72208ed9f4d7cdd38ee310280542e38b1c43fb6de26b3672e1ec3cc99bcb246f66a938a3241ab3e91f7c861fbf77710b1e5e49915bae974203ba0e9e9c9cbc373d6d6d305a040a89c2a77f50b27d5782bbbf7acccf28349235dd16cf6dd374f7295e1de8a45c02d37499182b01cc0201a085d61a2144d8b2ac8fb6ed340e77240c4261890e04c250185262546d534a032154b59e0ad394e41c98182bf268ce6721ed9f064e0253356f6da2e24c1f030f783c15fe6da680af8021602bd051532ca9b8521488559f61aa86c29343578fbf0264a94c906c7d3409214c20043457a116ff6de6795578012889ff6b98fe016ea0ce1c6a2573410000000049454e44ae426082</data> + </image> +</images> +<tabstops> + <tabstop>listBox</tabstop> + <tabstop>iconCanvas</tabstop> + <tabstop>browseButton</tabstop> + <tabstop>searchLine</tabstop> +</tabstops> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>klistbox.h</includehint> + <includehint>kiconcanvas.h</includehint> + <includehint>kiconviewsearchline.h</includehint> + <includehint>kprogress.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/src/likeback.cpp b/src/likeback.cpp new file mode 100644 index 0000000..5beafc8 --- /dev/null +++ b/src/likeback.cpp @@ -0,0 +1,854 @@ +/*************************************************************************** + * Copyright (C) 2006 by Sebastien Laout * + * [email protected] * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Library 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 Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * + ***************************************************************************/ + +#include <kapplication.h> +#include <kaboutdata.h> +#include <kconfig.h> +#include <kaction.h> +#include <kiconloader.h> +#include <kaboutdata.h> +#include <klocale.h> +#include <kdebug.h> +#include <kmessagebox.h> +#include <qlayout.h> +#include <qtoolbutton.h> +#include <kpushbutton.h> +#include <qcheckbox.h> +#include <qradiobutton.h> +#include <qbuttongroup.h> +#include <qvgroupbox.h> +#include <kguiitem.h> +#include <qpopupmenu.h> +#include <qtextedit.h> +#include <qlayout.h> +#include <qlabel.h> +#include <kdialogbase.h> +#include <qhttp.h> +#include <kurl.h> +#include <kinputdialog.h> +#include <qvalidator.h> +#include <qaction.h> +#include <kdebug.h> + +#include <pwd.h> + +#include <iostream> + +#include "likeback.h" +#include "likeback_private.h" + +/****************************************/ +/********** class LikeBackBar: **********/ +/****************************************/ + +LikeBackBar::LikeBackBar(LikeBack *likeBack) + : QWidget(0, "LikeBackBar", Qt::WX11BypassWM | Qt::WStyle_NoBorder | Qt::WNoAutoErase | Qt::WStyle_StaysOnTop | Qt::WStyle_NoBorder | Qt::Qt::WGroupLeader) + , m_likeBack(likeBack) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + + QIconSet likeIconSet = kapp->iconLoader()->loadIconSet("likeback_like", KIcon::Small); + QIconSet dislikeIconSet = kapp->iconLoader()->loadIconSet("likeback_dislike", KIcon::Small); + QIconSet bugIconSet = kapp->iconLoader()->loadIconSet("likeback_bug", KIcon::Small); + QIconSet featureIconSet = kapp->iconLoader()->loadIconSet("likeback_feature", KIcon::Small); + + m_likeButton = new QToolButton(this, "likeback_like"); + m_likeButton->setIconSet(likeIconSet); + m_likeButton->setTextLabel("<p>" + i18n("Send application developers a comment about something you like")); + m_likeButton->setAutoRaise(true); + connect( m_likeButton, SIGNAL(clicked()), this, SLOT(clickedLike()) ); + layout->add(m_likeButton); + + m_dislikeButton = new QToolButton(this, "likeback_dislike"); + m_dislikeButton->setIconSet(dislikeIconSet); + m_dislikeButton->setTextLabel("<p>" + i18n("Send application developers a comment about something you dislike")); + m_dislikeButton->setAutoRaise(true); + connect( m_dislikeButton, SIGNAL(clicked()), this, SLOT(clickedDislike()) ); + layout->add(m_dislikeButton); + + m_bugButton = new QToolButton(this, "likeback_bug"); + m_bugButton->setIconSet(bugIconSet); + m_bugButton->setTextLabel("<p>" + i18n("Send application developers a comment about an improper behavior of the application")); + m_bugButton->setAutoRaise(true); + connect( m_bugButton, SIGNAL(clicked()), this, SLOT(clickedBug()) ); + layout->add(m_bugButton); + + m_featureButton = new QToolButton(this, "likeback_feature"); + m_featureButton->setIconSet(featureIconSet); + m_featureButton->setTextLabel("<p>" + i18n("Send application developers a comment about a new feature you desire")); + m_featureButton->setAutoRaise(true); + connect( m_featureButton, SIGNAL(clicked()), this, SLOT(clickedFeature()) ); + layout->add(m_featureButton); + + connect( &m_timer, SIGNAL(timeout()), this, SLOT(autoMove()) ); + + LikeBack::Button buttons = likeBack->buttons(); + m_likeButton->setShown( buttons & LikeBack::Like ); + m_dislikeButton->setShown( buttons & LikeBack::Dislike ); + m_bugButton->setShown( buttons & LikeBack::Bug ); + m_featureButton->setShown( buttons & LikeBack::Feature ); +} + +LikeBackBar::~LikeBackBar() +{ +} + +void LikeBackBar::startTimer() +{ + m_timer.start(10); +} + +void LikeBackBar::stopTimer() +{ + m_timer.stop(); +} + +void LikeBackBar::autoMove() +{ + static QWidget *lastWindow = 0; + + QWidget *window = kapp->activeWindow(); + // When a Kicker applet has the focus, like the Commandline QLineEdit, + // the systemtray icon indicates to be the current window and the LikeBack is shown next to the system tray icon. + // It's obviously bad ;-) : + bool shouldShow = (m_likeBack->userWantsToShowBar() && m_likeBack->enabledBar() && window && !window->inherits("KSystemTray")); + if (shouldShow) { + //move(window->x() + window->width() - 100 - width(), window->y()); + //move(window->x() + window->width() - 100 - width(), window->mapToGlobal(QPoint(0, 0)).y() - height()); + move(window->mapToGlobal(QPoint(0, 0)).x() + window->width() - width(), window->mapToGlobal(QPoint(0, 0)).y() + 1); + + if (window != lastWindow && m_likeBack->windowNamesListing() != LikeBack::NoListing) { + if (qstricmp(window->name(), "") == 0 || qstricmp(window->name(), "unnamed") == 0) { + std::cout << "===== LikeBack ===== UNNAMED ACTIVE WINDOW OF TYPE " << window->className() << " ======" << LikeBack::activeWindowPath() << std::endl; + } else if (m_likeBack->windowNamesListing() == LikeBack::AllWindows) { + std::cout << "LikeBack: Active Window: " << LikeBack::activeWindowPath() << std::endl; + } + } + lastWindow = window; + } + + // Show or hide the bar accordingly: + if (shouldShow && !isShown()) { + show(); + } else if (!shouldShow && isShown()) { + hide(); + } +} + +void LikeBackBar::clickedLike() +{ + m_likeBack->execCommentDialog(LikeBack::Like); +} + +void LikeBackBar::clickedDislike() +{ + m_likeBack->execCommentDialog(LikeBack::Dislike); +} + +void LikeBackBar::clickedBug() +{ + m_likeBack->execCommentDialog(LikeBack::Bug); +} + +void LikeBackBar::clickedFeature() +{ + m_likeBack->execCommentDialog(LikeBack::Feature); +} + +/********************************************/ +/********** class LikeBackPrivate: **********/ +/********************************************/ + +LikeBackPrivate::LikeBackPrivate() + : bar(0) + , config(0) + , aboutData(0) + , buttons(LikeBack::DefaultButtons) + , hostName() + , remotePath() + , hostPort(80) + , acceptedLocales() + , acceptedLanguagesMessage() + , windowListing(LikeBack::NoListing) + , showBar(false) + , disabledCount(0) + , fetchedEmail() + , action(0) +{ +} + +LikeBackPrivate::~LikeBackPrivate() +{ + delete bar; + delete action; + + config = 0; + aboutData = 0; +} + +/*************************************/ +/********** class LikeBack: **********/ +/*************************************/ + +LikeBack::LikeBack(Button buttons, bool showBarByDefault, KConfig *config, const KAboutData *aboutData) + : QObject() +{ + // Initialize properties (1/2): + d = new LikeBackPrivate(); + d->buttons = buttons; + d->config = config; + d->aboutData = aboutData; + d->showBarByDefault = showBarByDefault; + + // Use default KApplication config and aboutData if not provided: + if (d->config == 0) + d->config = kapp->config(); + if (d->aboutData == 0) + d->aboutData = kapp->aboutData(); + + // Initialize properties (2/2) [Needs aboutData to be set]: + d->showBar = userWantsToShowBar(); + + // Fetch the KControl user email address as a default one: + if (!emailAddressAlreadyProvided()) + fetchUserEmail(); + + // Initialize the button-bar: + d->bar = new LikeBackBar(this); + d->bar->resize(d->bar->sizeHint()); + + // Show the information message if it is the first time, and if the button-bar is shown: + static const char *messageShown = "LikeBack_starting_information"; + if (d->showBar && KMessageBox::shouldBeShownContinue(messageShown)) { + showInformationMessage(); + KMessageBox::saveDontShowAgainContinue(messageShown); + } + + // Show the bar if that's wanted by the developer or the user: + if (d->showBar) + QTimer::singleShot( 0, d->bar, SLOT(startTimer()) ); + +#if 0 + disableBar(); + // Alex: Oh, it drove me nuts + d->buttons = (Button) ( 0); showInformationMessage(); + d->buttons = (Button) ( Feature); showInformationMessage(); + d->buttons = (Button) ( Bug ); showInformationMessage(); + d->buttons = (Button) ( Bug | Feature); showInformationMessage(); + d->buttons = (Button) ( Dislike ); showInformationMessage(); + d->buttons = (Button) ( Dislike | Feature); showInformationMessage(); + d->buttons = (Button) ( Dislike | Bug ); showInformationMessage(); + d->buttons = (Button) ( Dislike | Bug | Feature); showInformationMessage(); + d->buttons = (Button) (Like ); showInformationMessage(); + d->buttons = (Button) (Like | Feature); showInformationMessage(); + d->buttons = (Button) (Like | Bug ); showInformationMessage(); + d->buttons = (Button) (Like | Bug | Feature); showInformationMessage(); + d->buttons = (Button) (Like | Dislike ); showInformationMessage(); + d->buttons = (Button) (Like | Dislike | Feature); showInformationMessage(); + d->buttons = (Button) (Like | Dislike | Bug ); showInformationMessage(); + d->buttons = (Button) (Like | Dislike | Bug | Feature); showInformationMessage(); + enableBar(); +#endif +} + +LikeBack::~LikeBack() +{ + delete d; +} + +void LikeBack::setWindowNamesListing(WindowListing windowListing) +{ + d->windowListing = windowListing; +} + +LikeBack::WindowListing LikeBack::windowNamesListing() +{ + return d->windowListing; +} + +void LikeBack::setAcceptedLanguages(const QStringList &locales, const QString &message) +{ + d->acceptedLocales = locales; + d->acceptedLanguagesMessage = message; +} + +QStringList LikeBack::acceptedLocales() +{ + return d->acceptedLocales; +} + +QString LikeBack::acceptedLanguagesMessage() +{ + return d->acceptedLanguagesMessage; +} + +void LikeBack::setServer(const QString &hostName, const QString &remotePath, Q_UINT16 hostPort) +{ + d->hostName = hostName; + d->remotePath = remotePath; + d->hostPort = hostPort; +} + +QString LikeBack::hostName() +{ + return d->hostName; +} + +QString LikeBack::remotePath() +{ + return d->remotePath; +} + +Q_UINT16 LikeBack::hostPort() +{ + return d->hostPort; +} + +void LikeBack::disableBar() +{ + d->disabledCount++; + if (d->bar && d->disabledCount > 0) { + d->bar->hide(); + d->bar->stopTimer(); + } +} + +void LikeBack::enableBar() +{ + d->disabledCount--; + if (d->disabledCount < 0) + std::cerr << "===== LikeBack ===== Enabled more times than it was disabled. Please refer to the disableBar() documentation for more information and hints." << std::endl; + if (d->bar && d->disabledCount <= 0) { + d->bar->startTimer(); + } +} + +bool LikeBack::enabledBar() +{ + return d->disabledCount <= 0; +} + +void LikeBack::execCommentDialog(Button type, const QString &initialComment, const QString &windowPath, const QString &context) +{ + disableBar(); + LikeBackDialog dialog(type, initialComment, windowPath, context, this); + dialog.exec(); + enableBar(); +} + +void LikeBack::execCommentDialogFromHelp() +{ + execCommentDialog(AllButtons, /*initialComment=*/"", /*windowPath=*/"HelpMenuAction"); +} + +LikeBack::Button LikeBack::buttons() +{ + return d->buttons; +} + +const KAboutData* LikeBack::aboutData() +{ + return d->aboutData; +} + +KConfig* LikeBack::config() +{ + return d->config; +} + +KAction* LikeBack::sendACommentAction(KActionCollection *parent) +{ + if (d->action == 0) + d->action = new KAction( + i18n("&Send a Comment to Developers"), /*icon=*/"mail_new", /*shortcut=*/"", + this, SLOT(execCommentDialog()), + parent, "likeback_send_a_comment" + ); + + return d->action; +} + +bool LikeBack::userWantsToShowBar() +{ + // Store the button-bar per version, so it can be disabled by the developer for the final version: + d->config->setGroup("LikeBack"); + return d->config->readBoolEntry("userWantToShowBarForVersion_" + d->aboutData->version(), d->showBarByDefault); +} + +void LikeBack::setUserWantsToShowBar(bool showBar) +{ + if (showBar == d->showBar) + return; + + d->showBar = showBar; + + // Store the button-bar per version, so it can be disabled by the developer for the final version: + d->config->setGroup("LikeBack"); + d->config->writeEntry("userWantToShowBarForVersion_" + d->aboutData->version(), showBar); + d->config->sync(); // Make sure the option is saved, even if the application crashes after that. + + if (showBar) + d->bar->startTimer(); +} + +void LikeBack::showInformationMessage() +{ + // Load and register the images needed by the message: + QPixmap likeIcon = kapp->iconLoader()->loadIcon("likeback_like", KIcon::Small); + QPixmap dislikeIcon = kapp->iconLoader()->loadIcon("likeback_dislike", KIcon::Small); + QPixmap bugIcon = kapp->iconLoader()->loadIcon("likeback_bug", KIcon::Small); + QPixmap featureIcon = kapp->iconLoader()->loadIcon("likeback_feature", KIcon::Small); + QMimeSourceFactory::defaultFactory()->setPixmap("likeback_icon_like", likeIcon); + QMimeSourceFactory::defaultFactory()->setPixmap("likeback_icon_dislike", dislikeIcon); + QMimeSourceFactory::defaultFactory()->setPixmap("likeback_icon_bug", bugIcon); + QMimeSourceFactory::defaultFactory()->setPixmap("likeback_icon_feature", featureIcon); + + // Show a message reflecting the allowed types of comment: + Button buttons = d->buttons; + int nbButtons = (buttons & Like ? 1 : 0) + + (buttons & Dislike ? 1 : 0) + + (buttons & Bug ? 1 : 0) + + (buttons & Feature ? 1 : 0); + KMessageBox::information(0, + "<p><b>" + (isDevelopmentVersion(d->aboutData->version()) ? + i18n("Welcome to this testing version of %1.") : + i18n("Welcome to %1.") + ).arg(d->aboutData->programName()) + "</b></p>" + "<p>" + i18n("To help us improve it, your comments are important.") + "</p>" + "<p>" + + ((buttons & LikeBack::Like) && (buttons & LikeBack::Dislike) ? + i18n("Each time you have a great or frustrating experience, " + "please click the appropriate face below the window title-bar, " + "briefly describe what you like or dislike and click Send.") + : (buttons & LikeBack::Like ? + i18n("Each time you have a great experience, " + "please click the smiling face below the window title-bar, " + "briefly describe what you like and click Send.") + : (buttons & LikeBack::Dislike ? + i18n("Each time you have a frustrating experience, " + "please click the frowning face below the window title-bar, " + "briefly describe what you dislike and click Send.") + : + QString() + ))) + "</p>" + + (buttons & LikeBack::Bug ? + "<p>" + + (buttons & (LikeBack::Like | LikeBack::Dislike) ? + i18n("Follow the same principle to quickly report a bug: " + "just click the broken-object icon in the top-right corner of the window, describe it and click Send.") + : + i18n("Each time you discover a bug in the application, " + "please click the broken-object icon below the window title-bar, " + "briefly describe the mis-behaviour and click Send.") + ) + "</p>" + : "") + + "<p>" + i18n("Example:", "Examples:", nbButtons) + "</p>" + + (buttons & LikeBack::Like ? + "<p><img source=\"likeback_icon_like\"> " + + i18n("<b>I like</b> the new artwork. Very refreshing.") + "</p>" + : "") + + (buttons & LikeBack::Dislike ? + "<p><img source=\"likeback_icon_dislike\"> " + + i18n("<b>I dislike</b> the welcome page of that assistant. Too time consuming.") + "</p>" + : "") + + (buttons & LikeBack::Bug ? + "<p><img source=\"likeback_icon_bug\"> " + + i18n("<b>The application has an improper behaviour</b> when clicking the Add button. Nothing happens.") + "</p>" + : "") + + (buttons & LikeBack::Feature ? + "<p><img source=\"likeback_icon_feature\"> " + + i18n("<b>I desire a new feature</b> allowing me to send my work by email.") + "</p>" + : "") + + "</tr></table>", + i18n("Help Improve the Application")); + + // Reset the images from the factory: + QMimeSourceFactory::defaultFactory()->setData("likeback_icon_like", 0L); + QMimeSourceFactory::defaultFactory()->setData("likeback_icon_dislike", 0L); + QMimeSourceFactory::defaultFactory()->setData("likeback_icon_bug", 0L); + QMimeSourceFactory::defaultFactory()->setData("likeback_icon_feature", 0L); +} + +QString LikeBack::activeWindowPath() +{ + // Compute the window hierarchy (from the latest to the oldest): + QStringList windowNames; + QWidget *window = kapp->activeWindow(); + while (window) { + QString name = window->name(); + // Append the class name to the window name if it is unnamed: + if (name == "unnamed") + name += QString(":") + window->className(); + windowNames.append(name); + window = dynamic_cast<QWidget*>(window->parent()); + } + + // Create the string of windows starting by the end (from the oldest to the latest): + QString windowPath; + for (int i = ((int)windowNames.count()) - 1; i >= 0; i--) { + if (windowPath.isEmpty()) + windowPath = windowNames[i]; + else + windowPath += QString("~~") + windowNames[i]; + } + + // Finally return the computed path: + return windowPath; +} + +bool LikeBack::emailAddressAlreadyProvided() +{ + d->config->setGroup("LikeBack"); + return d->config->readBoolEntry("emailAlreadyAsked", false); +} + +QString LikeBack::emailAddress() +{ + if (!emailAddressAlreadyProvided()) + askEmailAddress(); + + d->config->setGroup("LikeBack"); + return d->config->readEntry("emailAddress", ""); +} + +void LikeBack::setEmailAddress(const QString &address, bool userProvided) +{ + d->config->setGroup("LikeBack"); + d->config->writeEntry("emailAddress", address); + d->config->writeEntry("emailAlreadyAsked", userProvided || emailAddressAlreadyProvided()); + d->config->sync(); // Make sure the option is saved, even if the application crashes after that. +} + +void LikeBack::askEmailAddress() +{ + d->config->setGroup("LikeBack"); + + QString currentEmailAddress = d->config->readEntry("emailAddress", ""); + if (!emailAddressAlreadyProvided() && !d->fetchedEmail.isEmpty()) + currentEmailAddress = d->fetchedEmail; + + bool ok; + + QString emailExpString = "[\\w-\\.]+@[\\w-\\.]+\\.[\\w]+"; + //QString namedEmailExpString = "[.]*[ \\t]+<" + emailExpString + '>'; + //QRegExp emailExp("^(|" + emailExpString + '|' + namedEmailExpString + ")$"); + QRegExp emailExp("^(|" + emailExpString + ")$"); + QRegExpValidator emailValidator(emailExp, this); + + disableBar(); + QString email = KInputDialog::getText( + i18n("Email Address"), + "<p><b>" + i18n("Please provide your email address.") + "</b></p>" + + "<p>" + i18n("It will only be used to contact you back if more information is needed about your comments, ask you how to reproduce the bugs you report, send bug corrections for you to test, etc.") + "</p>" + + "<p>" + i18n("The email address is optional. If you do not provide any, your comments will be sent anonymously.") + "</p>", + currentEmailAddress, &ok, kapp->activeWindow(), /*name=*/(const char*)0, &emailValidator); + enableBar(); + + if (ok) + setEmailAddress(email); +} + +// FIXME: Should be moved to KAboutData? Cigogne will also need it. +bool LikeBack::isDevelopmentVersion(const QString &version) +{ + return version.find("alpha", /*index=*/0, /*caseSensitive=*/false) != -1 || + version.find("beta", /*index=*/0, /*caseSensitive=*/false) != -1 || + version.find("rc", /*index=*/0, /*caseSensitive=*/false) != -1 || + version.find("svn", /*index=*/0, /*caseSensitive=*/false) != -1 || + version.find("cvs", /*index=*/0, /*caseSensitive=*/false) != -1; +} + +/** + * Code from KBugReport::slotConfigureEmail() in kdeui/kbugreport.cpp: + */ +/*void LikeBack::beginFetchingEmail() +{ + if (m_process) + return; + m_process = new KProcess(); + *m_process << QString::fromLatin1("kcmshell") << QString::fromLatin1("kcm_useraccount"); + connect( m_process, SIGNAL(processExited(KProcess*)), SLOT(fetchUserEmail()) ); + if (!m_process->start()) { + kdDebug() << "Couldn't start kcmshell.." << endl; + delete m_process; + m_process = 0; + return; + } +// m_configureEmail->setEnabled(false); +}*/ + +/** + * Code from KBugReport::slotSetFrom() in kdeui/kbugreport.cpp: + */ +void LikeBack::fetchUserEmail() +{ +// delete m_process; +// m_process = 0; +// m_configureEmail->setEnabled(true); + + // ### KDE4: why oh why is KEmailSettings in kio? + KConfig emailConf( QString::fromLatin1("emaildefaults") ); + + // find out the default profile + emailConf.setGroup(QString::fromLatin1("Defaults")); + QString profile = QString::fromLatin1("PROFILE_"); + profile += emailConf.readEntry(QString::fromLatin1("Profile"), QString::fromLatin1("Default")); + + emailConf.setGroup(profile); + QString fromaddr = emailConf.readEntry(QString::fromLatin1("EmailAddress")); + if (fromaddr.isEmpty()) { + struct passwd *p; + p = getpwuid(getuid()); + d->fetchedEmail = QString::fromLatin1(p->pw_name); + } else { + QString name = emailConf.readEntry(QString::fromLatin1("FullName")); + if (!name.isEmpty()) + d->fetchedEmail = /*name + QString::fromLatin1(" <") +*/ fromaddr /*+ QString::fromLatin1(">")*/; + } +// m_from->setText( fromaddr ); +} + +/*******************************************/ +/********** class LikeBackDialog: **********/ +/*******************************************/ + +LikeBackDialog::LikeBackDialog(LikeBack::Button reason, const QString &initialComment, const QString &windowPath, const QString &context, LikeBack *likeBack) + : KDialogBase(KDialogBase::Swallow, i18n("Send a Comment to Developers"), KDialogBase::Ok | KDialogBase::Cancel | KDialogBase::Default, + KDialogBase::Ok, kapp->activeWindow(), /*name=*/"_likeback_feedback_window_", /*modal=*/true, /*separator=*/true) + , m_likeBack(likeBack) + , m_windowPath(windowPath) + , m_context(context) +{ + // If no specific "reason" is provided, choose the first one: + if (reason == LikeBack::AllButtons) { + LikeBack::Button buttons = m_likeBack->buttons(); + int firstButton = 0; + if (firstButton == 0 && (buttons & LikeBack::Like)) firstButton = LikeBack::Like; + if (firstButton == 0 && (buttons & LikeBack::Dislike)) firstButton = LikeBack::Dislike; + if (firstButton == 0 && (buttons & LikeBack::Bug)) firstButton = LikeBack::Bug; + if (firstButton == 0 && (buttons & LikeBack::Feature)) firstButton = LikeBack::Feature; + reason = (LikeBack::Button) firstButton; + } + + // If no window path is provided, get the current active window path: + if (m_windowPath.isEmpty()) + m_windowPath = LikeBack::activeWindowPath(); + + QWidget *page = new QWidget(this); + QVBoxLayout *pageLayout = new QVBoxLayout(page, /*margin=*/0, spacingHint()); + + // The introduction message: + QLabel *introduction = new QLabel(introductionText(), page); + pageLayout->addWidget(introduction); + + // The comment group: + m_group = new QButtonGroup(0);//i18n("Send Application Developers a Comment About:"), page); + QVGroupBox *box = new QVGroupBox(i18n("Send Application Developers a Comment About:"), page); + pageLayout->addWidget(box); + + // The radio buttons: + QWidget *buttons = new QWidget(box); + QGridLayout *buttonsGrid = new QGridLayout(buttons, /*nbRows=*/4, /*nbColumns=*/2, /*margin=*/0, spacingHint()); + if (m_likeBack->buttons() & LikeBack::Like) { + QPixmap likePixmap = kapp->iconLoader()->loadIcon("likeback_like", KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true); + QLabel *likeIcon = new QLabel(buttons); + likeIcon->setPixmap(likePixmap); + likeIcon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + QRadioButton *likeButton = new QRadioButton(i18n("Something you &like"), buttons); + buttonsGrid->addWidget(likeIcon, /*row=*/0, /*column=*/0); + buttonsGrid->addWidget(likeButton, /*row=*/0, /*column=*/1); + m_group->insert(likeButton, LikeBack::Like); + } + if (m_likeBack->buttons() & LikeBack::Dislike) { + QPixmap dislikePixmap = kapp->iconLoader()->loadIcon("likeback_dislike", KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true); + QLabel *dislikeIcon = new QLabel(buttons); + dislikeIcon->setPixmap(dislikePixmap); + dislikeIcon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + QRadioButton *dislikeButton = new QRadioButton(i18n("Something you &dislike"), buttons); + buttonsGrid->addWidget(dislikeIcon, /*row=*/1, /*column=*/0); + buttonsGrid->addWidget(dislikeButton, /*row=*/1, /*column=*/1); + m_group->insert(dislikeButton, LikeBack::Dislike); + } + if (m_likeBack->buttons() & LikeBack::Bug) { + QPixmap bugPixmap = kapp->iconLoader()->loadIcon("likeback_bug", KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true); + QLabel *bugIcon = new QLabel(buttons); + bugIcon->setPixmap(bugPixmap); + bugIcon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + QRadioButton *bugButton = new QRadioButton(i18n("An improper &behavior of this application"), buttons); + buttonsGrid->addWidget(bugIcon, /*row=*/2, /*column=*/0); + buttonsGrid->addWidget(bugButton, /*row=*/2, /*column=*/1); + m_group->insert(bugButton, LikeBack::Bug); + } + if (m_likeBack->buttons() & LikeBack::Feature) { + QPixmap featurePixmap = kapp->iconLoader()->loadIcon("likeback_feature", KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true); + QLabel *featureIcon = new QLabel(buttons); + featureIcon->setPixmap(featurePixmap); + featureIcon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + QRadioButton *featureButton = new QRadioButton(i18n("A new &feature you desire"), buttons); + buttonsGrid->addWidget(featureIcon, /*row=*/3, /*column=*/0); + buttonsGrid->addWidget(featureButton, /*row=*/3, /*column=*/1); + m_group->insert(featureButton, LikeBack::Feature); + } + m_group->setButton(reason); + + // The comment text box: + m_comment = new QTextEdit(box); + m_comment->setTabChangesFocus(true); + m_comment->setTextFormat(QTextEdit::PlainText); + m_comment->setText(initialComment); + + m_showButtons = new QCheckBox(i18n("Show comment buttons below &window titlebars"), page); + m_showButtons->setChecked(m_likeBack->userWantsToShowBar()); + pageLayout->addWidget(m_showButtons); + connect( m_showButtons, SIGNAL(stateChanged(int)), this, SLOT(changeButtonBarVisible()) ); + + setButtonOK(KGuiItem(i18n("&Send Comment"), "mail_send")); + enableButtonOK(false); + connect( m_comment, SIGNAL(textChanged()), this, SLOT(commentChanged()) ); + + setButtonGuiItem(Default, KGuiItem(i18n("&Email Address..."), "mail_generic")); + + resize(QSize(kapp->desktop()->width() * 1 / 2, kapp->desktop()->height() * 3 / 5).expandedTo(sizeHint())); + + QAction *sendShortcut = new QAction(this); + sendShortcut->setAccel(QString("Ctrl+Return")); + connect( sendShortcut, SIGNAL(activated()), actionButton(Ok), SLOT(animateClick()) ); + + setMainWidget(page); +} + +LikeBackDialog::~LikeBackDialog() +{ +} + +QString LikeBackDialog::introductionText() +{ + QString text = "<p>" + i18n("Please provide a brief description of your opinion of %1.").arg(m_likeBack->aboutData()->programName()) + " "; + + QString languagesMessage = ""; + if (!m_likeBack->acceptedLocales().isEmpty() && !m_likeBack->acceptedLanguagesMessage().isEmpty()) { + languagesMessage = m_likeBack->acceptedLanguagesMessage(); + QStringList locales = m_likeBack->acceptedLocales(); + for (QStringList::Iterator it = locales.begin(); it != locales.end(); ++it) { + QString locale = *it; + if (KGlobal::locale()->language().startsWith(locale)) + languagesMessage = ""; + } + } else { + if (!KGlobal::locale()->language().startsWith("en")) + languagesMessage = i18n("Please write in English."); + } + + if (!languagesMessage.isEmpty()) + // TODO: Replace the URL with a localized one: + text += languagesMessage + " " + + i18n("You may be able to use an <a href=\"%1\">online translation tool</a>.") + .arg("http://www.google.com/language_tools?hl=" + KGlobal::locale()->language()) + + " "; + + // If both "I Like" and "I Dislike" buttons are shown and one is clicked: + if ((m_likeBack->buttons() & LikeBack::Like) && (m_likeBack->buttons() & LikeBack::Dislike)) + text += i18n("To make the comments you send more useful in improving this application, try to send the same amount of positive and negative comments.") + " "; + + if (!(m_likeBack->buttons() & LikeBack::Feature)) + text += i18n("Do <b>not</b> ask for new features: your requests will be ignored."); + + return text; +} + +void LikeBackDialog::polish() +{ + KDialogBase::polish(); + m_comment->setFocus(); +} + +void LikeBackDialog::slotDefault() +{ + m_likeBack->askEmailAddress(); +} + +void LikeBackDialog::slotOk() +{ + send(); +} + +void LikeBackDialog::changeButtonBarVisible() +{ + m_likeBack->setUserWantsToShowBar(m_showButtons->isChecked()); +} + +void LikeBackDialog::commentChanged() +{ + QPushButton *sendButton = actionButton(Ok); + sendButton->setEnabled(!m_comment->text().isEmpty()); +} + +void LikeBackDialog::send() +{ + QString emailAddress = m_likeBack->emailAddress(); + + int reason = m_group->selectedId(); + QString type = (reason == LikeBack::Like ? "Like" : (reason == LikeBack::Dislike ? "Dislike" : (reason == LikeBack::Bug ? "Bug" : "Feature"))); + QString data = + "protocol=" + KURL::encode_string("1.0") + '&' + + "type=" + KURL::encode_string(type) + '&' + + "version=" + KURL::encode_string(m_likeBack->aboutData()->version()) + '&' + + "locale=" + KURL::encode_string(KGlobal::locale()->language()) + '&' + + "window=" + KURL::encode_string(m_windowPath) + '&' + + "context=" + KURL::encode_string(m_context) + '&' + + "comment=" + KURL::encode_string(m_comment->text()) + '&' + + "email=" + KURL::encode_string(emailAddress); + QHttp *http = new QHttp(m_likeBack->hostName(), m_likeBack->hostPort()); + + std::cout << "http://" << m_likeBack->hostName() << ":" << m_likeBack->hostPort() << m_likeBack->remotePath() << std::endl; + std::cout << data << std::endl; + connect( http, SIGNAL(requestFinished(int, bool)), this, SLOT(requestFinished(int, bool)) ); + + QHttpRequestHeader header("POST", m_likeBack->remotePath()); + header.setValue("Host", m_likeBack->hostName()); + header.setValue("Content-Type", "application/x-www-form-urlencoded"); + http->setHost(m_likeBack->hostName()); + http->request(header, data.utf8()); + + m_comment->setEnabled(false); +} + +void LikeBackDialog::requestFinished(int /*id*/, bool error) +{ + // TODO: Save to file if error (connection not present at the moment) + m_comment->setEnabled(true); + m_likeBack->disableBar(); + if (error) { + KMessageBox::error(this, i18n("<p>Error while trying to send the report.</p><p>Please retry later.</p>"), i18n("Transfer Error")); + } else { + KMessageBox::information( + this, + i18n("<p>Your comment has been sent successfully. It will help improve the application.</p><p>Thanks for your time.</p>"), + i18n("Comment Sent") + ); + close(); + } + m_likeBack->enableBar(); + + KDialogBase::slotOk(); +} + +#include "likeback_private.moc.cpp" +#include "likeback.moc" diff --git a/src/likeback.h b/src/likeback.h new file mode 100644 index 0000000..4b825a5 --- /dev/null +++ b/src/likeback.h @@ -0,0 +1,364 @@ +/*************************************************************************** + * Copyright (C) 2006 by Sebastien Laout * + * [email protected] * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Library 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 Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * + ***************************************************************************/ + +#ifndef LIKEBACK_H +#define LIKEBACK_H + +#include <qobject.h> + +class KConfig; +class KAboutData; +class KAction; +class KActionCollection; + +class LikeBackPrivate; +class LikeBackBar; +class LikeBackDialog; + +/** + * @short System to Get Quick Feedback from Beta-Testers + * + * This system allows users to communicate theire liking of the application to its developers. + * Thus, developers know what theire users prefer of theire applications, what should be enhanced, etc. + * + * Basically, how does it work? + * Whenever the user notice something good he appreciate or something he do not like, do not understand, do not find polished... + * he can send a few short words to the developers to tell them what he like or do not like. It is only two or three clicks away. + * It is fast and efficient. + * + * This greatly lowers the communication barrier between the application developers and the application users. + * It makes the developers understand and satisfy better the needs of the users. + * + * The LikeBack system has 5 components: + * @li In the application: The comment dialog, where the user write a comment, select a type of comment, etc. + * @li In the application: The KAction to plug in the Help menu. This action displays the comment dialog. + * @li In the application: The button-bar, that floats bellow titlebar of every windows of the application, and let the user to quickly show the comment dialog. + * The button-bar can be hidden. + * @li On the server: A PHP script that collects every comments that users send. The LikeBack object should be configured to contact that server. + * @li On the server: The developer interface. It lists every comments that were sent, let you sort them, add remarks to them, and mark them as fixed or another status. + * + * Here is an example of code to call to quickly setup LikeBack on the client: + * @code + * // Instanciate the LikeBack system, and show the first-use information dialog if the button-bar is shown: + * LikeBack *likeBack = new LikeBack(LikeBack::AllButtons, LikeBack::isDevelopmentVersion(kapp->aboutData->version())); // Show button-bar only in beta-versions + * likeBack->setServer("myapp.kde.org", "/likeback/send.php"); + * likeBack->setAcceptedLanguages(QStringList::split(";", "en;fr"), i18n("Please write in English or French.")); + * + * // Comment the following line once you are sure all your windows have a name: + * likeBack->setWindowNamesListing(LikeBack::WarnUnnamedWindows); + * + * // This line should be called early in your KMainWindow constructor because it references actionCollection(). + * // It should be called before createGUI() for the action to be plugged in the Help menu: + * likeBack->sendACommentAction(actionCollection()); + * @endcode + * + * @see Visit http://basket.kde.org/likeback.php for more information, screenshots, a tutorial, hints, return of experiences, and to download the server-side developer interface... + * @author Sebastien Laout <[email protected]> + */ +class LikeBack : public QObject +{ + Q_OBJECT + public: + /** + * Ids of every LikeBack buttons the button-bar can have. + * The four first values are each individual buttons you can enable or not. + * The next ones are combinations: all buttons at once, and the default set of buttons (Like, Dislike). + * Those values are used in the constructor, to set the allowed type of comments, and when triggering the comment dialog, to set the default checked type. + * @See The LikeBack constructor and execCommentDialog(). + */ + enum Button { + Like = 0x01, /// The user select that option to report a positive experience he got with the application. + Dislike = 0x02, /// The user select that option to report a frustrating experience he got with the application. + Bug = 0x04, /// The user select that option to report a bug in the application. + Feature = 0x10, /// The user select that option to ask for a new feature he desire. + /// If not enabled, the user is explicitely informed she cannot ask for new features. + AllButtons = Like | Dislike | Bug | Feature, /// Usable in the constructor to enable every posible buttons. + DefaultButtons = Like | Dislike /// Usable in the constructor to enable only the recommended default set of buttons. + }; + + /** + * Flags letting LikeBack print out name and path of each window you show during execution, for debugging purpose. + * @See The method setWindowNamesListing() explains how to use those values. + */ + enum WindowListing { + NoListing = 0, /// Do not print out any window name. For release time. + WarnUnnamedWindows = 1, /// Each time the user option a window, print out a message if the window is unnamed. For development needs, to check windows. + AllWindows = 2 /// Print out the window hierarchy of each opened windows during execution. For development needs, to check every windows have an understandable name. + }; + + /** + * You only need to call the constructor once, typically in main.cpp. + * Even if you do not show the button-bar by default, you should instanciate LikeBack, + * to include its action in the Help menu of your application, to let the users send comments or activate the bar. + * @param buttons The types of comments you want to get. Determine which radio-buttons are shown in the comment dialog, + * and which ones are displayed in the button-bar. Default buttons do not show the Bug and Feature buttons because you are + * likely to already have a way to get bug and feature reports (most of the time, it is a bugs.kde.org account). + * If you do not have that, then use the value LikeBack::AllButtons to show every possible buttons. + * @param showBarByDefault Determines if the floating button-bar should also be shown, in addition to the action in the Help menu. + * Advise: to avoid getting too much noise, enable it only if it is a small application or a development release. + * Notes: This is only a default value, the user will be able to enable or disabled the bar afterward. + * The button-bar display is stored by version. On a new version, your default value will take effect again. + * This allow you to disable the button-bar once the version is stable enought to be released as final. + * @param config Set the configuration file where to store the user email address and if the button-bar should be shown. + * By default (null), the KApplication configuration object is used. + * @param aboutData Set the KAboutData instance used to get the application name and version. By default (null), the KApplication about data object is used. + * The application name is only used in the first-use information message. + * The version is used to store the button-bar visibility per version (can be shown in a development version but not in a final one...) + * and to send with the comment, so you can filter per version and know if a comment refers the latest version of the application or not. + */ + LikeBack(Button buttons = DefaultButtons, bool showBarByDefault = false, KConfig *config = 0, const KAboutData *aboutData = 0); + + /** + * Destructor. + * Also hide the button-bar, if it was shown. + * Be careful, the KAction is deleted. Do not use it afterward, and take care to unplug it before destroying this LikeBack instance. + */ + ~LikeBack(); + + /** + * This method is interesting while setting up the system for the first time. + * LikeBack send the current window name (and hierarchy) with the comment. This allows you to put the comments in theire context. + * So, of course, you are encouraged to give a name to your windows. It is done in the constructor of the widgets. + * This method allows to output the name of the current window to the standard output. + * So you can use the application, open all the windows, and when you see a warning, you know which window you should assign a name. + * @see The WindowListing flags for an enumeration and explaining of every possibilities. + * @Note If you do not name your windows, the name of the classes will be sent. So it is not that grave. + */ + void setWindowNamesListing(WindowListing windowListing); + + /** + * @Returns The window listing flag. + * @see setWindowNamesListing() + */ + WindowListing windowNamesListing(); + + /** + * By default, only English comments are accepted. The user is informed she must write in this language by a sentence placed in the comment dialog. + * If you have people talking other languages in your development team, it can be interesting to call this method to define the accepted locales (languages), + * and provide a message to inform users. The developer interface on the server let developers view comments in theire locale. + * Note that no verification is done to check if the user used the right language, it would be impossible. + * The list of locales is there to make it possible to NOT show the message for users of the accepted languages. + * For instance, if you accept only English and French, and that the application run in a French environment, + * it is likely the user is French and will write comments using French. Telling him he should write in French is unnecessary and redundant. + * Passing an empty list and an empty string to the method will make LikeBack display the default message telling the user only English is accepted. + * Example of call you can quickly copy, paste and adapt: + * @code + * likeBack->setAcceptedLanguages(QStringList::split(";", "en;fr"), i18n("Please write in English or French.")); + * @endcode + * @Note During tests, if you do not see the sentence, it is because you are running the application with an "accepted language": do not be surprised ;-) + * @param locales The list of locales where the message does not need to be shown. See TODO TODO for a list of available locales for you to choose. + * @param message The message to displays to the user to tell him what languages are accepted to write his comments. + */ + void setAcceptedLanguages(const QStringList &locales, const QString &message); + + /** + * @Returns The list of accepted locales for the user to write comments. + * @see setAcceptedLanguages() + */ + QStringList acceptedLocales(); + + /** + * @Returns The message displayed to users who are not running the application in an accepted locale. + * @see setAcceptedLanguages() + */ + QString acceptedLanguagesMessage(); + + /** + * Set the path where LikeBack should send every comments. + * It is composed of the server host name, the path to the PHP script used to send comments, and optionnaly a port number if it is not 80. + * This call is mandatory for LikeBack to work. + * @param hostName The server host name to contact when sending comments. For instance "myapp.kde.org". + * @param remotePath The path to the send script on the server. For instance, "/likeback/send.php". + * @param hostPort Optionnal port used to contact the server using the HTTP protocol. By default, it is port 80. + */ + void setServer(const QString &hostName, const QString &remotePath, Q_UINT16 hostPort = 80); + + /** + * @Returns The server host name to contact when sending comments. + * @see setServer() + */ + QString hostName(); + + /** + * @Returns The path to the send script on the server. + * @see setServer() + */ + QString remotePath(); + + /** + * @Returns The port used to contact the server using the HTTP protocol. + * @see setServer() + */ + Q_UINT16 hostPort(); + + /** + * Get the KAction letting user to show the comment dialog. + * You should plug it in your Help menu, just bellow the "Report a Bug" action, or replace it. + * Adding the action below "Report a Bug" or replacing "Report a Bug" depends on your application and if you have a Bugzilla account. + * If you do not have a Bugzilla account, LikeBack is a good way for your small application to get bug reports: remove "Report a Bug". + * For more information about how to configure LikeBack depending on your application size and settings, see the constructor documentation. + * @Note The action is named "likeback_send_a_comment". So you should add the following XML in the *ui.rc file of your application: + * @code + * <Action name="likeback_send_a_comment" /> + * @endcode + */ + KAction* sendACommentAction(KActionCollection *parent = 0); + + /** + * @Returns The path of the currently active window. Each windows are separated with "~~". + * Normally, you should not need to call this method since it is used to send the window path. + * But if you call execCommentDialog(), you could need to use it. + */ + static QString activeWindowPath(); + + /** + * @Returns The combination of buttons that are shown in the comment dialog and the button-bar. + */ + Button buttons(); + + /** + * @Returns true if the button-bar is currently enabled. Ie, if it has been re-enabled as many times as it has been disabled. + * @see The method disableBar() for more information on how enabling/disabling works. + */ + bool enabledBar(); + + public slots: + + /** + * Temporarily disable the button-bar: it is hiden from the screen if it was shown. + * Does not affect anything if the user has not choosen to show the button-bar. + * @Note Calls to enableBar() and disableBar() are ref-counted. + * This means that the number of times disableBar() is called is memorized, + * and enableBar() will only have effect after it has been called as many times as disableBar() was called before. + * So, make sure to always call enableBar() the same number of times ou called disableBar(). + * And please make sure to ALWAYS call disableBar() BEFORE enableBar(). + * In the counter-case, another code could call disableBar() and EXCPECT the bar to be disabled. But it will not, because its call only canceled yours. + * @Note Sometimes, you will absolutely need to call enableBar() before disableBar(). + * For instance, MyWindow::show() calls enableBar() and MyWindow::hide() calls disableBar(). + * This is the trick used to show the LikeBack button-bar of a Kontact plugin only when the main widget of that plugin is active. + * In this case, call disableBar() at the begin of your program, so the disable count will never be negative. + * @Note If the bar is enabled, it does not mean the bar is shown. For that, the developer (using showBarByDefault in the construcor) + * or the user (by checking the checkbox in the comment dialog) have to explicitely show the bar. + */ + void disableBar(); + + /** + * Re-enable the button-bar one time. + * @see The method disableBar() for more information on how enabling/disabling works. + */ + void enableBar(); + + /** + * Show the first-use information dialog telling the user the meaning of the LikeBack system and giving examples of every comment types. + */ + void showInformationMessage(); + + /** + * Popup the comment dialog. + * With no parameter, it popups in the default configuration: the first type is checked, empty message, current window path, and empty context. + * You can use the following parameters to customize how it should appears: + * @param type Which radiobutton should be checked when poping up. AllButton, the default value, means the first available type will be checked. + * @param initialComment The text to put in the comment text area. Allows you to popup the dialog in some special circumstances, + * like to let the user report an internal error by populating the comment area with technical details useful for you to debug. + * @param windowPath The window path to send with the comment. If empty (the default), the current window path is took. + * Separate window names with "~~". For instance "MainWindow~~NewFile~~FileOpen". + * If you popup the dialog after an error occurred, you can put the error name in that field (if the window path has no sense in that context). + * When the dialog is popuped up from the sendACommentAction() KAction, this value is "HelpMenu", because there is no way to know if the user + * is commenting a thing he found/thinked about in a sub-dialog. + * @param context Not used for the moment. Will allow more fine-grained application status report. + */ + void execCommentDialog(Button type = AllButtons, const QString &initialComment = "", const QString &windowPath = "", const QString &context = ""); + + /** + * Popups the dialog for the user to set his email address. + * The popup will always be shown, even if the user already provided an email address. + */ + void askEmailAddress(); + + private: + LikeBackPrivate *d; + + /** + * Get the user email address from KControl. + */ + void fetchUserEmail(); + + private slots: + /** + * Slot triggered by the "Help -> Send a Comment to Developers" KAction. + * It popups the comment dialog, and set the window path to "HelpMenuAction", + * because current window path has no meaning in that case. + */ + void execCommentDialogFromHelp(); + + public: + + /** + * @Returns true if the user has enabled the LikeBack bar for this version. + */ + bool userWantsToShowBar(); + + /** + * Explicitely set if the floating button-bar should be shown or not. + * Tehorically, this choice should only be left to the user, + * and to the developers for the default value, already provided in the constructor. + */ + void setUserWantsToShowBar(bool showBar); + + /** + * @Returns A pointer to the KAboutData used to determin the application name and version. + * @See The LikeBack constructor for more information. + */ + const KAboutData *aboutData(); + + /** + * @Returns A pointer to the KConfig used to store user configuration (email address, if the button-bar should be shown). + * @See The LikeBack constructor for more information. + */ + KConfig *config(); + + /** + * During the first comment sending, the user is invited to enter his email address for the developers to be able to contact him back. + * He is only asked once, or he can set or change it by using the bottom-left button in the comment dialog. + * @Returns true if the user has already configured his email address. + */ + bool emailAddressAlreadyProvided(); + + /** + * @Returns The email user address, or ask it to the user if he have not provided or ignored it. + * @Returns An empty string if the user cancelled the request dialog. + */ + QString emailAddress(); + + /** + * Define or re-define the user email address. + * LikeBack will not ask it again to the user, unless you set @p userProvided to false. + * Then, this call can be considered as setting the default email address, that the user should confirm later. + */ + void setEmailAddress(const QString &address, bool userProvided = true); + + /** + * @Returns true if @p version is an Alpha, Beta, RC, SVN or CVS version. + * You can use this static method in the constructor to enable the button-bar by default only during beta-releases. + */ + static bool isDevelopmentVersion(const QString &version); +}; + +#endif // LIKEBACK_H diff --git a/src/likeback_private.h b/src/likeback_private.h new file mode 100644 index 0000000..37ed60f --- /dev/null +++ b/src/likeback_private.h @@ -0,0 +1,105 @@ +/*************************************************************************** + * Copyright (C) 2006 by Sebastien Laout * + * [email protected] * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Library 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 Library General Public * + * License along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * + ***************************************************************************/ + +#ifndef LIKEBACK_PRIVATE_H +#define LIKEBACK_PRIVATE_H + +#include <kdialogbase.h> +#include <qtimer.h> + +#include "likeback.h" + +class QToolButton; +class QTextEdit; +class QCheckBox; +class QButtonGroup; +class Kaction; + +class LikeBackPrivate +{ + public: + LikeBackPrivate(); + ~LikeBackPrivate(); + LikeBackBar *bar; + KConfig *config; + const KAboutData *aboutData; + LikeBack::Button buttons; + QString hostName; + QString remotePath; + Q_UINT16 hostPort; + QStringList acceptedLocales; + QString acceptedLanguagesMessage; + LikeBack::WindowListing windowListing; + bool showBarByDefault; + bool showBar; + int disabledCount; + QString fetchedEmail; + KAction *action; +}; + +class LikeBackBar : public QWidget +{ + Q_OBJECT + public: + LikeBackBar(LikeBack *likeBack); + ~LikeBackBar(); + public slots: + void startTimer(); + void stopTimer(); + private slots: + void autoMove(); + void clickedLike(); + void clickedDislike(); + void clickedBug(); + void clickedFeature(); + private: + LikeBack *m_likeBack; + QTimer m_timer; + QToolButton *m_likeButton; + QToolButton *m_dislikeButton; + QToolButton *m_bugButton; + QToolButton *m_featureButton; +}; + +class LikeBackDialog : public KDialogBase +{ + Q_OBJECT + public: + LikeBackDialog(LikeBack::Button reason, const QString &initialComment, const QString &windowPath, const QString &context, LikeBack *likeBack); + ~LikeBackDialog(); + private: + LikeBack *m_likeBack; + QString m_windowPath; + QString m_context; + QButtonGroup *m_group; + QTextEdit *m_comment; + QCheckBox *m_showButtons; + QString introductionText(); + private slots: + void polish(); + void slotDefault(); + void slotOk(); + void changeButtonBarVisible(); + void commentChanged(); + void send(); + void requestFinished(int id, bool error); +}; + +#endif // LIKEBACK_PRIVATE_H diff --git a/src/linklabel.cpp b/src/linklabel.cpp new file mode 100644 index 0000000..be6e2e2 --- /dev/null +++ b/src/linklabel.cpp @@ -0,0 +1,695 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qlabel.h> +#include <kurl.h> +#include <qlayout.h> +#include <kiconloader.h> +#include <qcursor.h> +#include <klocale.h> +#include <qpushbutton.h> +#include <qcheckbox.h> +#include <qcombobox.h> +#include <qhgroupbox.h> +#include <qpainter.h> +#include <kglobalsettings.h> +#include <qstyle.h> +#include <kapplication.h> +#include <kaboutdata.h> +#include <kdialogbase.h> +#include <kcmodule.h> +#include <kdebug.h> + +#include "linklabel.h" +#include "variouswidgets.h" +#include "tools.h" +#include "global.h" +#include "kcolorcombo2.h" +#include "htmlexporter.h" + +/** LinkLook */ + +LinkLook *LinkLook::soundLook = new LinkLook(/*useLinkColor=*/false, /*canPreview=*/false); +LinkLook *LinkLook::fileLook = new LinkLook(/*useLinkColor=*/false, /*canPreview=*/true); +LinkLook *LinkLook::localLinkLook = new LinkLook(/*useLinkColor=*/true, /*canPreview=*/true); +LinkLook *LinkLook::networkLinkLook = new LinkLook(/*useLinkColor=*/true, /*canPreview=*/false); +LinkLook *LinkLook::launcherLook = new LinkLook(/*useLinkColor=*/true, /*canPreview=*/false); + +LinkLook::LinkLook(bool useLinkColor, bool canPreview) +{ + m_useLinkColor = useLinkColor; + m_canPreview = canPreview; + m_iconSize = 0; +} + +LinkLook::LinkLook(const LinkLook &other) +{ + m_useLinkColor = other.useLinkColor(); + m_canPreview = other.canPreview(); + setLook( other.italic(), other.bold(), other.underlining(), + other.color(), other.hoverColor(), + other.iconSize(), other.preview() ); +} + +void LinkLook::setLook(bool italic, bool bold, int underlining, + QColor color, QColor hoverColor, + int iconSize, int preview) +{ + m_italic = italic; + m_bold = bold; + m_underlining = underlining; + m_color = color; + m_hoverColor = hoverColor; + m_iconSize = iconSize; + m_preview = (canPreview() ? preview : None); +} + +int LinkLook::previewSize() const +{ + if (previewEnabled()) { + switch (preview()) { + default: + case None: return 0; + case IconSize: return iconSize(); + case TwiceIconSize: return iconSize() * 2; + case ThreeIconSize: return iconSize() * 3; + } + } else + return 0; +} + +QColor LinkLook::effectiveColor() const +{ + if (m_color.isValid()) + return m_color; + else + return defaultColor(); +} + +QColor LinkLook::effectiveHoverColor() const +{ + if (m_hoverColor.isValid()) + return m_hoverColor; + else + return defaultHoverColor(); +} + +QColor LinkLook::defaultColor() const +{ + if (m_useLinkColor) + return KGlobalSettings::linkColor(); + else + return KGlobalSettings::textColor(); +} + +QColor LinkLook::defaultHoverColor() const +{ + return Qt::red; +} + +LinkLook* LinkLook::lookForURL(const KURL &url) +{ + return url.isLocalFile() ? localLinkLook : networkLinkLook; +} + +QString LinkLook::toCSS(const QString &cssClass, const QColor &defaultTextColor) const +{ + // Set the link class: + QString css = QString(" .%1 a { display: block; width: 100%;").arg(cssClass); + if (underlineOutside()) + css += " text-decoration: underline;"; + else + css += " text-decoration: none;"; + if (m_italic == true) + css += " font-style: italic;"; + if (m_bold == true) + css += " font-weight: bold;"; + QColor textColor = (color().isValid() || m_useLinkColor ? effectiveColor() : defaultTextColor); + css += QString(" color: %1; }\n").arg(textColor.name()); + + // Set the hover state class: + QString hover; + if (m_underlining == OnMouseHover) + hover = "text-decoration: underline;"; + else if (m_underlining == OnMouseOutside) + hover = "text-decoration: none;"; + if (effectiveHoverColor() != effectiveColor()) { + if (!hover.isEmpty()) + hover += " "; + hover += QString("color: %4;").arg(effectiveHoverColor().name()); + } + + // But include it only if it contain a different style than non-hover state: + if (!hover.isEmpty()) + css += QString(" .%1 a:hover { %2 }\n").arg(cssClass, hover); + + return css; +} + +/** LinkLabel */ + +LinkLabel::LinkLabel(int hAlign, int vAlign, QWidget *parent, const char *name, WFlags f) + : QFrame(parent, name, f), m_isSelected(false), m_isHovered(false), m_look(0) +{ + initLabel(hAlign, vAlign); +} + +LinkLabel::LinkLabel(const QString &title, const QString &icon, LinkLook *look, int hAlign, int vAlign, + QWidget *parent, const char *name, WFlags f) + : QFrame(parent, name, f), m_isSelected(false), m_isHovered(false), m_look(0) +{ + initLabel(hAlign, vAlign); + setLink(title, icon, look); +} + +void LinkLabel::initLabel(int hAlign, int vAlign) +{ + m_layout = new QBoxLayout(this, QBoxLayout::LeftToRight); + m_icon = new QLabel(this); + m_title = new QLabel(this); + m_spacer1 = new QSpacerItem(0, 0, QSizePolicy::Preferred/*Expanding*/, QSizePolicy::Preferred/*Expanding*/); + m_spacer2 = new QSpacerItem(0, 0, QSizePolicy::Preferred/*Expanding*/, QSizePolicy::Preferred/*Expanding*/); + + m_hAlign = hAlign; + m_vAlign = vAlign; + + m_title->setTextFormat(Qt::PlainText); + + // DEGUB: + //m_icon->setPaletteBackgroundColor("lightblue"); + //m_title->setPaletteBackgroundColor("lightyellow"); +} + +LinkLabel::~LinkLabel() +{ +} + +void LinkLabel::setLink(const QString &title, const QString &icon, LinkLook *look) +{ + if (look) + m_look = look; // Needed for icon size + + m_title->setText(title); + m_title->setShown( ! title.isEmpty() ); + + if (icon.isEmpty()) + m_icon->clear(); + else { + QPixmap pixmap = DesktopIcon(icon, m_look->iconSize(), m_look->iconSize(), kapp); + if (!pixmap.isNull()) + m_icon->setPixmap(pixmap); + } + m_icon->setShown( ! icon.isEmpty() ); + + if (look) + setLook(look); +} + +void LinkLabel::setLook(LinkLook *look) // FIXME: called externaly (so, without setLink()) it's buggy (icon not +{ + m_look = look; + + QFont font; + font.setBold(look->bold()); + font.setUnderline(look->underlineOutside()); + font.setItalic(look->italic()); + m_title->setFont(font); + m_title->setPaletteForegroundColor( m_isSelected ? KApplication::palette().active().highlightedText() : look->effectiveColor() ); + + m_icon->setShown( m_icon->pixmap() && ! m_icon->pixmap()->isNull() ); + + setAlign(m_hAlign, m_vAlign); +} + +void LinkLabel::setAlign(int hAlign, int vAlign) +{ + m_hAlign = hAlign; + m_vAlign = vAlign; + + if (!m_look) + return; + + // Define alignment flags : + //FIXME TODO: Use directly flags ! + int hFlag, vFlag, wBreak; + switch (hAlign) { + default: + case 0: hFlag = Qt::AlignLeft; break; + case 1: hFlag = Qt::AlignHCenter; break; + case 2: hFlag = Qt::AlignRight; break; + } + switch (vAlign) { + case 0: vFlag = Qt::AlignTop; break; + default: + case 1: vFlag = Qt::AlignVCenter; break; + case 2: vFlag = Qt::AlignBottom; break; + } + wBreak = Qt::WordBreak * (hAlign != 1); + + // Clear the widget : + m_layout->removeItem(m_spacer1); + m_layout->remove(m_icon); + m_layout->remove(m_title); + m_layout->removeItem(m_spacer2); + + // Otherwise, minimumSize will be incoherent (last size ? ) + m_layout->setResizeMode(QLayout::Minimum); + + // And re-populate the widget with the appropriates things and order + bool addSpacers = hAlign == 1; + m_layout->setDirection(QBoxLayout::LeftToRight); + //m_title->setSizePolicy( QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Maximum/*Expanding*/, 0, 0, false) ); + m_icon->setSizePolicy( QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred/*Expanding*/, 0, 0, false) ); + m_spacer1->changeSize( 0, 0, QSizePolicy::Expanding, QSizePolicy::Preferred/*Expanding*/ ); + m_spacer2->changeSize( 0, 0, QSizePolicy::Expanding, QSizePolicy::Preferred/*Expanding*/ ); + + m_icon->setAlignment( hFlag | vFlag ); + m_title->setAlignment( hFlag | vFlag | wBreak ); + if ( addSpacers && (vAlign != 0) || + (m_title->text().isEmpty() && hAlign == 2) ) + m_layout->addItem(m_spacer1); + if (hAlign == 2) { // If align at right, icon is at right + m_layout->addWidget(m_title); + m_layout->addWidget(m_icon); + } else { + m_layout->addWidget(m_icon); + m_layout->addWidget(m_title); + } + if ( addSpacers && (vAlign != 2) || + (m_title->text().isEmpty() && hAlign == 0) ) + m_layout->addItem(m_spacer2); +} + +void LinkLabel::enterEvent(QEvent*) +{ + m_isHovered = true; + if ( ! m_isSelected ) + m_title->setPaletteForegroundColor(m_look->effectiveHoverColor()); + + QFont font = m_title->font(); + font.setUnderline(m_look->underlineInside()); + m_title->setFont(font); +} + +void LinkLabel::leaveEvent(QEvent*) +{ + m_isHovered = false; + if ( ! m_isSelected ) + m_title->setPaletteForegroundColor(m_look->effectiveColor()); + + QFont font = m_title->font(); + font.setUnderline(m_look->underlineOutside()); + m_title->setFont(font); +} + +void LinkLabel::setSelected(bool selected) +{ + m_isSelected = selected; + if (selected) + m_title->setPaletteForegroundColor(KApplication::palette().active().highlightedText()); + else if (m_isHovered) + m_title->setPaletteForegroundColor(m_look->effectiveHoverColor()); + else + m_title->setPaletteForegroundColor(m_look->effectiveColor()); +} + +void LinkLabel::setPaletteBackgroundColor(const QColor &color) +{ + QFrame::setPaletteBackgroundColor(color); + m_title->setPaletteBackgroundColor(color); +} + +int LinkLabel::heightForWidth(int w) const +{ + int iconS = (m_icon->isShown()) ? m_look->iconSize() : 0; // Icon size + int iconW = iconS; // Icon width to remove to w + int titleH = (m_title->isShown()) ? m_title->heightForWidth(w - iconW) : 0; // Title height + + return (titleH >= iconS) ? titleH : iconS; // No margin for the moment ! +} + +QString LinkLabel::toHtml(const QString &imageName) +{ + QString begin = "<font color=" + m_look->effectiveColor().name() + ">"; + QString end = "</font>"; + if (m_look->italic()) { + begin += "<i>"; + end.prepend("</i>"); + } + if (m_look->bold()) { + begin += "<b>"; + end.prepend("</b>"); + } + if (m_look->underlineOutside()) { + begin += "<u>"; + end.prepend("</u>"); + } + if (m_icon->pixmap()) { + QPixmap icon(*m_icon->pixmap()); + begin.prepend("<img src=" + imageName + " style=\"vertical-align: middle\"> "); + QMimeSourceFactory::defaultFactory()->setPixmap(imageName, icon); + } else + QMimeSourceFactory::defaultFactory()->setData(imageName, 0L); + return begin + Tools::textToHTMLWithoutP(m_title->text()) + end; +} + +/** class LinkDisplay + */ + +LinkDisplay::LinkDisplay() + : m_title(), m_icon(), m_preview(), m_look(0), m_font(), m_minWidth(0), m_width(0), m_height(0) +{ +} + +void LinkDisplay::setLink(const QString &title, const QString &icon, LinkLook *look, const QFont &font) +{ + setLink(title, icon, m_preview, look, font); +} + +void LinkDisplay::setLink(const QString &title, const QString &icon, const QPixmap &preview, LinkLook *look, const QFont &font) +{ + m_title = title; + m_icon = icon; + m_preview = preview; + m_look = look; + m_font = font; + + // "Constants": + int BUTTON_MARGIN = kapp->style().pixelMetric(QStyle::PM_ButtonMargin); + int LINK_MARGIN = BUTTON_MARGIN + 2; + + // Recompute m_minWidth: + QRect textRect = QFontMetrics(labelFont(font, false)).boundingRect(0, 0, /*width=*/1, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, m_title); + int iconPreviewWidth = QMAX(m_look->iconSize(), (m_look->previewEnabled() ? m_preview.width() : 0)); + m_minWidth = BUTTON_MARGIN - 1 + iconPreviewWidth + LINK_MARGIN + textRect.width(); + // Recompute m_maxWidth: + textRect = QFontMetrics(labelFont(font, false)).boundingRect(0, 0, /*width=*/50000000, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, m_title); + m_maxWidth = BUTTON_MARGIN - 1 + iconPreviewWidth + LINK_MARGIN + textRect.width(); + // Adjust m_width: + if (m_width < m_minWidth) + setWidth(m_minWidth); + // Recompute m_height: + m_height = heightForWidth(m_width); +} + +void LinkDisplay::setWidth(int width) +{ + if (width < m_minWidth) + width = m_minWidth; + + if (width != m_width) { + m_width = width; + m_height = heightForWidth(m_width); + } +} + +/** Paint on @p painter + * in (@p x, @p y, @p width, @p height) + * using @p colorGroup for the button drawing (if @p isHovered) + * and the LinkLook color() for the text, + * unless [the LinkLook !color.isValid() and it does not useLinkColor()] or [@p isDefaultColor is false]: in this case it will use @p colorGroup.text(). + * It will draw the button if @p isIconButtonHovered. + */ +void LinkDisplay::paint(QPainter *painter, int x, int y, int width, int height, const QColorGroup &colorGroup, + bool isDefaultColor, bool isSelected, bool isHovered, bool isIconButtonHovered) const +{ + int BUTTON_MARGIN = kapp->style().pixelMetric(QStyle::PM_ButtonMargin); + int LINK_MARGIN = BUTTON_MARGIN + 2; + + QPixmap pixmap; + // Load the preview...: + if (!isHovered && m_look->previewEnabled() && !m_preview.isNull()) + pixmap = m_preview; + // ... Or the icon (if no preview or if the "Open" icon should be shown): + else { + int iconSize = m_look->iconSize(); + QString iconName = (isHovered ? Global::openNoteIcon() : m_icon); + KIcon::States iconState = (isIconButtonHovered ? KIcon::ActiveState : KIcon::DefaultState); + pixmap = kapp->iconLoader()->loadIcon(iconName, KIcon::Desktop, iconSize, iconState, 0L, /*canReturnNull=*/false); + } + int iconPreviewWidth = QMAX(m_look->iconSize(), (m_look->previewEnabled() ? m_preview.width() : 0)); + int pixmapX = (iconPreviewWidth - pixmap.width()) / 2; + int pixmapY = (height - pixmap.height()) / 2; + // Draw the button (if any) and the icon: + if (isHovered) + kapp->style().drawPrimitive(QStyle::PE_ButtonCommand, painter, QRect(-1, -1, iconPreviewWidth + 2*BUTTON_MARGIN, height + 2), + colorGroup, QStyle::Style_Enabled | (isIconButtonHovered ? QStyle::Style_MouseOver : 0)); + painter->drawPixmap(x + BUTTON_MARGIN - 1 + pixmapX, y + pixmapY, pixmap); + + // Figure out the text color: + if (isSelected) + painter->setPen(KGlobalSettings::highlightedTextColor()); + else if (isIconButtonHovered) + painter->setPen(m_look->effectiveHoverColor()); + else if (!isDefaultColor || (!m_look->color().isValid() && !m_look->useLinkColor())) // If the color is FORCED or if the link color default to the text color: + painter->setPen(colorGroup.text()); + else + painter->setPen(m_look->effectiveColor()); + // Draw the text: + painter->setFont(labelFont(m_font, isIconButtonHovered)); + painter->drawText(x + BUTTON_MARGIN - 1 + iconPreviewWidth + LINK_MARGIN, y, width - BUTTON_MARGIN + 1 - iconPreviewWidth - LINK_MARGIN, height, + Qt::AlignAuto | Qt::AlignVCenter | Qt::WordBreak, m_title); +} + +QPixmap LinkDisplay::feedbackPixmap(int width, int height, const QColorGroup &colorGroup, bool isDefaultColor) +{ + int theWidth = QMIN(width, maxWidth()); + int theHeight = QMIN(height, heightForWidth(theWidth)); + QPixmap pixmap(theWidth, theHeight); + pixmap.fill(colorGroup.background()); + QPainter painter(&pixmap); + paint(&painter, 0, 0, theWidth, theHeight, colorGroup, isDefaultColor, + /*isSelected=*/false, /*isHovered=*/false, /*isIconButtonHovered=*/false); + painter.end(); + return pixmap; +} + +bool LinkDisplay::iconButtonAt(const QPoint &pos) const +{ + int BUTTON_MARGIN = kapp->style().pixelMetric(QStyle::PM_ButtonMargin); +// int LINK_MARGIN = BUTTON_MARGIN + 2; + int iconPreviewWidth = QMAX(m_look->iconSize(), (m_look->previewEnabled() ? m_preview.width() : 0)); + + return pos.x() <= BUTTON_MARGIN - 1 + iconPreviewWidth + BUTTON_MARGIN; +} + +QRect LinkDisplay::iconButtonRect() const +{ + int BUTTON_MARGIN = kapp->style().pixelMetric(QStyle::PM_ButtonMargin); +// int LINK_MARGIN = BUTTON_MARGIN + 2; + int iconPreviewWidth = QMAX(m_look->iconSize(), (m_look->previewEnabled() ? m_preview.width() : 0)); + + return QRect(0, 0, BUTTON_MARGIN - 1 + iconPreviewWidth + BUTTON_MARGIN, m_height); +} + +QFont LinkDisplay::labelFont(QFont font, bool isIconButtonHovered) const +{ + if (m_look->italic()) + font.setItalic(true); + if (m_look->bold()) + font.setBold(true); + if (isIconButtonHovered) { + if (m_look->underlineInside()) + font.setUnderline(true); + } else { + if (m_look->underlineOutside()) + font.setUnderline(true); + } + return font; +} + +int LinkDisplay::heightForWidth(int width) const +{ + int BUTTON_MARGIN = kapp->style().pixelMetric(QStyle::PM_ButtonMargin); + int LINK_MARGIN = BUTTON_MARGIN + 2; + int iconPreviewWidth = QMAX(m_look->iconSize(), (m_look->previewEnabled() ? m_preview.width() : 0)); + int iconPreviewHeight = QMAX(m_look->iconSize(), (m_look->previewEnabled() ? m_preview.height() : 0)); + + QRect textRect = QFontMetrics(labelFont(m_font, false)).boundingRect(0, 0, width - BUTTON_MARGIN + 1 - iconPreviewWidth - LINK_MARGIN, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, m_title); + return QMAX(textRect.height(), iconPreviewHeight + 2*BUTTON_MARGIN - 2); +} + +QString LinkDisplay::toHtml(const QString &/*imageName*/) const +{ + // TODO + return ""; +} + +QString LinkDisplay::toHtml(HTMLExporter *exporter, const KURL &url, const QString &title) +{ + QString linkIcon; + if (m_look->previewEnabled() && !m_preview.isNull()) { + QString fileName = Tools::fileNameForNewFile("preview_" + url.fileName() + ".png", exporter->iconsFolderPath); + QString fullPath = exporter->iconsFolderPath + fileName; + m_preview.save(fullPath, "PNG"); + linkIcon = QString("<img src=\"%1\" width=\"%2\" height=\"%3\" alt=\"\">") + .arg(exporter->iconsFolderName + fileName, QString::number(m_preview.width()), QString::number(m_preview.height())); + } else { + linkIcon = exporter->iconsFolderName + exporter->copyIcon(m_icon, m_look->iconSize()); + linkIcon = QString("<img src=\"%1\" width=\"%2\" height=\"%3\" alt=\"\">") + .arg(linkIcon, QString::number(m_look->iconSize()), QString::number(m_look->iconSize())); + } + + QString linkTitle = Tools::textToHTMLWithoutP(title.isEmpty() ? m_title : title); + + return QString("<a href=\"%1\">%2 %3</a>").arg(url.prettyURL(), linkIcon, linkTitle); +} + +/** LinkLookEditWidget **/ + +LinkLookEditWidget::LinkLookEditWidget(KCModule *module, const QString exTitle, const QString exIcon, + QWidget *parent, const char *name, WFlags fl) + : QWidget(parent, name, fl) +{ + QLabel *label; + QVBoxLayout *layout = new QVBoxLayout(this, KDialogBase::marginHint(), KDialogBase::spacingHint()); + + m_italic = new QCheckBox(i18n("I&talic"), this); + layout->addWidget(m_italic); + + m_bold = new QCheckBox(i18n("&Bold"), this); + layout->addWidget(m_bold); + + QGridLayout *gl = new QGridLayout(layout, /*rows=*//*(look->canPreview() ? 5 : 4)*/5, /*columns=*//*3*/4); + gl->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding), 1, /*2*/3); + + m_underlining = new QComboBox(false, this); + m_underlining->insertItem(i18n("Always")); + m_underlining->insertItem(i18n("Never")); + m_underlining->insertItem(i18n("On mouse hovering")); + m_underlining->insertItem(i18n("When mouse is outside")); + label = new QLabel(m_underlining, i18n("&Underline:"), this); + gl->addWidget(label, 0, 0); + gl->addWidget(m_underlining, 0, 1); + + m_color = new KColorCombo2(QRgb(), this); + label = new QLabel(m_color, i18n("Colo&r:"), this); + gl->addWidget(label, 1, 0); + gl->addWidget(m_color, 1, 1); + + m_hoverColor = new KColorCombo2(QRgb(), this); + label = new QLabel(m_hoverColor, i18n("&Mouse hover color:"), this); + gl->addWidget(label, 2, 0); + gl->addWidget(m_hoverColor, 2, 1); + + QHBoxLayout *icoLay = new QHBoxLayout(/*parent=*/0L, /*margin=*/0, KDialogBase::spacingHint()); + m_iconSize = new IconSizeCombo(false, this); + icoLay->addWidget(m_iconSize); + label = new QLabel(m_iconSize, i18n("&Icon size:"), this); + gl->addWidget(label, 3, 0); + gl->addItem( icoLay, 3, 1); + + m_preview = new QComboBox(false, this); + m_preview->insertItem(i18n("None")); + m_preview->insertItem(i18n("Icon size")); + m_preview->insertItem(i18n("Twice the icon size")); + m_preview->insertItem(i18n("Three times the icon size")); + m_label = new QLabel(m_preview, i18n("&Preview:"), this); + m_hLabel = new HelpLabel( + i18n("You disabled preview but still see images?"), + i18n("<p>This is normal because there are several type of notes.<br>" + "This setting only applies to file and local link notes.<br>" + "The images you see are image notes, not file notes.<br>" + "File notes are generic documents, whereas image notes are pictures you can draw in.</p>" + "<p>When dropping files to baskets, %1 detects their type and shows you the content of the files.<br>" + "For instance, when dropping image or text files, image and text notes are created for them.<br>" + "For type of files %2 does not understand, they are shown as generic file notes with just an icon or file preview and a filename.</p>" + "<p>If you do not want the application to create notes depending on the content of the files you drop, " + "go to the \"General\" page and uncheck \"Image or animation\" in the \"View Content of Added Files for the Following Types\" group.</p>") + // TODO: Note: you can resize down maximum size of images... + .arg(kapp->aboutData()->programName(), kapp->aboutData()->programName()), + this); + gl->addWidget(m_label, 4, 0); + gl->addWidget(m_preview, 4, 1); + gl->addMultiCellWidget(m_hLabel, /*fromRow=*/5, /*toRow=*/5, /*fromCol=*/1, /*toCol*/2); + + QGroupBox *gb = new QHGroupBox(i18n("Example"), this); + m_exLook = new LinkLook; + m_example = new LinkLabel(exTitle, exIcon, m_exLook, 1, 1, gb); + m_example->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_example->setCursor(QCursor(Qt::PointingHandCursor)); + layout->addWidget(gb); + m_exTitle = exTitle; + m_exIcon = exIcon; + + connect( m_italic, SIGNAL(stateChanged(int)), this, SLOT(slotChangeLook()) ); + connect( m_bold, SIGNAL(stateChanged(int)), this, SLOT(slotChangeLook()) ); + connect( m_underlining, SIGNAL(activated(int)), this, SLOT(slotChangeLook()) ); + connect( m_color, SIGNAL(changed(const QColor&)), this, SLOT(slotChangeLook()) ); + connect( m_hoverColor, SIGNAL(changed(const QColor&)), this, SLOT(slotChangeLook()) ); + connect( m_iconSize, SIGNAL(activated(int)), this, SLOT(slotChangeLook()) ); + connect( m_preview, SIGNAL(activated(int)), this, SLOT(slotChangeLook()) ); + + connect( m_italic, SIGNAL(stateChanged(int)), module, SLOT(changed()) ); + connect( m_bold, SIGNAL(stateChanged(int)), module, SLOT(changed()) ); + connect( m_underlining, SIGNAL(activated(int)), module, SLOT(changed()) ); + connect( m_color, SIGNAL(changed(const QColor&)), module, SLOT(changed()) ); + connect( m_hoverColor, SIGNAL(changed(const QColor&)), module, SLOT(changed()) ); + connect( m_iconSize, SIGNAL(activated(int)), module, SLOT(changed()) ); + connect( m_preview, SIGNAL(activated(int)), module, SLOT(changed()) ); +} + +void LinkLookEditWidget::set(LinkLook *look) +{ + m_look = look; + + m_italic->setChecked(look->italic()); + m_bold->setChecked(look->bold()); + m_underlining->setCurrentItem(look->underlining()); + m_preview->setCurrentItem(look->preview()); + m_color->setDefaultColor(m_look->defaultColor()); + m_color->setColor(m_look->color()); + m_hoverColor->setDefaultColor(m_look->defaultHoverColor()); + m_hoverColor->setColor(m_look->hoverColor()); + m_iconSize->setSize(look->iconSize()); + m_exLook = new LinkLook(*look); + m_example->setLook(m_exLook); + + if (!look->canPreview()) { + m_label->setEnabled(false); + m_hLabel->setEnabled(false); + m_preview->setEnabled(false); + } + slotChangeLook(); +} + +void LinkLookEditWidget::slotChangeLook() +{ + saveToLook(m_exLook); + m_example->setLink(m_exTitle, m_exIcon, m_exLook); // and can't reload it at another size +} + +LinkLookEditWidget::~LinkLookEditWidget() +{ +} + +void LinkLookEditWidget::saveChanges() +{ + saveToLook(m_look); +} + +void LinkLookEditWidget::saveToLook(LinkLook *look) +{ + look->setLook( m_italic->isOn(), m_bold->isOn(), m_underlining->currentItem(), + m_color->color(), m_hoverColor->color(), + m_iconSize->iconSize(), (look->canPreview() ? m_preview->currentItem() : LinkLook::None) ); +} + +#include "linklabel.moc" diff --git a/src/linklabel.h b/src/linklabel.h new file mode 100644 index 0000000..3e964c1 --- /dev/null +++ b/src/linklabel.h @@ -0,0 +1,208 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef LINKLABEL_H +#define LINKLABEL_H + +#include <qframe.h> + +class QString; +class KURL; +class QColor; +class QLabel; +class QBoxLayout; +class QSpacerItem; +class QPushButton; +class QCheckBox; +class QComboBox; + +class KColorCombo2; +class IconSizeCombo; + +class HTMLExporter; +class HelpLabel; +class KCModule; + +/** Store the style of links + * @author S�astien Laot + */ +class LinkLook +{ + public: + enum Underlining { Always = 0, Never, OnMouseHover, OnMouseOutside }; + enum Preview { None = 0, IconSize, TwiceIconSize, ThreeIconSize }; + LinkLook(bool useLinkColor = true, bool canPreview = true); + LinkLook(const LinkLook &other); + void setLook( bool italic, bool bold, int underlining, + QColor color, QColor hoverColor, + int iconSize, int preview /*= None*/ ); + inline bool italic() const { return m_italic; } + inline bool bold() const { return m_bold; } + inline int underlining() const { return m_underlining; } + inline QColor color() const { return m_color; } + inline QColor hoverColor() const { return m_hoverColor; } + inline int iconSize() const { return m_iconSize; } + inline int preview() const { return m_preview; } + inline bool useLinkColor() const { return m_useLinkColor; } + inline bool canPreview() const { return m_canPreview; } + /* Helpping Functions */ + bool underlineOutside() const { return underlining() == Always || underlining() == OnMouseOutside; } + bool underlineInside() const { return underlining() == Always || underlining() == OnMouseHover; } + bool previewEnabled() const { return canPreview() && preview() > None; } + int previewSize() const; + QColor effectiveColor() const; + QColor effectiveHoverColor() const; + QColor defaultColor() const; + QColor defaultHoverColor() const; + QString toCSS(const QString &cssClass, const QColor &defaultTextColor) const; + private: + bool m_italic; + bool m_bold; + int m_underlining; + QColor m_color; + QColor m_hoverColor; + int m_iconSize; + int m_preview; + bool m_useLinkColor; + bool m_canPreview; + public: + /* Global Looks */ + static LinkLook *soundLook; + static LinkLook *fileLook; + static LinkLook *localLinkLook; + static LinkLook *networkLinkLook; + static LinkLook *launcherLook; + /* Static method to get a LinkLook from an URL */ + static LinkLook* lookForURL(const KURL &url); +}; + +/** Used to represent links with icon and specific look + * Note : This label will appear blank while LinkLook willn't be set + * @author S�astien Laot + */ +class LinkLabel : public QFrame +{ + Q_OBJECT + public: + LinkLabel(int hAlign, int vAlign, QWidget *parent = 0, const char *name = 0, WFlags f = 0); + LinkLabel(const QString &title, const QString &icon, LinkLook *look, int hAlign, int vAlign, + QWidget *parent = 0, const char *name = 0, WFlags f = 0); + ~LinkLabel(); + public: + void setLink(const QString &title, const QString &icon, LinkLook *look = 0); + void setLook(LinkLook *look); + void setAlign(int hAlign, int vAlign); + void setSelected(bool selected); + void setPaletteBackgroundColor(const QColor &color); + int heightForWidth(int w = -1) const; + QString toHtml(const QString &imageName); + protected: + void initLabel(int hAlign, int vAlign); + void enterEvent(QEvent*); + void leaveEvent(QEvent*); + private: + QBoxLayout *m_layout; + QLabel *m_icon; + QLabel *m_title; + QSpacerItem *m_spacer1; + QSpacerItem *m_spacer2; + + bool m_isSelected; + bool m_isHovered; + + LinkLook *m_look; + int m_hAlign; + int m_vAlign; +}; + +/** THE NEW CLASS TO DISPLAY Links FOR THE NEW BASKET ENGINE. + * We should get ride of class LinkLabel soon. + * And LinkLabel will be entirely rewritten to use this LinkDisplay as the drawing primitives. + * @author S�astien Laot + */ +class LinkDisplay +{ + public: + LinkDisplay(); /// << Create a new empty unselected LinkDisplay. Please then call setLink() to make sense. + // Configure the link displayer: + void setLink(const QString &title, const QString &icon, LinkLook *look, const QFont &font); /// << Change the content and disposition. minWidth(), width() & height() can have changed. Keep the old preview (if any) + void setLink(const QString &title, const QString &icon, const QPixmap &preview, LinkLook *look, const QFont &font); /// << Idem but change the preview too (or remove it if it is invalid) + void setWidth(int width); /// << Set a new width. @see height() that will be computed. + // Get its properties: + int minWidth() const { return m_minWidth; } /// << @return the minimum width to display this link. + int maxWidth() const { return m_maxWidth; } /// << @return the maximum width to display this link. + int width() const { return m_width; } /// << @return the width of the link. It is never less than minWidth()! + int height() const { return m_height; } /// << @return the height if the link after having set it a width. + // And finaly, use it: + void paint(QPainter *painter, int x, int y, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered, bool isIconButtonHovered) const; /// << Draw the link on a painter. Set textColor to be !isValid() to use the LinkLook color. Otherwise it will use this color! + QPixmap feedbackPixmap(int width, int height, const QColorGroup &colorGroup, bool isDefaultColor); /// << @return the pixmap to put under the cursor while dragging this object. + // Eventually get some information about the link display: + bool iconButtonAt(const QPoint &pos) const; /// << @return true if the icon button is under point @p pos. + QRect iconButtonRect() const; /// << @return the rectangle of the icon button. + // Utility function: + QFont labelFont(QFont font, bool isIconButtonHovered) const; /// << @return the font for this link, according to parent font AND LinkLook! + int heightForWidth(int width) const; /// << @return the needed height to display the link in function of a width. + QString toHtml(const QString &imageName) const; /// << Convert the link to HTML code, using the LinkLook to style it. + QString toHtml(HTMLExporter *exporter, const KURL &url, const QString &title = ""); + private: + QString m_title; + QString m_icon; + QPixmap m_preview; + LinkLook *m_look; + QFont m_font; + int m_minWidth; + int m_maxWidth; + int m_width; + int m_height; +}; + +/** A widget to edit a LinkLook, showing a live example to the user. + * @author S�astien Laot + */ +class LinkLookEditWidget : public QWidget +{ + Q_OBJECT + public: + LinkLookEditWidget(KCModule* module, const QString exTitle, const QString exIcon, + QWidget *parent = 0, const char *name = 0, WFlags fl = 0); + ~LinkLookEditWidget(); + void saveChanges(); + void saveToLook(LinkLook *look); + void set(LinkLook *look); + private slots: + void slotChangeLook(); + protected: + LinkLook *m_look; + QCheckBox *m_italic; + QCheckBox *m_bold; + QComboBox *m_underlining; + KColorCombo2 *m_color; + KColorCombo2 *m_hoverColor; + IconSizeCombo *m_iconSize; + QComboBox *m_preview; + LinkLook *m_exLook; + LinkLabel *m_example; + QString m_exTitle; + QString m_exIcon; + HelpLabel *m_hLabel; + QLabel *m_label; +}; + +#endif // LINKLABEL_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..7a7d0c0 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,115 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <stdlib.h> + +#include <kcmdlineargs.h> +#include <kaboutdata.h> +#include <kiconloader.h> +#include <qpixmap.h> +#include <klocale.h> +#include <kglobalaccel.h> +#include <kmessagebox.h> +#include <kstandarddirs.h> +#include <kdebug.h> +#include <qfile.h> +#include <qfileinfo.h> + +#include <kconfig.h> // TMP IN ALPHA 1 + +#include "application.h" +#include "backgroundmanager.h" +#include "mainwindow.h" +#include "settings.h" +#include "global.h" +#include "debugwindow.h" +#include "notedrag.h" +#include "basket.h" +#include "aboutdata.h" +#include "basket_options.h" +#include "backup.h" + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +/* Thanks to JuK for this Application class */ +/*#if KDE_IS_VERSION( 3, 1, 90 ) +typedef KUniqueApplication Application; // KDE 3.2 and later already re-show the main window +#else +class Application : public KUniqueApplication +{ + public: + Application() : KUniqueApplication(true, true, false) {} + virtual ~Application() {} + virtual int newInstance() { + if (win) + win->setActive(true); + return KUniqueApplication::newInstance(); + } +}; +#endif +*/ + +int main(int argc, char *argv[]) +{ + // KCmdLineArgs::init will modify argv[0] so we remember it: + const char *argv0 = (argc >= 1 ? argv[0] : ""); + + KCmdLineArgs::init(argc, argv, Global::about()); + KCmdLineArgs::addCmdLineOptions(basket_options); + + KUniqueApplication::addCmdLineOptions(); + //KUniqueApplication app; + Application app; + + Backup::figureOutBinaryPath(argv0, app); + + /* Main Window */ + MainWindow* win = new MainWindow(); + Global::bnpView->handleCommandLine(); + app.setMainWidget(win); +// if (!(Settings::useSystray() && KCmdLineArgs::parsedArgs() && KCmdLineArgs::parsedArgs()->isSet("start-hidden"))) +// win->show(); + + if (Settings::useSystray()) { + // The user wanted to not show the window (but it is already hidden by default, so we do nothing): + if (KCmdLineArgs::parsedArgs() && KCmdLineArgs::parsedArgs()->isSet("start-hidden")) + ; + // When the application is restored by KDE session, restore its state: + else if (app.isRestored()) + win->setShown(!Settings::startDocked()); + // Else, the application has been launched explicitely by the user (KMenu, keyboard shortcut...), so he need it, we show it: + else + win->show(); + } else + // No system tray icon: always show: + win->show(); + + // Self-test of the presence of basketui.rc (the only requiered file after basket executable) + if (Global::bnpView->popupMenu("basket") == 0L) + // An error message will be show by BNPView::popupMenu() + return 1; + + /* Go */ + int result = app.exec(); + //return result; + exit(result); // Do not clean up memory to not crash while deleting the KApplication, or do not hang up on KDE exit +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..ce8bf0f --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,345 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qtabwidget.h> +#include <qlayout.h> +#include <qtooltip.h> +#include <qcursor.h> +#include <qwhatsthis.h> +#include <qvaluelist.h> +#include <qregexp.h> +#include <qbuttongroup.h> +#include <kstringhandler.h> + +#include <ksqueezedtextlabel.h> +#include <qpoint.h> +#include <qpixmap.h> +#include <qinputdialog.h> +#include <kpopupmenu.h> +#include <kiconloader.h> +#include <kiconeffect.h> +#include <qiconset.h> +#include <kaction.h> +#include <kapp.h> +#include <klocale.h> +#include <kmenubar.h> +#include <kedittoolbar.h> +#include <kdebug.h> +#include <qsignalmapper.h> +#include <qstringlist.h> + +#include <qpainter.h> +#include <qstyle.h> +#include <kglobalsettings.h> +#include <kstandarddirs.h> +#include <qdir.h> +#include <qstringlist.h> +#include <kmessagebox.h> +#include <kstatusbar.h> +#include <qtimer.h> +#include <qaction.h> +#include <kstdaccel.h> +#include <kglobalaccel.h> +#include <kkeydialog.h> +#include <kpassivepopup.h> +#include <kconfig.h> +#include <kcolordialog.h> +#include <kaboutdata.h> + +#include <kdeversion.h> +#include <qdesktopwidget.h> +#include <kwin.h> + +#include <kprogress.h> + +#include "mainwindow.h" +#include "basket.h" +#include "basketproperties.h" +#include "note.h" +#include "noteedit.h" +#include "settings.h" +#include "global.h" +//#include "addbasketwizard.h" +#include "newbasketdialog.h" +#include "basketfactory.h" +#include "popupmenu.h" +#include "xmlwork.h" +#include "debugwindow.h" +#include "notefactory.h" +#include "notedrag.h" +#include "tools.h" +#include "tag.h" +#include "formatimporter.h" +#include "softwareimporters.h" +#include "regiongrabber.h" +#include "password.h" +#include "bnpview.h" +#include "systemtray.h" +#include "clickablelabel.h" +#include "basketstatusbar.h" +#include <iostream> +#include <ksettings/dialog.h> +#include <kcmultidialog.h> + +/** Container */ + +MainWindow::MainWindow(QWidget *parent, const char *name) + : KMainWindow(parent, name != 0 ? name : "MainWindow"), m_settings(0), m_quit(false) +{ + BasketStatusBar* bar = new BasketStatusBar(statusBar()); + m_baskets = new BNPView(this, "BNPViewApp", this, actionCollection(), bar); + setCentralWidget(m_baskets); + + setupActions(); + statusBar()->show(); + statusBar()->setSizeGripEnabled(true); + + setAutoSaveSettings(/*groupName=*/QString::fromLatin1("MainWindow"), /*saveWindowSize=*//*FIXME:false:Why was it false??*/true); + +// m_actShowToolbar->setChecked( toolBar()->isShown() ); + m_actShowStatusbar->setChecked( statusBar()->isShown() ); + connect( m_baskets, SIGNAL(setWindowCaption(const QString &)), this, SLOT(setCaption(const QString &))); + +// InlineEditors::instance()->richTextToolBar(); + setStandardToolBarMenuEnabled(true); + + createGUI("basketui.rc"); + applyMainWindowSettings(KGlobal::config(), autoSaveGroup()); +} + +MainWindow::~MainWindow() +{ + saveMainWindowSettings(KGlobal::config(), autoSaveGroup()); + delete m_settings; +} + +void MainWindow::setupActions() +{ + actQuit = KStdAction::quit( this, SLOT(quit()), actionCollection() ); + new KAction(i18n("Minimize"), "", 0, + this, SLOT(minimizeRestore()), actionCollection(), "minimizeRestore" ); + /** Settings : ************************************************************/ +// m_actShowToolbar = KStdAction::showToolbar( this, SLOT(toggleToolBar()), actionCollection()); + m_actShowStatusbar = KStdAction::showStatusbar( this, SLOT(toggleStatusBar()), actionCollection()); + +// m_actShowToolbar->setCheckedState( KGuiItem(i18n("Hide &Toolbar")) ); + + (void) KStdAction::keyBindings( this, SLOT(showShortcutsSettingsDialog()), actionCollection() ); + + (void) KStdAction::configureToolbars(this, SLOT(configureToolbars()), actionCollection() ); + + //KAction *actCfgNotifs = KStdAction::configureNotifications(this, SLOT(configureNotifications()), actionCollection() ); + //actCfgNotifs->setEnabled(false); // Not yet implemented ! + + actAppConfig = KStdAction::preferences( this, SLOT(showSettingsDialog()), actionCollection() ); +} + +/*void MainWindow::toggleToolBar() +{ + if (toolBar()->isVisible()) + toolBar()->hide(); + else + toolBar()->show(); + + saveMainWindowSettings( KGlobal::config(), autoSaveGroup() ); +}*/ + +void MainWindow::toggleStatusBar() +{ + if (statusBar()->isVisible()) + statusBar()->hide(); + else + statusBar()->show(); + + saveMainWindowSettings( KGlobal::config(), autoSaveGroup() ); +} + +void MainWindow::configureToolbars() +{ + saveMainWindowSettings( KGlobal::config(), autoSaveGroup() ); + + KEditToolbar dlg(actionCollection()); + connect( &dlg, SIGNAL(newToolbarConfig()), this, SLOT(slotNewToolbarConfig()) ); + dlg.exec(); +} + +void MainWindow::configureNotifications() +{ + // TODO + // KNotifyDialog *dialog = new KNotifyDialog(this, "KNotifyDialog", false); + // dialog->show(); +} + +void MainWindow::slotNewToolbarConfig() // This is called when OK or Apply is clicked +{ + // ...if you use any action list, use plugActionList on each here... + createGUI("basketui.rc"); // TODO: Reconnect tags menu aboutToShow() ?? + if (!Global::bnpView->isPart()) + Global::bnpView->connectTagsMenu(); // The Tags menu was created again! + plugActionList( QString::fromLatin1("go_baskets_list"), actBasketsList); + applyMainWindowSettings( KGlobal::config(), autoSaveGroup() ); +} + +void MainWindow::showSettingsDialog() +{ + if(m_settings == 0) + m_settings = new KSettings::Dialog(kapp->activeWindow()); + if (Global::mainWindow()) { + m_settings->dialog()->showButton(KDialogBase::Help, false); // Not implemented! + m_settings->dialog()->showButton(KDialogBase::Default, false); // Not implemented! + m_settings->dialog()->exec(); + } else + m_settings->show(); +} + +void MainWindow::showShortcutsSettingsDialog() +{ + KKeyDialog::configure(actionCollection(), "basketui.rc"); + //.setCaption(..) + //actionCollection()->writeSettings(); +} + +void MainWindow::polish() +{ + bool shouldSave = false; + + // If position and size has never been set, set nice ones: + // - Set size to sizeHint() + // - Keep the window manager placing the window where it want and save this + if (Settings::mainWindowSize().isEmpty()) { +// std::cout << "Main Window Position: Initial Set in show()" << std::endl; + int defaultWidth = kapp->desktop()->width() * 5 / 6; + int defaultHeight = kapp->desktop()->height() * 5 / 6; + resize(defaultWidth, defaultHeight); // sizeHint() is bad (too small) and we want the user to have a good default area size + shouldSave = true; + } else { +// std::cout << "Main Window Position: Recall in show(x=" +// << Settings::mainWindowPosition().x() << ", y=" << Settings::mainWindowPosition().y() +// << ", width=" << Settings::mainWindowSize().width() << ", height=" << Settings::mainWindowSize().height() +// << ")" << std::endl; + //move(Settings::mainWindowPosition()); + //resize(Settings::mainWindowSize()); + } + + KMainWindow::polish(); + + if (shouldSave) { +// std::cout << "Main Window Position: Save size and position in show(x=" +// << pos().x() << ", y=" << pos().y() +// << ", width=" << size().width() << ", height=" << size().height() +// << ")" << std::endl; + Settings::setMainWindowPosition(pos()); + Settings::setMainWindowSize(size()); + Settings::saveConfig(); + } +} + +void MainWindow::resizeEvent(QResizeEvent *event) +{ +// std::cout << "Main Window Position: Save size in resizeEvent(width=" << size().width() << ", height=" << size().height() << ") ; isMaximized=" +// << (isMaximized() ? "true" : "false") << std::endl; + Settings::setMainWindowSize(size()); + Settings::saveConfig(); + + // Added to make it work (previous lines do not work): + //saveMainWindowSettings( KGlobal::config(), autoSaveGroup() ); + KMainWindow::resizeEvent(event); +} + +void MainWindow::moveEvent(QMoveEvent *event) +{ +// std::cout << "Main Window Position: Save position in moveEvent(x=" << pos().x() << ", y=" << pos().y() << ")" << std::endl; + Settings::setMainWindowPosition(pos()); + Settings::saveConfig(); + + // Added to make it work (previous lines do not work): + //saveMainWindowSettings( KGlobal::config(), autoSaveGroup() ); + KMainWindow::moveEvent(event); +} + +bool MainWindow::queryExit() +{ + hide(); + return true; +} + +void MainWindow::quit() +{ + m_quit = true; + close(); +} + +bool MainWindow::queryClose() +{ +/* if (m_shuttingDown) // Set in askForQuit(): we don't have to ask again + return true;*/ + + if (kapp->sessionSaving()) { + Settings::setStartDocked(false); // If queryClose() is called it's because the window is shown + Settings::saveConfig(); + return true; + } + + if (Settings::useSystray() && !m_quit) { + Global::systemTray->displayCloseMessage(i18n("Basket")); + hide(); + return false; + } else + return askForQuit(); +} + +bool MainWindow::askForQuit() +{ + QString message = i18n("<p>Do you really want to quit %1?</p>").arg(kapp->aboutData()->programName()); + if (Settings::useSystray()) + message += i18n("<p>Notice that you do not have to quit the application before ending your KDE session. " + "If you end your session while the application is still running, the application will be reloaded the next time you log in.</p>"); + + int really = KMessageBox::warningContinueCancel( this, message, i18n("Quit Confirm"), + KStdGuiItem::quit(), "confirmQuitAsking" ); + + if (really == KMessageBox::Cancel) + { + m_quit = false; + return false; + } + + return true; +} + +void MainWindow::minimizeRestore() +{ + if(isVisible()) + hide(); + else + show(); +} + +void MainWindow::changeActive() +{ +#if KDE_IS_VERSION( 3, 2, 90 ) // KDE 3.3.x + kapp->updateUserTimestamp(); // If "activate on mouse hovering systray", or "on drag throught systray" + Global::systemTray->toggleActive(); +#else + setActive( ! isActiveWindow() ); +#endif +} + +#include "mainwindow.moc" diff --git a/src/mainwindow.h b/src/mainwindow.h new file mode 100644 index 0000000..4ea57a2 --- /dev/null +++ b/src/mainwindow.h @@ -0,0 +1,107 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef CONTAINER_H +#define CONTAINER_H + +#include <kmainwindow.h> +#include <qtabbar.h> +#include <qtabwidget.h> +#include <qlabel.h> +#include <ksystemtray.h> +#include <qptrlist.h> +#include <qpoint.h> +#include <qclipboard.h> +#include <kaction.h> +#include <qpixmap.h> +#include <qdesktopwidget.h> +#include <qtimer.h> +#include <qsplitter.h> + +class QWidget; +class QPoint; +class KAction; +class KToggleAction; +class QPopupMenu; +class QSignalMapper; +class QStringList; +class QToolTipGroup; +class KPassivePopup; +class Basket; +class DecoratedBasket; +class Container; +class RegionGrabber; +class NoteSelection; +class BNPView; +class ClickableLabel; +namespace KSettings { class Dialog; }; + + +/** The window that contain baskets, organized by tabs. + * @author S�astien Laot + */ +class MainWindow : public KMainWindow +{ + Q_OBJECT + public: + /** Construtor, initializer and destructor */ + MainWindow(QWidget *parent = 0, const char *name = 0); + ~MainWindow(); + private: + void setupActions(); + public slots: + bool askForQuit(); + /** Settings **/ +// void toggleToolBar(); + void toggleStatusBar(); + void showShortcutsSettingsDialog(); + void configureToolbars(); + void configureNotifications(); + void showSettingsDialog(); + void minimizeRestore(); + void quit(); + void changeActive(); + void slotNewToolbarConfig(); + + protected: + bool queryExit(); + bool queryClose(); + virtual void resizeEvent(QResizeEvent*); + virtual void moveEvent(QMoveEvent*); + public: + void polish(); + + private: + // Settings actions : +// KToggleAction *m_actShowToolbar; + KToggleAction *m_actShowStatusbar; + KAction *actQuit; + KAction *actAppConfig; + QPtrList<KAction> actBasketsList; + + private: + QVBoxLayout *m_layout; + BNPView *m_baskets; + bool m_startDocked; + KSettings::Dialog *m_settings; + bool m_quit; +}; + +#endif // CONTAINER_H diff --git a/src/newbasketdialog.cpp b/src/newbasketdialog.cpp new file mode 100644 index 0000000..0d5bc44 --- /dev/null +++ b/src/newbasketdialog.cpp @@ -0,0 +1,333 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kicondialog.h> +#include <qlineedit.h> +#include <kiconview.h> +#include <qlayout.h> +#include <qlabel.h> +#include <klocale.h> +#include <kglobalsettings.h> +#include <kpushbutton.h> +#include <kguiitem.h> +#include <kmessagebox.h> +#include <qsize.h> +#include <qpainter.h> +#include <kapplication.h> +#include <kiconloader.h> +#include <kmainwindow.h> + +#include "newbasketdialog.h" +#include "basketfactory.h" +#include "basket.h" +#include "basketlistview.h" +#include "variouswidgets.h" +#include "kcolorcombo2.h" +#include "tools.h" +#include "global.h" +#include "bnpview.h" + +/** class SingleSelectionKIconView: */ + +SingleSelectionKIconView::SingleSelectionKIconView(QWidget *parent, const char *name, WFlags f) + : KIconView(parent, name, f), m_lastSelected(0) +{ + connect( this, SIGNAL(selectionChanged(QIconViewItem*)), this, SLOT(slotSelectionChanged(QIconViewItem*)) ); + connect( this, SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged()) ); +} + +QDragObject* SingleSelectionKIconView::dragObject() +{ + return 0; +} + +void SingleSelectionKIconView::slotSelectionChanged(QIconViewItem *item) +{ + if (item) + m_lastSelected = item; +} + +void SingleSelectionKIconView::slotSelectionChanged() +{ + if (m_lastSelected && !m_lastSelected->isSelected()) + m_lastSelected->setSelected(true); +} + +/** class NewBasketDefaultProperties: */ + +NewBasketDefaultProperties::NewBasketDefaultProperties() + : icon("") + , backgroundImage("") + , backgroundColor() + , textColor() + , freeLayout(false) + , columnCount(1) +{ +} + +/** class NewBasketDialog: */ + +NewBasketDialog::NewBasketDialog(Basket *parentBasket, const NewBasketDefaultProperties &defaultProperties, QWidget *parent) + : KDialogBase(KDialogBase::Swallow, i18n("New Basket"), KDialogBase::Ok | KDialogBase::Cancel, + KDialogBase::Ok, parent, /*name=*/"NewBasket", /*modal=*/true, /*separator=*/true) + , m_defaultProperties(defaultProperties) +{ + QWidget *page = new QWidget(this); + QVBoxLayout *topLayout = new QVBoxLayout(page, /*margin=*/0, spacingHint()); + + // Icon, Name and Background Color: + QHBoxLayout *nameLayout = new QHBoxLayout(0, marginHint()*2/3, spacingHint()); + m_icon = new KIconButton(page); + m_icon->setIconType(KIcon::NoGroup, KIcon::Action); + m_icon->setIconSize(16); + m_icon->setIcon(m_defaultProperties.icon.isEmpty() ? "basket" : m_defaultProperties.icon); + int size = QMAX(m_icon->sizeHint().width(), m_icon->sizeHint().height()); + m_icon->setFixedSize(size, size); // Make it square! + QToolTip::add(m_icon, i18n("Icon")); + m_name = new QLineEdit(/*i18n("Basket"), */page); + m_name->setMinimumWidth(m_name->fontMetrics().maxWidth()*20); + connect( m_name, SIGNAL(textChanged(const QString&)), this, SLOT(nameChanged(const QString&)) ); + enableButtonOK(false); + QToolTip::add(m_name, i18n("Name")); + m_backgroundColor = new KColorCombo2(QColor(), KGlobalSettings::baseColor(), page); + m_backgroundColor->setColor(QColor()); + m_backgroundColor->setFixedSize(m_backgroundColor->sizeHint()); + m_backgroundColor->setColor(m_defaultProperties.backgroundColor); + QToolTip::add(m_backgroundColor, i18n("Background color")); + nameLayout->addWidget(m_icon); + nameLayout->addWidget(m_name); + nameLayout->addWidget(m_backgroundColor); + topLayout->addLayout(nameLayout); + + QHBoxLayout *layout = new QHBoxLayout(/*parent=*/0, /*margin=*/0, spacingHint()); + KPushButton *button = new KPushButton( KGuiItem(i18n("&Manage Templates..."), "configure"), page ); + connect( button, SIGNAL(clicked()), this, SLOT(manageTemplates()) ); + button->hide(); + + // Compute the right template to use as the default: + QString defaultTemplate = "free"; + if (!m_defaultProperties.freeLayout) { + if (m_defaultProperties.columnCount == 1) + defaultTemplate = "1column"; + else if (m_defaultProperties.columnCount == 2) + defaultTemplate = "2columns"; + else + defaultTemplate = "3columns"; + } + + // Empty: + // * * * * * + // Personnal: + // *To Do + // Professionnal: + // *Meeting Summary + // Hobbies: + // * + m_templates = new SingleSelectionKIconView(page); + m_templates->setItemsMovable(false); + m_templates->setMode(KIconView::Select); + m_templates->setGridX(m_templates->maxItemWidth()); + KIconViewItem *lastTemplate = 0; + QPixmap icon(40, 53); + + QPainter painter(&icon); + painter.fillRect(0, 0, icon.width(), icon.height(), KGlobalSettings::baseColor()); + painter.setPen(KGlobalSettings::textColor()); + painter.drawRect(0, 0, icon.width(), icon.height()); + painter.end(); + lastTemplate = new KIconViewItem(m_templates, lastTemplate, i18n("One column"), icon); + + if (defaultTemplate == "1column") + m_templates->setSelected(lastTemplate, true); + + painter.begin(&icon); + painter.fillRect(0, 0, icon.width(), icon.height(), KGlobalSettings::baseColor()); + painter.setPen(KGlobalSettings::textColor()); + painter.drawRect(0, 0, icon.width(), icon.height()); + painter.drawLine(icon.width() / 2, 0, icon.width() / 2, icon.height()); + painter.end(); + lastTemplate = new KIconViewItem(m_templates, lastTemplate, i18n("Two columns"), icon); + + if (defaultTemplate == "2columns") + m_templates->setSelected(lastTemplate, true); + + painter.begin(&icon); + painter.fillRect(0, 0, icon.width(), icon.height(), KGlobalSettings::baseColor()); + painter.setPen(KGlobalSettings::textColor()); + painter.drawRect(0, 0, icon.width(), icon.height()); + painter.drawLine(icon.width() / 3, 0, icon.width() / 3, icon.height()); + painter.drawLine(icon.width() * 2 / 3, 0, icon.width() * 2 / 3, icon.height()); + painter.end(); + lastTemplate = new KIconViewItem(m_templates, lastTemplate, i18n("Three columns"), icon); + + if (defaultTemplate == "3columns") + m_templates->setSelected(lastTemplate, true); + + painter.begin(&icon); + painter.fillRect(0, 0, icon.width(), icon.height(), KGlobalSettings::baseColor()); + painter.setPen(KGlobalSettings::textColor()); + painter.drawRect(0, 0, icon.width(), icon.height()); + painter.drawRect(icon.width() / 5, icon.width() / 5, icon.width() / 4, icon.height() / 8); + painter.drawRect(icon.width() * 2 / 5, icon.width() * 2 / 5, icon.width() / 4, icon.height() / 8); + painter.end(); + lastTemplate = new KIconViewItem(m_templates, lastTemplate, i18n("Free"), icon); + + if (defaultTemplate == "free") + m_templates->setSelected(lastTemplate, true); + +/* painter.begin(&icon); + painter.fillRect(0, 0, icon.width(), icon.height(), KGlobalSettings::baseColor()); + painter.setPen(KGlobalSettings::textColor()); + painter.drawRect(0, 0, icon.width(), icon.height()); + painter.drawRect(icon.width() * 2 / 5, icon.height() * 3 / 7, icon.width() / 5, icon.height() / 7); + painter.end(); + lastTemplate = new KIconViewItem(m_templates, lastTemplate, i18n("Mind map"), icon);*/ + + m_templates->setMinimumHeight(topLayout->minimumSize().width() * 9 / 16); + + QLabel *label = new QLabel(m_templates, i18n("&Template:"), page); + layout->addWidget(label, /*stretch=*/0, Qt::AlignBottom); + layout->addStretch(); + layout->addWidget(button, /*stretch=*/0, Qt::AlignBottom); + topLayout->addLayout(layout); + topLayout->addWidget(m_templates); + + layout = new QHBoxLayout(/*parent=*/0, /*margin=*/0, spacingHint()); + m_createIn = new QComboBox(page); + m_createIn->insertItem(i18n("(Baskets)")); + label = new QLabel(m_createIn, i18n("C&reate in:"), page); + HelpLabel *helpLabel = new HelpLabel(i18n("How is it useful?"), i18n( + "<p>Creating baskets inside of other baskets to form a hierarchy allows you to be more organized by eg.:</p><ul>" + "<li>Grouping baskets by themes or topics;</li>" + "<li>Grouping baskets in folders for different projects;</li>" + "<li>Making sections with sub-baskets representing chapters or pages;</li>" + "<li>Making a group of baskets to export together (to eg. email them to people).</li></ul>"), page); + layout->addWidget(label); + layout->addWidget(m_createIn); + layout->addWidget(helpLabel); + layout->addStretch(); + topLayout->addLayout(layout); + + m_basketsMap.clear(); + m_basketsMap.insert(/*index=*/0, /*basket=*/0L); + populateBasketsList(Global::bnpView->firstListViewItem(), /*indent=*/1, /*index=*/1); + + connect( m_templates, SIGNAL(doubleClicked(QIconViewItem*)), this, SLOT(slotOk()) ); + connect( m_templates, SIGNAL(returnPressed(QIconViewItem*)), this, SLOT(returnPressed()) ); + + if (parentBasket) { + int index = 0; + + for (QMap<int, Basket*>::Iterator it = m_basketsMap.begin(); it != m_basketsMap.end(); ++it) + if (it.data() == parentBasket) { + index = it.key(); + break; + } + if (index <= 0) + return; + + if (m_createIn->currentItem() != index) + m_createIn->setCurrentItem(index); + } + + setMainWidget(page); +} + +void NewBasketDialog::returnPressed() +{ + actionButton(KDialogBase::Ok)->animateClick(); +} + +int NewBasketDialog::populateBasketsList(QListViewItem *item, int indent, int index) +{ + static const int ICON_SIZE = 16; + + while (item) { + // Get the basket data: + Basket *basket = ((BasketListViewItem*)item)->basket(); + QPixmap icon = kapp->iconLoader()->loadIcon(basket->icon(), KIcon::NoGroup, ICON_SIZE, KIcon::DefaultState, 0L, /*canReturnNull=*/false); + icon = Tools::indentPixmap(icon, indent, 2 * ICON_SIZE / 3); + + // Append item to the list: + m_createIn->insertItem(icon, basket->basketName()); + m_basketsMap.insert(index, basket); + ++index; + + // Append childs of item to the list: + index = populateBasketsList(item->firstChild(), indent + 1, index); + + // Add next sibling basket: + item = item->nextSibling(); + } + + return index; +} + +NewBasketDialog::~NewBasketDialog() +{ +} + +void NewBasketDialog::polish() +{ + KDialogBase::polish(); + m_name->setFocus(); +} + +void NewBasketDialog::nameChanged(const QString &newName) +{ + enableButtonOK(!newName.isEmpty()); +} + +void NewBasketDialog::slotOk() +{ + QIconViewItem *item = ((SingleSelectionKIconView*)m_templates)->selectedItem(); + QString templateName; + if (item->text() == i18n("One column")) + templateName = "1column"; + if (item->text() == i18n("Two columns")) + templateName = "2columns"; + if (item->text() == i18n("Three columns")) + templateName = "3columns"; + if (item->text() == i18n("Free-form")) + templateName = "free"; + if (item->text() == i18n("Mind map")) + templateName = "mindmap"; + + Global::bnpView->closeAllEditors(); + + QString backgroundImage; + QColor textColor; + if (m_backgroundColor->color() == m_defaultProperties.backgroundColor) { + backgroundImage = m_defaultProperties.backgroundImage; + textColor = m_defaultProperties.textColor; + } + + BasketFactory::newBasket(m_icon->icon(), m_name->text(), backgroundImage, m_backgroundColor->color(), textColor, templateName, m_basketsMap[m_createIn->currentItem()]); + if(Global::mainWindow()) Global::mainWindow()->show(); + + KDialogBase::slotOk(); +} + +void NewBasketDialog::manageTemplates() +{ + KMessageBox::information(this, "Wait a minute! There is no template for now: they will come with time... :-D"); +} + +#include "newbasketdialog.moc" diff --git a/src/newbasketdialog.h b/src/newbasketdialog.h new file mode 100644 index 0000000..9e9477f --- /dev/null +++ b/src/newbasketdialog.h @@ -0,0 +1,100 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef NEWBASKETDIALOG_H +#define NEWBASKETDIALOG_H + +#include <kdialogbase.h> +#include <kiconview.h> +#include <qmap.h> + +class KIconButton; +class QLineEdit; +class QDragObject; +class QListViewItem; + +class Basket; + +class KColorCombo2; + +/** The class KIconView allow to drag items. We don't want to, so we disable it. + * This class also unselect the selected item when the user right click an empty space. We don't want to, so we reselect it if that happens. + * @author S�bastien Lao�t + */ +class SingleSelectionKIconView : public KIconView +{ + Q_OBJECT + public: + SingleSelectionKIconView(QWidget *parent = 0, const char *name = 0, WFlags f = 0); + QDragObject* dragObject(); + QIconViewItem* selectedItem() { return m_lastSelected; } + private slots: + void slotSelectionChanged(QIconViewItem *item); + void slotSelectionChanged(); + private: + QIconViewItem *m_lastSelected; +}; + +/** Struct to store default properties of a new basket. + * When the dialog shows up, the @p icon is used, as well as the @p backgroundColor. + * A template is choosen depending on @p freeLayout and @p columnLayout. + * If @p columnLayout is too high, the template with the more columns will be chosen instead. + * If the user change the background color in the dialog, then @p backgroundImage and @p textColor will not be used! + * @author S�bastien Lao�t + */ +struct NewBasketDefaultProperties +{ + QString icon; + QString backgroundImage; + QColor backgroundColor; + QColor textColor; + bool freeLayout; + int columnCount; + + NewBasketDefaultProperties(); +}; + +/** The dialog to create a new basket from a template. + * @author S�bastien Lao�t + */ +class NewBasketDialog : public KDialogBase +{ + Q_OBJECT + public: + NewBasketDialog(Basket *parentBasket, const NewBasketDefaultProperties &defaultProperties, QWidget *parent = 0); + ~NewBasketDialog(); + void polish(); + protected slots: + void slotOk(); + void returnPressed(); + void manageTemplates(); + void nameChanged(const QString &newName); + private: + int populateBasketsList(QListViewItem *item, int indent, int index); + NewBasketDefaultProperties m_defaultProperties; + KIconButton *m_icon; + QLineEdit *m_name; + KColorCombo2 *m_backgroundColor; + KIconView *m_templates; + QComboBox *m_createIn; + QMap<int, Basket*> m_basketsMap; +}; + +#endif // NEWBASKETDIALOG_H diff --git a/src/note.cpp b/src/note.cpp new file mode 100644 index 0000000..fd1fb34 --- /dev/null +++ b/src/note.cpp @@ -0,0 +1,2890 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qpainter.h> +#include <kglobalsettings.h> +#include <qstyle.h> +#include <kapplication.h> +#include <kstyle.h> +#include <qcursor.h> +#include <kiconloader.h> +#include <kpixmapeffect.h> +#include <kpixmap.h> +#include <kglobal.h> +#include <klocale.h> +#include <kurifilter.h> +#include <qfile.h> + +#include <stdlib.h> // rand() function +#include <math.h> // sqrt() and pow() functions + +#include <iostream> + +#ifdef None +#undef None +#endif + +#include "basket.h" +#include "tag.h" +#include "note.h" +#include "tools.h" +#include "settings.h" +#include "notefactory.h" // For NoteFactory::filteredURL() + +/** class Note: */ + +#define FOR_EACH_CHILD(childVar) \ + for (Note *childVar = firstChild(); childVar; childVar = childVar->next()) + +// TODO: +#define FOR_EACH_VISIBLE_CHILD(childVar) \ + for (...) + +int Note::NOTE_MARGIN = 2; +int Note::INSERTION_HEIGHT = 5; +int Note::EXPANDER_WIDTH = 9; +int Note::EXPANDER_HEIGHT = 9; +int Note::GROUP_WIDTH = 2*NOTE_MARGIN + EXPANDER_WIDTH; +int Note::HANDLE_WIDTH = GROUP_WIDTH; +int Note::RESIZER_WIDTH = GROUP_WIDTH; +int Note::TAG_ARROW_WIDTH = 5; +int Note::EMBLEM_SIZE = 16; +int Note::MIN_HEIGHT = 2*NOTE_MARGIN + EMBLEM_SIZE; + +Note::Note(Basket *parent) + : m_prev(0), m_next(0), + m_x(0), m_y(-1), m_width(-1), m_height(-1), + m_groupWidth(250), + m_isFolded(false), m_firstChild(0L), m_parentNote(0), + m_basket(parent), m_content(0), m_addedDate(QDateTime::currentDateTime()), m_lastModificationDate(QDateTime::currentDateTime()), + m_computedAreas(false), m_onTop(false), + m_deltaX(0), m_deltaY(0), m_deltaHeight(0), m_collapseFinished(true), m_expandingFinished(true), + m_hovered(false), m_hoveredZone(Note::None), m_focused(false), m_selected(false), m_wasInLastSelectionRect(false), + m_computedState(), m_emblemsCount(0), m_haveInvisibleTags(false), + m_matching(true) +{ +} + +Note::~Note() +{ + delete m_content; + deleteChilds(); +} + +QString Note::addedStringDate() +{ + return KGlobal::locale()->formatDateTime(m_addedDate); +} + +QString Note::lastModificationStringDate() +{ + return KGlobal::locale()->formatDateTime(m_lastModificationDate); +} + +QString Note::toText(const QString &cuttedFullPath) +{ + if (content()) { + // Convert note to text: + QString text = content()->toText(cuttedFullPath); + // If we should not export tags with the text, return immediatly: + if (!Settings::exportTextTags()) + return text; + // Compute the text equivalent of the tag states: + QString firstLine; + QString otherLines; + for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { + if (!(*it)->textEquivalent().isEmpty()) { + firstLine += (*it)->textEquivalent() + " "; + if ((*it)->onAllTextLines()) + otherLines += (*it)->textEquivalent() + " "; + } + } + // Merge the texts: + if (firstLine.isEmpty()) + return text; + if (otherLines.isEmpty()) + return firstLine + text; + QStringList lines = QStringList::split('\n', text, /*allowEmptyEntries=*/true); + QString result = firstLine + lines[0] + (lines.count() > 1 ? "\n" : ""); + for (uint i = 1/*Skip the first line*/; i < lines.count(); ++i) + result += otherLines + lines[i] + (i < lines.count() - 1 ? "\n" : ""); + return result; + } else + return ""; +} + +bool Note::computeMatching(const FilterData &data) +{ + // Groups are always matching: + if (!content()) + return true; + + // If we were editing this note and there is a save operation in the middle, then do not hide it suddently: + if (basket()->editedNote() == this) + return true; + + bool matching; + // First match tags (they are fast to compute): + switch (data.tagFilterType) { + default: + case FilterData::DontCareTagsFilter: matching = true; break; + case FilterData::NotTaggedFilter: matching = m_states.count() <= 0; break; + case FilterData::TaggedFilter: matching = m_states.count() > 0; break; + case FilterData::TagFilter: matching = hasTag(data.tag); break; + case FilterData::StateFilter: matching = hasState(data.state); break; + } + + // Don't try to match the content text if we are not matching now (the filter is of 'AND' type) or if we shouldn't try to match the string: + if (matching && !data.string.isEmpty()) + matching = content()->match(data); + + return matching; +} + +int Note::newFilter(const FilterData &data) +{ + bool wasMatching = matching(); + m_matching = computeMatching(data); + setOnTop(wasMatching && matching()); + if (!matching()) + setSelected(false); + + int countMatches = (content() && matching() ? 1 : 0); + + FOR_EACH_CHILD (child) + countMatches += child->newFilter(data); + + return countMatches; +} + +void Note::deleteSelectedNotes(bool deleteFilesToo) +{ + if (content() && isSelected()) { + basket()->unplugNote(this); + if (deleteFilesToo && content() && content()->useFile()) + Tools::deleteRecursively(fullPath());//basket()->deleteFiles(fullPath()); // Also delete the folder if it's a folder + //delete this; + return; + } + + Note *child = firstChild(); + Note *next; + while (child) { + next = child->next(); // If we delete 'child' on the next line, child->next() will be 0! + child->deleteSelectedNotes(deleteFilesToo); + child = next; + } +} + +int Note::count() +{ + if (content()) + return 1; + + int count = 0; + FOR_EACH_CHILD (child) + count += child->count(); + return count; +} + +int Note::countDirectChilds() +{ + int count = 0; + FOR_EACH_CHILD (child) + ++count; + return count; +} + +QString Note::fullPath() +{ + if (content()) + return basket()->fullPath() + content()->fileName(); + else + return ""; +} + +void Note::update() +{ + basket()->updateNote(this); +} + +void Note::setFocused(bool focused) +{ + if (m_focused == focused) + return; + + m_focused = focused; + unbufferize(); + update(); // FIXME: ??? +} + +void Note::setSelected(bool selected) +{ + if (isGroup()) + selected = false; // A group cannot be selected! + + if (m_selected == selected) + return; + + if (!selected && basket()->editedNote() == this) { + basket()->closeEditor(); + return; // To avoid a bug that would count 2 less selected notes instead of 1 less! Because m_selected is modified only below. + } + + if (selected) + basket()->addSelectedNote(); + else + basket()->removeSelectedNote(); + + m_selected = selected; + unbufferize(); + update(); // FIXME: ??? +} + +void Note::resetWasInLastSelectionRect() +{ + m_wasInLastSelectionRect = false; + + FOR_EACH_CHILD (child) + child->resetWasInLastSelectionRect(); +} + +void Note::finishLazyLoad() +{ + if (content()) + content()->finishLazyLoad(); + + FOR_EACH_CHILD (child) + child->finishLazyLoad(); +} + +void Note::selectIn(const QRect &rect, bool invertSelection, bool unselectOthers /*= true*/) +{ +// QRect myRect(x(), y(), width(), height()); + +// bool intersects = myRect.intersects(rect); + + // Only intersects with visible areas. + // If the note is not visible, the user don't think it will be selected while selecting the note(s) that hide this, so act like the user think: + bool intersects = false; + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &r = *it; + if (r.intersects(rect)) { + intersects = true; + break; + } + } + + bool toSelect = intersects || (!unselectOthers && isSelected()); + if (invertSelection) { + if (m_wasInLastSelectionRect == intersects) + toSelect = isSelected(); + else if (intersects xor m_wasInLastSelectionRect) + toSelect = !isSelected();// xor intersects; + } + setSelected(toSelect); + m_wasInLastSelectionRect = intersects; + + Note *child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + child->selectIn(rect, invertSelection, unselectOthers); + else + child->setSelectedRecursivly(false); + child = child->next(); + first = false; + } +} + +bool Note::allSelected() +{ + if (isGroup()) { + Note *child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + if (!child->allSelected()) + return false;; + child = child->next(); + first = false; + } + return true; + } else + return isSelected(); +} + +void Note::setSelectedRecursivly(bool selected) +{ + setSelected(selected && matching()); + + FOR_EACH_CHILD (child) + child->setSelectedRecursivly(selected); +} + +void Note::invertSelectionRecursivly() +{ + if (content()) + setSelected(!isSelected() && matching()); + + FOR_EACH_CHILD (child) + child->invertSelectionRecursivly(); +} + +void Note::unselectAllBut(Note *toSelect) +{ + if (this == toSelect) + setSelectedRecursivly(true); + else { + setSelected(false); + + Note *child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + child->unselectAllBut(toSelect); + else + child->setSelectedRecursivly(false); + child = child->next(); + first = false; + } + } +} + +void Note::invertSelectionOf(Note *toSelect) +{ + if (this == toSelect) + setSelectedRecursivly(!isSelected()); + else { + Note *child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + child->invertSelectionOf(toSelect); + child = child->next(); + first = false; + } + } +} + +Note* Note::theSelectedNote() +{ + if (!isGroup() && isSelected()) + return this; + + Note *selectedOne; + Note *child = firstChild(); + while (child) { + selectedOne = child->theSelectedNote(); + if (selectedOne) + return selectedOne; + child = child->next(); + } + + return 0; +} + +NoteSelection* Note::selectedNotes() +{ + if (content()) + if (isSelected()) + return new NoteSelection(this); + else + return 0; + + NoteSelection *selection = new NoteSelection(this); + + FOR_EACH_CHILD (child) + selection->append(child->selectedNotes()); + + if (selection->firstChild) { + if (selection->firstChild->next) + return selection; + else { + // If 'selection' is a groupe with only one content, return directly that content: + NoteSelection *reducedSelection = selection->firstChild; +// delete selection; // TODO: Cut all connexions of 'selection' before deleting it! + for (NoteSelection *node = reducedSelection; node; node = node->next) + node->parent = 0; + return reducedSelection; + } + } else { + delete selection; + return 0; + } +} + +bool Note::isAfter(Note *note) +{ + if (this == 0 || note == 0) + return true; + + Note *next = this; + while (next) { + if (next == note) + return false; + next = next->nextInStack(); + } + return true; +} + +bool Note::contains(Note *note) +{ +// if (this == note) +// return true; + + while (note) + if (note == this) + return true; + else + note = note->parentNote(); + +// FOR_EACH_CHILD (child) +// if (child->contains(note)) +// return true; + return false; +} + +Note* Note::firstRealChild() +{ + Note *child = m_firstChild; + while (child) { + if ( !child->isGroup() /*&& child->matching()*/ ) + return child; + child = child->firstChild(); + } + // Empty group: + return 0; +} + +Note* Note::lastRealChild() +{ + Note *child = lastChild(); + while (child) { + if (child->content()) + return child; + Note *possibleChild = child->lastRealChild(); + if (possibleChild && possibleChild->content()) + return possibleChild; + child = child->prev(); + } + return 0; +} + +Note* Note::lastChild() +{ + Note *child = m_firstChild; + while (child && child->next()) + child = child->next(); + + return child; +} + +Note* Note::lastSibling() +{ + Note *last = this; + while (last && last->next()) + last = last->next(); + + return last; +} + +int Note::yExpander() +{ + Note *child = firstRealChild(); + if (child && !child->isShown()) + child = child->nextShownInStack(); // FIXME: Restrict scope to 'this' + + if (child) + return (child->height() - EXPANDER_HEIGHT) / 2 + !(child->height()%2); + else // Groups always have at least 2 notes, except for columns which can have no child (but should exists anyway): + return 0; +} + +bool Note::isFree() +{ + return parentNote() == 0 && basket()->isFreeLayout(); +} + +bool Note::isColumn() +{ + return parentNote() == 0 && basket()->isColumnsLayout(); +} + +bool Note::hasResizer() +{ + // "isFree" || "isColmun but not the last" + return parentNote() == 0 && (basket()->isFreeLayout() || m_next != 0L); +} + +int Note::resizerHeight() +{ + return (isColumn() ? basket()->contentsHeight() : height()); +} + +void Note::setHoveredZone(Zone zone) // TODO: Remove setHovered(bool) and assume it is hovered if zone != None !!!!!!! +{ + if (m_hoveredZone != zone) { + if (content()) + content()->setHoveredZone(m_hoveredZone, zone); + m_hoveredZone = zone; + unbufferize(); + } +} + +Note::Zone Note::zoneAt(const QPoint &pos, bool toAdd) +{ + // Keep the resizer highlighted when resizong, even if the cursor is over another note: + if (basket()->resizingNote() == this) + return Resizer; + + // When dropping/pasting something on a column resizer, add it at the bottom of the column, and don't group it whith the whole column: + if (toAdd && isColumn() && hasResizer()) { + int right = rightLimit() - x(); + if ((pos.x() >= right) && (pos.x() < right + RESIZER_WIDTH) && (pos.y() >= 0) && (pos.y() < resizerHeight())) // Code copied from below + return BottomColumn; + } + + // Below a column: + if (isColumn()) { + if (pos.y() >= height() && pos.x() < rightLimit() - x()) + return BottomColumn; + } + + // If toAdd, return only TopInsert, TopGroup, BottomInsert or BottomGroup + // (by spanning those areas in 4 equal rectangles in the note): + if (toAdd) { + if (!isFree() && !Settings::groupOnInsertionLine()) + return (pos.y() < height() / 2 ? TopInsert : BottomInsert); + if (isColumn() && pos.y() >= height()) + return BottomGroup; + if (pos.y() < height() / 2) + if (pos.x() < width() / 2 && !isFree()) + return TopInsert; + else + return TopGroup; + else + if (pos.x() < width() / 2 && !isFree()) + return BottomInsert; + else + return BottomGroup; + } + + // If in the resizer: + if (hasResizer()) { + int right = rightLimit() - x(); + if ((pos.x() >= right) && (pos.x() < right + RESIZER_WIDTH) && (pos.y() >= 0) && (pos.y() < resizerHeight())) + return Resizer; + } + + // If isGroup, return only Group, GroupExpander, TopInsert or BottomInsert: + if (isGroup()) { + if (pos.y() < INSERTION_HEIGHT) + return (isFree() ? TopGroup : TopInsert); + if (pos.y() >= height() - INSERTION_HEIGHT) + return (isFree() ? BottomGroup : BottomInsert); + + if (pos.x() >= NOTE_MARGIN && pos.x() < NOTE_MARGIN + EXPANDER_WIDTH) { + int yExp = yExpander(); + if (pos.y() >= yExp && pos.y() < yExp + EXPANDER_HEIGHT) + return GroupExpander; + } + if (pos.x() < width()) + return Group; + else + return Note::None; + } + + // Else, it's a normal note: + + if (pos.x() < HANDLE_WIDTH) + return Handle; + + if (pos.y() < INSERTION_HEIGHT) + if ((!isFree() && !Settings::groupOnInsertionLine()) || pos.x() < width() / 2 && !isFree()) + return TopInsert; + else + return TopGroup; + + if (pos.y() >= height() - INSERTION_HEIGHT) + if ((!isFree() && !Settings::groupOnInsertionLine()) || pos.x() < width() / 2 && !isFree()) + return BottomInsert; + else + return BottomGroup; + + for (int i =0; i < m_emblemsCount; i++) { + if ( pos.x() >= HANDLE_WIDTH + (NOTE_MARGIN+EMBLEM_SIZE)*i && + pos.x() < HANDLE_WIDTH + (NOTE_MARGIN+EMBLEM_SIZE)*i + NOTE_MARGIN+EMBLEM_SIZE ) + return (Zone)(Emblem0 + i); + } + + if (pos.x() < HANDLE_WIDTH + (NOTE_MARGIN+EMBLEM_SIZE)*m_emblemsCount + NOTE_MARGIN + TAG_ARROW_WIDTH + NOTE_MARGIN) + return TagsArrow; + + if (!linkAt(pos).isEmpty()) + return Link; + + int customZone = content()->zoneAt(pos - QPoint(contentX(), NOTE_MARGIN)); + if (customZone) + return (Note::Zone)customZone; + + return Content; +} + +QString Note::linkAt(const QPoint &pos) +{ + QString link = m_content->linkAt(pos - QPoint(contentX(), NOTE_MARGIN)); + if (link.isEmpty()) + return link; + else + return NoteFactory::filteredURL(KURL(link)).prettyURL();//KURIFilter::self()->filteredURI(link); +} + +int Note::contentX() +{ + return HANDLE_WIDTH + NOTE_MARGIN + (EMBLEM_SIZE+NOTE_MARGIN)*m_emblemsCount + TAG_ARROW_WIDTH + NOTE_MARGIN; +} + +QRect Note::zoneRect(Note::Zone zone, const QPoint &pos) +{ + if (zone >= Emblem0) + return QRect(HANDLE_WIDTH + (NOTE_MARGIN+EMBLEM_SIZE)*(zone-Emblem0), + INSERTION_HEIGHT, + NOTE_MARGIN + EMBLEM_SIZE, + height() - 2*INSERTION_HEIGHT); + + int yExp; + int right; + int xGroup = (isFree() ? (isGroup() ? 0 : GROUP_WIDTH) : width() / 2); + QRect rect; + int insertSplit = (Settings::groupOnInsertionLine() ? 2 : 1); + switch (zone) { + case Note::Handle: return QRect(0, 0, HANDLE_WIDTH, height()); + case Note::Group: + yExp = yExpander(); + if (pos.y() < yExp) return QRect(0, INSERTION_HEIGHT, width(), yExp - INSERTION_HEIGHT); + if (pos.y() > yExp + EXPANDER_HEIGHT) return QRect(0, yExp + EXPANDER_HEIGHT, width(), height() - yExp - EXPANDER_HEIGHT - INSERTION_HEIGHT); + if (pos.x() < NOTE_MARGIN) return QRect(0, 0, NOTE_MARGIN, height()); + else return QRect(width() - NOTE_MARGIN, 0, NOTE_MARGIN, height()); + case Note::TagsArrow: return QRect(HANDLE_WIDTH + (NOTE_MARGIN+EMBLEM_SIZE)*m_emblemsCount, + INSERTION_HEIGHT, + NOTE_MARGIN + TAG_ARROW_WIDTH + NOTE_MARGIN, + height() - 2*INSERTION_HEIGHT); + case Note::Custom0: + case Note::Content: rect = content()->zoneRect(zone, pos - QPoint(contentX(), NOTE_MARGIN)); + rect.moveBy(contentX(), NOTE_MARGIN); + return rect.intersect( QRect(contentX(), INSERTION_HEIGHT, width() - contentX(), height() - 2*INSERTION_HEIGHT) ); // Only IN contentRect + case Note::GroupExpander: return QRect(NOTE_MARGIN, yExpander(), EXPANDER_WIDTH, EXPANDER_HEIGHT); + case Note::Resizer: right = rightLimit(); + return QRect(right - x(), 0, RESIZER_WIDTH, resizerHeight()); + case Note::Link: + case Note::TopInsert: if (isGroup()) return QRect(0, 0, width(), INSERTION_HEIGHT); + else return QRect(HANDLE_WIDTH, 0, width() / insertSplit - HANDLE_WIDTH, INSERTION_HEIGHT); + case Note::TopGroup: return QRect(xGroup, 0, width() - xGroup, INSERTION_HEIGHT); + case Note::BottomInsert: if (isGroup()) return QRect(0, height() - INSERTION_HEIGHT, width(), INSERTION_HEIGHT); + else return QRect(HANDLE_WIDTH, height() - INSERTION_HEIGHT, width() / insertSplit - HANDLE_WIDTH, INSERTION_HEIGHT); + case Note::BottomGroup: return QRect(xGroup, height() - INSERTION_HEIGHT, width() - xGroup, INSERTION_HEIGHT); + case Note::BottomColumn: return QRect(0, height(), rightLimit() - x(), basket()->contentsHeight() - height()); + case Note::None: return QRect(/*0, 0, -1, -1*/); + default: return QRect(/*0, 0, -1, -1*/); + } +} + +void Note::setCursor(Zone zone) +{ + switch (zone) { + case Note::Handle: + case Note::Group: basket()->viewport()->setCursor(Qt::SizeAllCursor); break; + case Note::Resizer: if (isColumn()) + basket()->viewport()->setCursor(Qt::SplitHCursor); + else + basket()->viewport()->setCursor(Qt::SizeHorCursor); break; + + case Note::Custom0: content()->setCursor(basket()->viewport(), zone); break; + + case Note::Link: + case Note::TagsArrow: + case Note::GroupExpander: basket()->viewport()->setCursor(Qt::PointingHandCursor); break; + + case Note::Content: basket()->viewport()->setCursor(Qt::IbeamCursor); break; + + case Note::TopInsert: + case Note::TopGroup: + case Note::BottomInsert: + case Note::BottomGroup: + case Note::BottomColumn: basket()->viewport()->setCursor(Qt::CrossCursor); break; + case Note::None: basket()->viewport()->unsetCursor(); break; + default: + State *state = stateForEmblemNumber(zone - Emblem0); + if (state && state->parentTag()->states().count() > 1) + basket()->viewport()->setCursor(Qt::PointingHandCursor); + else + basket()->viewport()->unsetCursor(); + } +} + +void Note::addAnimation(int deltaX, int deltaY, int deltaHeight) +{ + // Don't process animation that make the note stay in place! + if (deltaX == 0 && deltaY == 0 && deltaHeight == 0) + return; + + // If it was not animated previsouly, make it animated: + if (m_deltaX == 0 && m_deltaY == 0 && m_deltaHeight == 0) + basket()->addAnimatedNote(this); + + // Configure the animation: + m_deltaX += deltaX; + m_deltaY += deltaY; + m_deltaHeight += deltaHeight; +} + +void Note::setFinalPosition(int x, int y) +{ + addAnimation(x - finalX(), y - finalY()); +} + +void Note::initAnimationLoad() +{ + int x, y; + switch (rand() % 4) { + case 0: // Put it on top: + x = basket()->contentsX() + rand() % basket()->contentsWidth(); + y = -height(); + break; + case 1: // Put it on bottom: + x = basket()->contentsX() + rand() % basket()->contentsWidth(); + y = basket()->contentsY() + basket()->visibleHeight(); + break; + case 2: // Put it on left: + x = -width() - (hasResizer() ? Note::RESIZER_WIDTH : 0); + y = basket()->contentsY() + rand() % basket()->visibleHeight(); + break; + case 3: // Put it on right: + default: // In the case of... + x = basket()->contentsX() + basket()->visibleWidth(); + y = basket()->contentsY() + rand() % basket()->visibleHeight(); + break; + } + cancelAnimation(); + addAnimation(finalX() - x, finalY() - y); + setX(x); + setY(y); + + if (isGroup()) { + const int viewHeight = basket()->contentsY() + basket()->visibleHeight(); + Note *child = firstChild(); + bool first = true; + while (child) { + if (child->finalY() < viewHeight) { + if ((showSubNotes() || first) && child->matching()) + child->initAnimationLoad(); + } else + break; // 'child' are not a free notes (because child of at least one note, 'this'), so 'child' is ordered vertically. + child = child->next(); + first = false; + } + } +} + + +bool Note::advance() +{ + // Animate X: + if (m_deltaX != 0) { + int deltaX = m_deltaX / 3; + if (deltaX == 0) + deltaX = (m_deltaX > 0 ? 1 : -1); + setX(m_x + deltaX); + m_deltaX -= deltaX; + } + + // Animate Y: + if (m_deltaY != 0) { + int deltaY = m_deltaY / 3; + if (deltaY == 0) + deltaY = (m_deltaY > 0 ? 1 : -1); + setY(m_y + deltaY); + m_deltaY -= deltaY; + } + + // Animate Height: + if (m_deltaHeight != 0) { + int deltaHeight = m_deltaHeight / 3; + if (deltaHeight == 0) + deltaHeight = (m_deltaHeight > 0 ? 1 : -1); + m_height += deltaHeight; + unbufferize(); + m_deltaHeight -= deltaHeight; + } + + if (m_deltaHeight == 0) { + m_collapseFinished = true; + m_expandingFinished = true; + } + + // Return true if the animation is finished: + return (m_deltaX == 0 && m_deltaY == 0 && m_deltaHeight == 0); +} + +void Note::unsetWidth() +{ + m_width = 0; + unbufferize(); + + FOR_EACH_CHILD (child) + child->unsetWidth(); +} + +void Note::requestRelayout() +{ + m_width = 0; + unbufferize(); + basket()->relayoutNotes(true); // TODO: A signal that will relayout ONCE and DELAYED if called several times +} + +void Note::setWidth(int width) // TODO: inline ? +{ + if (m_width != width) + setWidthForceRelayout(width); +} + +void Note::setWidthForceRelayout(int width) +{ + unbufferize(); + m_width = (width < minWidth() ? minWidth() : width); + int contentWidth = width - contentX() - NOTE_MARGIN; + if (m_content) { ///// FIXME: is this OK? + if (contentWidth < 1) + contentWidth = 1; + if (contentWidth < m_content->minWidth()) + contentWidth = m_content->minWidth(); + m_height = m_content->setWidthAndGetHeight(contentWidth/* < 1 ? 1 : contentWidth*/) + 2 * NOTE_MARGIN; + if (m_height < 3 * INSERTION_HEIGHT) // Assure a minimal size... + m_height = 3 * INSERTION_HEIGHT; + } +} + +int Note::minWidth() +{ + if (m_content) + return contentX() + m_content->minWidth() + NOTE_MARGIN; + else + return GROUP_WIDTH; ///// FIXME: is this OK? +} + +int Note::minRight() +{ + if (isGroup()) { + int right = finalX() + width(); + Note* child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + right = QMAX(right, child->minRight()); + child = child->next(); + first = false; + } + if (isColumn()) { + int minColumnRight = finalX() + 2*HANDLE_WIDTH; + if (right < minColumnRight) + return minColumnRight; + } + return right; + } else + return finalX() + minWidth(); +} + +void Note::setX(int x) +{ + if (m_x == x) + return; + + if (isBufferized() && basket()->hasBackgroundImage()) { + // Unbufferize only if the background change: + if (basket()->isTiledBackground()) + unbufferize(); + else { + int bgw = basket()->backgroundPixmap()->width(); + if (m_x >= bgw && x < bgw) // Was not in the background image and is now inside it: + unbufferize(); + else if (m_x < bgw) // Was in the background image and is now at another position of the background image or is now outside: + unbufferize(); + } + } + + m_x = x; +} + +void Note::setY(int y) +{ + if (m_y == y) + return; + + if (isBufferized() && basket()->hasBackgroundImage()) { + // Unbufferize only if the background change: + if (basket()->isTiledBackground()) + unbufferize(); + else { + int bgh = basket()->backgroundPixmap()->height(); + if (m_y >= bgh && y < bgh) // Was not in the background image and is now inside it: + unbufferize(); + else if (m_y < bgh) // Was in the background image and is now at another position of the background image or is now outside: + unbufferize(); + } + } + + m_y = y; +} + + +void Note::toggleFolded(bool animate) +{ + // Close the editor if it was editing a note that we are about to hide after collapsing: + if (!m_isFolded && basket() && basket()->isDuringEdit()) { + if (contains(basket()->editedNote()) && firstRealChild() != basket()->editedNote()) + basket()->closeEditor(); + } + + // Important to close the editor FIRST, because else, the last edited note would not show during folding animation (don't ask me why ;-) ): + m_isFolded = ! m_isFolded; + + unbufferize(); + + if (animate) { + // We animate collapsing (so sub-notes fluidly go under the first note) + // We don't animate expanding: we place sub-notes directly under the first note (and the next relayout will animate the expanding) + // But if user quickly collapsed and then expand (while the collapsing animation isn't finished), we animate anyway + bool animateSetUnder = (m_isFolded || !m_collapseFinished); +// std::cout << "fold:" << m_isFolded << " collapseFinished:" << m_collapseFinished << " animateSetUnder:" << animateSetUnder << std::endl; + + if (m_isFolded) + m_collapseFinished = false; + else + m_expandingFinished = false; + + Note* note = firstChild(); + if (note) { + note->setOnTop(true); + while ( (note = note->next()) ) { // Don't process the first child: it is OK + note->setRecursivelyUnder(/*firstRealChild*/firstChild(), animateSetUnder); + note->setOnTop(false); + } + } + } + + //if (basket()->focusedNote() && !basket()->focusedNote()->isShown()) { + if (basket()->isLoaded()) { + basket()->setFocusedNote(firstRealChild()); + basket()->m_startOfShiftSelectionNote = firstRealChild(); + } + + if (basket()->isLoaded() && !m_isFolded) { + //basket()->setFocusedNote(this); + basket()->relayoutNotes(true); + basket()->ensureNoteVisible(this); + } + + basket()->save(); // FIXME: SHOULD WE ALWAYS SAVE ???????? +} + +void Note::setRecursivelyUnder(Note *under, bool animate) +{ + int y = /*finalHeight() > under->finalHeight() ? under->finalY() :*/ under->finalBottom() - finalHeight() + 1; + if (animate) + setFinalPosition(finalX(), y); + else { + setY(y); + cancelAnimation(); + } + + if (isGroup()) + FOR_EACH_CHILD (child) + child->setRecursivelyUnder(under, animate); +} + + +Note* Note::noteAt(int x, int y) +{ + if (matching() && hasResizer()) { + int right = rightLimit(); + // TODO: This code is dupliacted 3 times: !!!! + if ((x >= right) && (x < right + RESIZER_WIDTH) && (y >= m_y) && (y < m_y + resizerHeight())) { + if ( ! m_computedAreas ) + recomputeAreas(); + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + if (rect.contains(x, y)) + return this; + } + } + } + + if (isGroup()) { + if ((x >= m_x) && (x < m_x + width()) && (y >= m_y) && (y < m_y + m_height)) { + if ( ! m_computedAreas ) + recomputeAreas(); + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + if (rect.contains(x, y)) + return this; + } + return NULL; + } + Note *child = firstChild(); + Note *found; + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) { + found = child->noteAt(x, y); + if (found) + return found; + } + child = child->next(); + first = false; + } + } else if (matching() && y >= m_y && y < m_y + m_height && x >= m_x && x < m_x + m_width) { + if ( ! m_computedAreas ) + recomputeAreas(); + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + if (rect.contains(x, y)) + return this; + } + return NULL; + } + + return NULL; +} + +QRect Note::rect() +{ + return QRect(x(), y(), width(), height()); +} + +QRect Note::resizerRect() +{ + return QRect(rightLimit(), y(), RESIZER_WIDTH, resizerHeight()); +} + + +bool Note::showSubNotes() +{ + return !m_isFolded || !m_collapseFinished || basket()->isFiltering(); +} + +void Note::relayoutAt(int x, int y, bool animate) +{ + if (!matching()) + return; + + m_computedAreas = false; + m_areas.clear(); + + // Don't relayout free notes one under the other, because by definition they are freely positionned! + if (isFree()) { + x = finalX(); + y = finalY(); + // If it's a column, it always have the same "fixed" position (no animation): + } else if (isColumn()) { + x = (prev() ? prev()->rightLimit() + RESIZER_WIDTH : 0); + y = 0; + cancelAnimation(); + setX(x); + setY(y); + // But relayout others vertically if they are inside such primary groups or if it is a "normal" basket: + } else { + if (animate) + setFinalPosition(x, y); + else { + cancelAnimation(); + setX(x); + setY(y); + } + } + + // Then, relayout sub-notes (only the first, if the group is folded) and so, assign an height to the group: + if (isGroup()) { + int h = 0; + Note *child = firstChild(); + bool first = true; + while (child) { + if (child->matching() && (!m_isFolded || first || basket()->isFiltering())) { // Don't use showSubNotes() but use !m_isFolded because we don't want a relayout for the animated collapsing notes + child->relayoutAt(x + width(), y+h, animate); + h += child->finalHeight(); + } else // In case the user collapse a group, then move it and then expand it: + child->setXRecursivly(x + width()); // notes SHOULD have a good X coordonate, and not the old one! + // For future animation when re-match, but on bottom of already matched notes! + // Find parent primary note and set the Y to THAT y: + if (!child->matching()) + child->setY(parentPrimaryNote()->y()); + child = child->next(); + first = false; + } + if (finalHeight() != h || m_height != h) { + unbufferize(); + if (animate) + addAnimation(0, 0, h - finalHeight()); + else { + m_height = h; + unbufferize(); + } + } + } else { + setWidth(finalRightLimit() - x); + // If rightLimit is excedded, set the top-level right limit!!! + // and NEED RELAYOUT + } + + // Set the basket area limits (but not for child notes: no need, because they will look for theire parent note): + if (!parentNote()) { + if (basket()->tmpWidth < finalRightLimit() + (hasResizer() ? RESIZER_WIDTH : 0)) + basket()->tmpWidth = finalRightLimit() + (hasResizer() ? RESIZER_WIDTH : 0); + if (basket()->tmpHeight < finalY() + finalHeight()) + basket()->tmpHeight = finalY() + finalHeight(); + // However, if the note exceed the allowed size, let it! : + } else if (!isGroup()) { + if (basket()->tmpWidth < finalX() + width()) + basket()->tmpWidth = finalX() + width(); + if (basket()->tmpHeight < finalY() + finalHeight()) + basket()->tmpHeight = finalY() + finalHeight(); + } +} + +void Note::setXRecursivly(int x) +{ + m_deltaX = 0; + setX(x); + + FOR_EACH_CHILD (child) + child->setXRecursivly(x + width()); +} + +void Note::setYRecursivly(int y) +{ + m_deltaY = 0; + setY(y); + + FOR_EACH_CHILD (child) + child->setYRecursivly(y); +} + +void Note::setGroupWidth(int width) +{ + m_groupWidth = width; +} + +int Note::groupWidth() +{ + if (hasResizer()) + return m_groupWidth; + else + return rightLimit() - x(); +} + +int Note::rightLimit() +{ + if (isColumn() && m_next == 0L) // The last column + return QMAX(x() + minWidth(), basket()->visibleWidth()); + else if (parentNote()) + return parentNote()->rightLimit(); + else + return m_x + m_groupWidth; +} + +int Note::finalRightLimit() +{ + if (isColumn() && m_next == 0L) // The last column + return QMAX(finalX() + minWidth(), basket()->visibleWidth()); + else if (parentNote()) + return parentNote()->finalRightLimit(); + else + return finalX() + m_groupWidth; +} + +/* + * This code is derivated from drawMetalGradient() from the Qt documentation: + */ +void drawGradient( QPainter *p, const QColor &colorTop, const QColor & colorBottom, + int x, int y, int w, int h, + bool sunken, bool horz, bool flat ) /*const*/ +{ + QColor highlight(colorBottom); + QColor subh1(colorTop); + QColor subh2(colorTop); + + QColor topgrad(colorTop); + QColor botgrad(colorBottom); + + + if ( flat && !sunken ) + p->fillRect(x, y, w, h, colorTop); + else { + int i = 0; + int x1 = x; + int y1 = y; + int x2 = x + w - 1; + int y2 = y + h - 1; + if ( horz ) + x2 = x2; + else + y2 = y2; + +#define DRAWLINE if (horz) \ + p->drawLine( x1, y1+i, x2, y1+i ); \ + else \ + p->drawLine( x1+i, y1, x1+i, y2 ); \ + i++; + + // Gradient: + int ng = (horz ? h : w); // how many lines for the gradient? + + int h1, h2, s1, s2, v1, v2; + if ( !sunken ) { + topgrad.hsv( &h1, &s1, &v1 ); + botgrad.hsv( &h2, &s2, &v2 ); + } else { + botgrad.hsv( &h1, &s1, &v1 ); + topgrad.hsv( &h2, &s2, &v2 ); + } + + if ( ng > 1 ) { + for ( int j =0; j < ng; j++ ) { + p->setPen( QColor( h1 + ((h2-h1)*j)/(ng-1), + s1 + ((s2-s1)*j)/(ng-1), + v1 + ((v2-v1)*j)/(ng-1), QColor::Hsv ) ); + DRAWLINE; + } + } else if ( ng == 1 ) { + p->setPen( QColor((h1+h2)/2, (s1+s2)/2, (v1+v2)/2, QColor::Hsv) ); + DRAWLINE; + } + } +} + +void Note::drawExpander(QPainter *painter, int x, int y, const QColor &background, bool expand, Basket *basket) +{ + // If the current style is a KStyle, use it to draw the expander (plus or minus): + if (dynamic_cast<KStyle*>(&(kapp->style())) != NULL) { + // Set the 4 rounded corners background to background color: + QColorGroup cg(basket->colorGroup()); + cg.setColor(QColorGroup::Base, background); + + // Fill the inside of the expander in white, typically: + QBrush brush(KGlobalSettings::baseColor()); + painter->fillRect(x, y, 9, 9, brush); + + // Draw it: + ((KStyle&)(kapp->style())).drawKStylePrimitive( KStyle::KPE_ListViewExpander, + painter, + basket->viewport(), + QRect(x, y, 9, 9), + cg, + (expand ? QStyle::Style_On : QStyle::Style_Off) ); + // Else, QStyle does not provide easy way to do so (if it's doable at all...) + // So, I'm drawing it myself my immitating Plastik (pretty style)... + // After all, the note/group handles are all non-QStyle aware so that doesn't matter if the expander is a custom one too. + } else { + int width = EXPANDER_WIDTH; + int height = EXPANDER_HEIGHT; + const QColorGroup &cg = basket->colorGroup(); + + // Fill white area: + painter->fillRect(x + 1, y + 1, width - 2, height - 2, cg.base()); + // Draw contour lines: + painter->setPen(cg.dark()); + painter->drawLine(x + 2, y, x + width - 3, y); + painter->drawLine(x + 2, y + height - 1, x + width - 3, y + height - 1); + painter->drawLine(x, y + 2, x, y + height - 3); + painter->drawLine(x + width - 1, y + 2, x + width - 1, y + height - 3); + // Draw edge points: + painter->drawPoint(x + 1, y + 1); + painter->drawPoint(x + width - 2, y + 1); + painter->drawPoint(x + 1, y + height - 2); + painter->drawPoint(x + width - 2, y + height - 2); + // Draw anti-aliased points: + painter->setPen(Tools::mixColor(cg.dark(), background)); + painter->drawPoint(x + 1, y); + painter->drawPoint(x + width - 2, y); + painter->drawPoint(x, y + 1); + painter->drawPoint(x + width - 1, y + 1); + painter->drawPoint(x, y + height - 2); + painter->drawPoint(x + width - 1, y + height - 2); + painter->drawPoint(x + 1, y + height - 1); + painter->drawPoint(x + width - 2, y + height - 1); + // Draw plus / minus: + painter->setPen(cg.text()); + painter->drawLine(x + 2, y + height / 2, x + width - 3, y + height / 2); + if (expand) + painter->drawLine(x + width / 2, y + 2, x + width / 2, y + height - 3); + } +} + +QColor expanderBackground(int height, int y, const QColor &foreground) +{ + // We will divide height per two, substract one and use that below a division bar: + // To avoid division by zero error, height should be bigger than 3. + // And to avoid y errors or if y is on the borders, we return the border color: the background color. + if (height <= 3 || y <= 0 || y >= height - 1) + return foreground; + + QColor dark = foreground.dark(110); // 1/1.1 of brightness + QColor light = foreground.light(150); // 50% brighter + + int h1, h2, s1, s2, v1, v2; + int ng; + if (y <= (height-2)/2) { + light.hsv( &h1, &s1, &v1 ); + dark.hsv( &h2, &s2, &v2 ); + ng = (height-2)/2; + y -= 1; + } else { + dark.hsv( &h1, &s1, &v1 ); + foreground.hsv( &h2, &s2, &v2 ); + ng = (height-2)-(height-2)/2; + y -= 1 + (height-2)/2; + } + return QColor( h1 + ((h2-h1)*y)/(ng-1), + s1 + ((s2-s1)*y)/(ng-1), + v1 + ((v2-v1)*y)/(ng-1), QColor::Hsv ); +} + +void Note::drawHandle(QPainter *painter, int x, int y, int width, int height, const QColor &background, const QColor &foreground) +{ + QPen backgroundPen(background); + QPen foregroundPen(foreground); + + QColor dark = foreground.dark(110); // 1/1.1 of brightness + QColor light = foreground.light(150); // 50% brighter + + // Draw the surrounding rectangle: + painter->setPen(foregroundPen); + painter->drawLine(0, 0, width - 1, 0); + painter->drawLine(0, 0, 0, height - 1); + painter->drawLine(width - 1, 0, width - 1, height - 1); + painter->drawLine(0, height - 1, width - 1, height - 1); + + // Draw the gradients: + drawGradient( painter, light, dark, 1 + x, 1 + y, width-2, (height-2)/2, /*sunken=*/false, /*horz=*/true, /*flat=*/false ); + drawGradient( painter, dark, foreground, 1 + x, 1 + y + (height-2)/2, width-2, (height-2)-(height-2)/2, /*sunken=*/false, /*horz=*/true, /*flat=*/false ); + + // Round the top corner with background color: + painter->setPen(backgroundPen); + painter->drawLine(0, 0, 0, 3); + painter->drawLine(1, 0, 3, 0); + painter->drawPoint(1, 1); + // Round the bottom corner with background color: + painter->drawLine(0, height-1, 0, height-4); + painter->drawLine(1, height-1, 3, height-1); + painter->drawPoint(1, height-2); + + // Surrounding line of the rounded top-left corner: + painter->setPen(foregroundPen); + painter->drawLine(1, 2, 1, 3); + painter->drawLine(2, 1, 3, 1); + + // Anti-aliased rounded top corner (1/2): + painter->setPen(Tools::mixColor(foreground, background)); + painter->drawPoint(0, 3); + painter->drawPoint(3, 0); + // Anti-aliased rounded bottom corner: + painter->drawPoint(0, height - 4); + painter->drawPoint(3, height - 1); + // Anti-aliased rounded top corner (2/2): + painter->setPen(Tools::mixColor(foreground, light)); + painter->drawPoint(2, 2); + + // Draw the grips: + int xGrips = 4; + int marginedHeight = (height * 80 / 100); // 10% empty on top, and 10% empty on bottom, so 20% of the height should be empty of any grip, and 80% should be in the grips + int nbGrips = (marginedHeight - 3) / 6; + if (nbGrips < 2) + nbGrips = 2; + int yGrips = (height + 1 - nbGrips * 6 - 3) / 2; // +1 to avoid rounding errors, -nbGrips*6-3 the size of the grips + QColor darker = foreground.dark(130); + QColor lighter = foreground.light(130); + for (int i = 0; i < nbGrips; ++i) { + /// Dark color: + painter->setPen(darker); + // Top-left point: + painter->drawPoint(xGrips, yGrips); + painter->drawPoint(xGrips + 1, yGrips); + painter->drawPoint(xGrips, yGrips + 1); + // Bottom-right point: + painter->drawPoint(xGrips + 4, yGrips + 3); + painter->drawPoint(xGrips + 5, yGrips + 3); + painter->drawPoint(xGrips + 4, yGrips + 4); + /// Light color: + painter->setPen(lighter); + // Top-left point: + painter->drawPoint(xGrips + 1, yGrips + 1); + // Bottom-right point: + painter->drawPoint(xGrips + 5, yGrips + 4); + yGrips += 6; + } + // The remaining point: + painter->setPen(darker); + painter->drawPoint(xGrips, yGrips); + painter->drawPoint(xGrips + 1, yGrips); + painter->drawPoint(xGrips, yGrips + 1); + painter->setPen(lighter); + painter->drawPoint(xGrips + 1, yGrips + 1); +} + +void Note::drawResizer(QPainter *painter, int x, int y, int width, int height, const QColor &background, const QColor &foreground, bool rounded) +{ + QPen backgroundPen(background); + QPen foregroundPen(foreground); + + QColor dark = foreground.dark(110); // 1/1.1 of brightness + QColor light = foreground.light(150); // 50% brighter + QColor midLight = foreground.light(105); // 5% brighter + + // Draw the surrounding rectangle: + painter->setPen(foregroundPen); + painter->drawRect(0, 0, width, height); + + // Draw the gradients: + drawGradient( painter, light, dark, 1 + x, 1 + y, width-2, (height-2)/2, /*sunken=*/false, /*horz=*/true, /*flat=*/false ); + drawGradient( painter, dark, foreground, 1 + x, 1 + y + (height-2)/2, width-2, (height-2)-(height-2)/2, /*sunken=*/false, /*horz=*/true, /*flat=*/false ); + + if (rounded) { + // Round the top corner with background color: + painter->setPen(backgroundPen); + painter->drawLine(width - 1, 0, width - 3, 0); + painter->drawLine(width - 1, 1, width - 1, 2); + painter->drawPoint(width - 2, 1); + // Round the bottom corner with background color: + painter->drawLine(width - 1, height - 1, width - 1, height - 4); + painter->drawLine(width - 2, height - 1, width - 4, height - 1); + painter->drawPoint(width - 2, height-2); + + // Surrounding line of the rounded top-left corner: + painter->setPen(foregroundPen); + painter->drawLine(width-2, 2, width-2, 3); + painter->drawLine(width-3, 1, width-4, 1); + + // Anti-aliased rounded top corner (1/2): + painter->setPen(Tools::mixColor(foreground, background)); + painter->drawPoint(width - 1, 3); + painter->drawPoint(width - 4, 0); + // Anti-aliased rounded bottom corner: + painter->drawPoint(width - 1, height - 4); + painter->drawPoint(width - 4, height - 1); + // Anti-aliased rounded top corner (2/2): + painter->setPen(Tools::mixColor(foreground, light)); + painter->drawPoint(width - 3, 2); + } + + // Draw the arows: + int xArrow = 2; + int hMargin = 9; + int countArrows = (height >= hMargin*4 + 6*3 ? 3 : (height >= hMargin*3 + 6*2 ? 2 : 1)); + QColor darker = foreground.dark(130); + QColor lighter = foreground.light(130); + for (int i = 0; i < countArrows; ++i) { + int yArrow; + switch (countArrows) { + default: + case 1: yArrow = (height-6) / 2; break; + case 2: yArrow = (i == 1 ? hMargin : height - hMargin - 6); break; + case 3: yArrow = (i == 1 ? hMargin : (i == 2 ? (height-6) / 2 : height - hMargin - 6)); break; + } + /// Dark color: + painter->setPen(darker); + // Left arrow: + painter->drawLine(xArrow, yArrow + 2, xArrow + 2, yArrow); + painter->drawLine(xArrow, yArrow + 2, xArrow + 2, yArrow + 4); + // Right arrow: + painter->drawLine(width - 1 - xArrow, yArrow + 2, width - 1 - xArrow - 2, yArrow); + painter->drawLine(width - 1 - xArrow, yArrow + 2, width - 1 - xArrow - 2, yArrow + 4); + /// Light color: + painter->setPen(lighter); + // Left arrow: + painter->drawLine(xArrow, yArrow + 2 + 1, xArrow + 2, yArrow + 1); + painter->drawLine(xArrow, yArrow + 2 + 1, xArrow + 2, yArrow + 4 + 1); + // Right arrow: + painter->drawLine(width - 1 - xArrow, yArrow + 2 + 1, width - 1 - xArrow - 2, yArrow + 1); + painter->drawLine(width - 1 - xArrow, yArrow + 2 + 1, width - 1 - xArrow - 2, yArrow + 4 + 1); + } +} + +void Note::drawInactiveResizer(QPainter *painter, int x, int y, int height, const QColor &background, bool column) +{ + // If background color is too dark, we compute a lighter color instead of a darker: + QColor darkBgColor = (Tools::tooDark(background) ? background.light(120) : background.dark(105)); + if (column) { + int halfWidth = RESIZER_WIDTH / 2; + drawGradient(painter, darkBgColor, background, x, y, halfWidth, height, /*sunken=*/false, /*horz=*/false, /*flat=*/false); + drawGradient(painter, background, darkBgColor, halfWidth, y, RESIZER_WIDTH - halfWidth, height, /*sunken=*/false, /*horz=*/false, /*flat=*/false); + } else + drawGradient(painter, darkBgColor, background, x, y, RESIZER_WIDTH, height, /*sunken=*/false, /*horz=*/false, /*flat=*/false ); +} + + +#include <qimage.h> +#include <kimageeffect.h> + +/* type: 1: topLeft + * 2: bottomLeft + * 3: topRight + * 4: bottomRight + * 5: fourCorners + * 6: noteInsideAndOutsideCorners + * (x,y) relate to the painter origin + * (width,height) only used for 5:fourCorners type + */ +void Note::drawRoundings(QPainter *painter, int x, int y, int type, int width, int height) +{ + int right; + + switch (type) { + case 1: + x += this->x(); + y += this->y(); + basket()->blendBackground(*painter, QRect(x, y, 4, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 1, 2, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 3, 1, 1), this->x(), this->y()); + break; + case 2: + x += this->x(); + y += this->y(); + basket()->blendBackground(*painter, QRect(x, y - 1, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 1, 2, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + 2, 4, 1), this->x(), this->y()); + break; + case 3: + right = rightLimit(); + x += right; + y += this->y(); + basket()->blendBackground(*painter, QRect(x - 1, y, 4, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 1, y + 1, 2, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 2, y + 2, 1, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 2, y + 3, 1, 1), right, this->y()); + break; + case 4: + right = rightLimit(); + x += right; + y += this->y(); + basket()->blendBackground(*painter, QRect(x + 2, y - 1, 1, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 2, y, 1, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x + 1, y + 1, 2, 1), right, this->y()); + basket()->blendBackground(*painter, QRect(x - 1, y + 2, 4, 1), right, this->y()); + break; + case 5: + // First make sure the corners are white (depending on the widget style): + painter->setPen(basket()->backgroundColor()); + painter->drawPoint(x, y); + painter->drawPoint(x + width - 1, y); + painter->drawPoint(x + width - 1, y + height - 1); + painter->drawPoint(x, y + height - 1); + // And then blend corners: + x += this->x(); + y += this->y(); + basket()->blendBackground(*painter, QRect(x, y, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 1, y, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 1, y + height - 1, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x, y + height - 1, 1, 1), this->x(), this->y()); + break; + case 6: + x += this->x(); + y += this->y(); + //if (!isSelected()) { + // Inside left corners: + basket()->blendBackground(*painter, QRect(x + HANDLE_WIDTH + 1, y + 1, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + HANDLE_WIDTH, y + 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + HANDLE_WIDTH + 1, y + height - 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + HANDLE_WIDTH, y + height - 3, 1, 1), this->x(), this->y()); + // Inside right corners: + basket()->blendBackground(*painter, QRect(x + width - 4, y + 1, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 3, y + 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 4, y + height - 2, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 3, y + height - 3, 1, 1), this->x(), this->y()); + //} + // Outside right corners: + basket()->blendBackground(*painter, QRect(x + width - 1, y, 1, 1), this->x(), this->y()); + basket()->blendBackground(*painter, QRect(x + width - 1, y + height - 1, 1, 1), this->x(), this->y()); + break; + } +} + +/// Blank Spaces Drawing: + +void Note::setOnTop(bool onTop) +{ + m_onTop = onTop; + + Note *note = firstChild(); + while (note) { + note->setOnTop(onTop); + note = note->next(); + } +} + +void substractRectOnAreas(const QRect &rectToSubstract, QValueList<QRect> &areas, bool andRemove) +{ + for (QValueList<QRect>::iterator it = areas.begin(); it != areas.end(); ) { + QRect &rect = *it; + // Split the rectangle if it intersects with rectToSubstract: + if (rect.intersects(rectToSubstract)) { + // Create the top rectangle: + if (rectToSubstract.top() > rect.top()) { + areas.insert(it, QRect(rect.left(), rect.top(), rect.width(), rectToSubstract.top() - rect.top())); + rect.setTop(rectToSubstract.top()); + } + // Create the bottom rectangle: + if (rectToSubstract.bottom() < rect.bottom()) { + areas.insert(it, QRect(rect.left(), rectToSubstract.bottom() + 1, rect.width(), rect.bottom() - rectToSubstract.bottom())); + rect.setBottom(rectToSubstract.bottom()); + } + // Create the left rectangle: + if (rectToSubstract.left() > rect.left()) { + areas.insert(it, QRect(rect.left(), rect.top(), rectToSubstract.left() - rect.left(), rect.height())); + rect.setLeft(rectToSubstract.left()); + } + // Create the right rectangle: + if (rectToSubstract.right() < rect.right()) { + areas.insert(it, QRect(rectToSubstract.right() + 1, rect.top(), rect.right() - rectToSubstract.right(), rect.height())); + rect.setRight(rectToSubstract.right()); + } + // Remove the rectangle if it's entirely contained: + if (andRemove && rectToSubstract.contains(rect)) + it = areas.remove(it); + else + ++it; + } else + ++it; + } +} + +void Note::recomputeAreas() +{ + // Initialize the areas with the note rectangle(s): + m_areas.clear(); + m_areas.append(visibleRect()); + if (hasResizer()) + m_areas.append(resizerRect()); + + // Cut the areas where other notes are on top of this note: + Note *note = basket()->firstNote(); + bool noteIsAfterThis = false; + while (note) { + noteIsAfterThis = recomputeAreas(note, noteIsAfterThis); + note = note->next(); + } +} + +bool Note::recomputeAreas(Note *note, bool noteIsAfterThis) +{ + if (note == this) + noteIsAfterThis = true; + // Only compute overlapping of notes AFTER this, or ON TOP this: + //else if ( note->matching() && noteIsAfterThis && (!isOnTop() || (isOnTop() && note->isOnTop())) || (!isOnTop() && note->isOnTop()) ) { + else if ( note->matching() && noteIsAfterThis && (!(isOnTop() || isEditing()) || ((isOnTop() || isEditing()) && (note->isOnTop() || note->isEditing()))) || + (!(isOnTop() || isEditing()) && (note->isOnTop() || note->isEditing())) ) { + //if (!(isSelected() && !note->isSelected())) { // FIXME: FIXME: FIXME: FIXME: This last condition was added LATE, so we should look if it's ALWAYS good: + substractRectOnAreas(note->visibleRect(), m_areas, true); + if (note->hasResizer()) + substractRectOnAreas(note->resizerRect(), m_areas, true); + //} + } + + if (note->isGroup()) { + Note *child = note->firstChild(); + bool first = true; + while (child) { + if ((note->showSubNotes() || first) && note->matching()) + noteIsAfterThis = recomputeAreas(child, noteIsAfterThis); + child = child->next(); + first = false; + } + } + + return noteIsAfterThis; +} + +bool Note::isEditing() +{ + return basket()->editedNote() == this; +} + +void Note::getGradientColors(const QColor &originalBackground, QColor *colorTop, QColor *colorBottom) +{ + bool wasTooDark = Tools::tooDark(originalBackground); + if (wasTooDark) { + *colorTop = originalBackground; + *colorBottom = originalBackground.light(120); + } else { + *colorTop = originalBackground.dark(105); + *colorBottom = originalBackground; + } +} + +/* Drawing policy: + * ============== + * - Draw the note on a pixmap and then draw the pixmap on screen is faster and flicker-free, rather than drawing directly on screen + * - The next time the pixmap can be directly redrawn on screen without (relatively low, for small texts) time-consuming text-drawing + * - To keep memory footprint low, we can destruct the bufferPixmap because redrawing it offscreen and applying it onscreen is nearly as fast as just drawing the pixmap onscreen + * - But as drawing the pixmap offscreen is little time consuming we can keep last visible notes buffered and then the redraw of the entire window is INSTANTANEOUS + * - We keep bufferized note/group draws BUT NOT the resizer: such objects are small and fast to draw, so we don't complexify code for that + */ +void Note::draw(QPainter *painter, const QRect &clipRect) +{ + if (!matching()) + return; + + /** Paint childs: */ + if (isGroup()) { + Note *child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + child->draw(painter, clipRect); + child = child->next(); + first = false; + } + } + + QRect myRect(x(), y(), width(), height()); + /** Paint the resizer if needed: */ + if (hasResizer()) { + int right = rightLimit(); + QRect resizerRect(right, y(), RESIZER_WIDTH, resizerHeight()); + if (resizerRect.intersects(clipRect)) { + // Prepare to draw the resizer: + QPixmap pixmap(RESIZER_WIDTH, resizerHeight()); + QPainter painter2(&pixmap); + // Draw gradient or resizer: + if (m_hovered && m_hoveredZone == Resizer) { + QColor baseColor(basket()->backgroundColor()); + QColor highColor(KGlobalSettings::highlightColor()); + drawResizer(&painter2, 0, 0, RESIZER_WIDTH, resizerHeight(), baseColor, highColor, /*rounded=*/!isColumn()); + if (!isColumn()) { + drawRoundings(&painter2, RESIZER_WIDTH - 3, 0, /*type=*/3); + drawRoundings(&painter2, RESIZER_WIDTH - 3, resizerHeight() - 3, /*type=*/4); + } + } else { + drawInactiveResizer(&painter2, /*x=*/0, /*y=*/0, /*height=*/resizerHeight(), basket()->backgroundColor(), isColumn()); + basket()->blendBackground(painter2, resizerRect); + } + // Draw inserter: + if (basket()->inserterShown() && resizerRect.intersects(basket()->inserterRect())) + basket()->drawInserter(painter2, right, y()); + // Draw selection rect: + if (basket()->isSelecting() && resizerRect.intersects(basket()->selectionRect())) { + QRect selectionRect = basket()->selectionRect(); + selectionRect.moveBy(-right, -y()); + QRect selectionRectInside(selectionRect.x() + 1, selectionRect.y() + 1, selectionRect.width() - 2, selectionRect.height() - 2); + if (selectionRectInside.width() > 0 && selectionRectInside.height() > 0) { + QColor insideColor = basket()->selectionRectInsideColor(); + QColor darkInsideColor(insideColor.dark(105)); + painter2.setClipRect(selectionRectInside); + if (isColumn()) { + int halfWidth = RESIZER_WIDTH / 2; + drawGradient(&painter2, darkInsideColor, insideColor, 0, 0, halfWidth, resizerHeight(), /*sunken=*/false, /*horz=*/false, /*flat=*/false ); + drawGradient(&painter2, insideColor, darkInsideColor, halfWidth, 0, RESIZER_WIDTH-halfWidth, resizerHeight(), /*sunken=*/false, /*horz=*/false, /*flat=*/false ); + } else + drawGradient( &painter2, darkInsideColor, insideColor, 0, 0, RESIZER_WIDTH, resizerHeight(), /*sunken=*/false, /*horz=*/false, /*flat=*/false ); + painter2.setClipping(false); + selectionRectInside.moveBy(right, y()); + basket()->blendBackground(painter2, selectionRectInside, right, y(), false); + } + painter2.setPen(KGlobalSettings::highlightColor().dark()); + painter2.drawRect(selectionRect); + painter2.setPen(Tools::mixColor(KGlobalSettings::highlightColor().dark(), basket()->backgroundColor())); + painter2.drawPoint(selectionRect.topLeft()); + painter2.drawPoint(selectionRect.topRight()); + painter2.drawPoint(selectionRect.bottomLeft()); + painter2.drawPoint(selectionRect.bottomRight()); + } + // Draw on screen: + painter2.end(); + /** Compute visible areas: */ + if ( ! m_computedAreas ) + recomputeAreas(); + if (m_areas.isEmpty()) + return; + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + painter->drawPixmap(rect.x(), rect.y(), pixmap, rect.x() - right, rect.y() - y(), rect.width(), rect.height()); + } + } + } + + /** Then, draw the note/group ONLY if needed: */ + if ( ! myRect.intersects(clipRect) ) + return; + + /** Compute visible areas: */ + if ( ! m_computedAreas ) + recomputeAreas(); + if (m_areas.isEmpty()) + return; + + /** Directly draw pixmap on screen if it is already buffered: */ + if (isBufferized()) { + drawBufferOnScreen(painter, m_bufferedPixmap); + return; + } + + /** Initialise buffer painter: */ + m_bufferedPixmap.resize(width(), height()); + QPainter painter2(&m_bufferedPixmap); + + /** Initialise colors: */ + QColor baseColor(basket()->backgroundColor()); + QColor highColor(KGlobalSettings::highlightColor()); + QColor midColor = Tools::mixColor(baseColor, highColor); + + /** Initialise brushs and pens: */ + QBrush baseBrush(baseColor); + QBrush highBrush(highColor); + QPen basePen(baseColor); + QPen highPen(highColor); + QPen midPen(midColor); + + /** Figure out the state of the note: */ + bool hovered = m_hovered && m_hoveredZone != TopInsert && m_hoveredZone != BottomInsert && m_hoveredZone != Resizer; + + /** And then draw the group: */ + if (isGroup()) { + // Draw background or handle: + if (hovered) { + drawHandle(&painter2, 0, 0, width(), height(), baseColor, highColor); + drawRoundings(&painter2, 0, 0, /*type=*/1); + drawRoundings(&painter2, 0, height() - 3, /*type=*/2); + } else { + painter2.fillRect(0, 0, width(), height(), baseBrush); + basket()->blendBackground(painter2, myRect, -1, -1, /*opaque=*/true); + } + + // Draw expander: + int yExp = yExpander(); + drawExpander(&painter2, NOTE_MARGIN, yExp, (hovered ? expanderBackground(height(), yExp+EXPANDER_HEIGHT/2, highColor) : baseColor), m_isFolded, basket()); + // Draw expander rounded edges: + if (hovered) { + QColor color1 = expanderBackground(height(), yExp, highColor); + QColor color2 = expanderBackground(height(), yExp + EXPANDER_HEIGHT - 1, highColor); + painter2.setPen(color1); + painter2.drawPoint(NOTE_MARGIN, yExp); + painter2.drawPoint(NOTE_MARGIN + 9 - 1, yExp); + painter2.setPen(color2); + painter2.drawPoint(NOTE_MARGIN, yExp + 9 - 1); + painter2.drawPoint(NOTE_MARGIN + 9 - 1, yExp + 9 - 1); + } else + drawRoundings(&painter2, NOTE_MARGIN, yExp, /*type=*/5, 9, 9); + + // Draw on screen: + painter2.end(); + drawBufferOnScreen(painter, m_bufferedPixmap); + return; + } + + /** Or draw the note: */ + // What are the background colors: + QColor background = basket()->backgroundColor(); + if (isSelected()) + if (m_computedState.backgroundColor().isValid()) + background = Tools::mixColor(Tools::mixColor(m_computedState.backgroundColor(), KGlobalSettings::highlightColor()), KGlobalSettings::highlightColor()); + else + background = KGlobalSettings::highlightColor(); + else if (m_computedState.backgroundColor().isValid()) + background = m_computedState.backgroundColor(); + QColor bgColor; + QColor darkBgColor; + getGradientColors(background, &darkBgColor, &bgColor); + // Draw background (color, gradient and pixmap): + drawGradient( &painter2, bgColor, darkBgColor, 0, 0, width(), height(), /*sunken=*/!hovered, /*horz=*/true, /*flat=*/false ); + if (!hovered) { + painter2.setPen(Tools::mixColor(bgColor, darkBgColor)); + painter2.drawLine(0, height() - 1, width(), height() - 1); + } + basket()->blendBackground(painter2, myRect); + + if (hovered) { + // Top/Bottom lines: + painter2.setPen(highPen); + painter2.drawLine(0, height() - 1, width(), height() - 1); + painter2.drawLine(0, 0, width(), 0); + // The handle: + drawHandle(&painter2, 0, 0, HANDLE_WIDTH, height(), baseColor, highColor); + drawRoundings(&painter2, 0, 0, /*type=*/1); + drawRoundings(&painter2, 0, height() - 3, /*type=*/2); + // Round handle-right-side border: + painter2.setPen(highPen); + painter2.drawPoint(HANDLE_WIDTH, 1); + painter2.drawPoint(HANDLE_WIDTH, height() - 2); + // Light handle top-right round corner: + painter2.setPen(QPen(highColor.light(150))); + painter2.drawPoint(HANDLE_WIDTH - 1, 1); + // Handle anti-aliased rounded handle-right-side corners: + QColor insideMidColor = Tools::mixColor(bgColor, highColor); + painter2.setPen(insideMidColor); + // Left inside round corners: + painter2.drawPoint(HANDLE_WIDTH + 1, 1); + painter2.drawPoint(HANDLE_WIDTH, 2); + painter2.drawPoint(HANDLE_WIDTH + 1, height() - 2); + painter2.drawPoint(HANDLE_WIDTH, height() - 3); + // Right inside round corners: + painter2.drawPoint(width() - 4, 1); + painter2.drawPoint(width() - 3, 2); + painter2.drawPoint(width() - 4, height() - 2); + painter2.drawPoint(width() - 3, height() - 3); + // Right rounded edge: + painter2.setPen(highPen); + painter2.fillRect(width() - 2, 0, 2, height(), highBrush); + painter2.drawPoint(width() - 3, 1); + painter2.drawPoint(width() - 3, height() - 2); + // Right anti-aliased rounded edge: + painter2.setPen(midPen); + painter2.drawPoint(width() - 1, 0); + painter2.drawPoint(width() - 1, height() - 1); + // Blend background pixmap: + drawRoundings(&painter2, 0, 0, /*type=*/6, width(), height()); + } + + if (isFocused()) { + QRect focusRect(HANDLE_WIDTH, NOTE_MARGIN - 1, width() - HANDLE_WIDTH - 2, height() - 2*NOTE_MARGIN + 2); + painter2.drawWinFocusRect(focusRect); + } + + // Draw the Emblems: + int yIcon = (height() - EMBLEM_SIZE) / 2; + int xIcon = HANDLE_WIDTH + NOTE_MARGIN; + for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { + if (!(*it)->emblem().isEmpty()) { + QPixmap stateEmblem = kapp->iconLoader()->loadIcon((*it)->emblem(), KIcon::NoGroup, 16, KIcon::DefaultState, 0L, false); + painter2.drawPixmap(xIcon, yIcon, stateEmblem); + xIcon += NOTE_MARGIN + EMBLEM_SIZE; + } + } + + // Determine the colors (for the richText drawing) and the text color (for the tags arrow too): + QColorGroup cg(basket()->colorGroup()); + cg.setColor(QColorGroup::Text, (m_computedState.textColor().isValid() ? m_computedState.textColor() : basket()->textColor()) ); + cg.setColor(QColorGroup::Background, bgColor); + if (isSelected()) + cg.setColor(QColorGroup::Text, KGlobalSettings::highlightedTextColor()); + + // Draw the Tags Arrow: + if (hovered) { + QColor textColor = cg.color(QColorGroup::Text); + QColor light = Tools::mixColor(textColor, bgColor); + QColor mid = Tools::mixColor(textColor, light); + painter2.setPen(light);//QPen(basket()->colorGroup().dark().light(150))); + painter2.drawLine(xIcon, yIcon + 6, xIcon + 4, yIcon + 6); + painter2.setPen(mid);//QPen(basket()->colorGroup().dark())); + painter2.drawLine(xIcon + 1, yIcon + 7, xIcon + 3, yIcon + 7); + painter2.setPen(textColor);//QPen(basket()->colorGroup().foreground())); + painter2.drawPoint(xIcon + 2, yIcon + 8); + } else if (m_haveInvisibleTags) { + painter2.setPen(cg.color(QColorGroup::Text)/*QPen(basket()->colorGroup().foreground())*/); + painter2.drawPoint(xIcon, yIcon + 7); + painter2.drawPoint(xIcon + 2, yIcon + 7); + painter2.drawPoint(xIcon + 4, yIcon + 7); + } + + // Draw content: + // Optimization: do not draw text notes because it is time consuming and should be done nearly at each tetx modification. + if (basket()->editedNote() != this || basket()->editedNote()->content()->type() != NoteType::Html) { + painter2.translate(contentX(), NOTE_MARGIN); + painter2.setFont( m_computedState.font(painter2.font()) ); + m_content->paint(&painter2, width() - contentX() - NOTE_MARGIN, height() - 2*NOTE_MARGIN, cg, !m_computedState.textColor().isValid(), isSelected(), hovered); + } + + // Draw on screen: + painter2.end(); + drawBufferOnScreen(painter, m_bufferedPixmap); +} + +void Note::drawBufferOnScreen(QPainter *painter, const QPixmap &contentPixmap) +{ + for (QValueList<QRect>::iterator it = m_areas.begin(); it != m_areas.end(); ++it) { + QRect &rect = *it; + if (rect.x() >= x() + width()) // It's a rect of the resizer, don't draw it! + continue; + // If the inserter is above the note, draw it, BUT NOT in the buffer pixmap, + // we copy the rectangle in a new pixmap, apply the inserter and then draw this new pixmap on screen: + if ( (basket()->inserterShown() && rect.intersects(basket()->inserterRect())) || + (basket()->isSelecting() && rect.intersects(basket()->selectionRect())) ) { + QPixmap pixmap3(rect.width(), rect.height()); + QPainter painter3(&pixmap3); + painter3.drawPixmap(0, 0, contentPixmap, rect.x() - x(), rect.y() - y(), rect.width(), rect.height()); + // Draw inserter: + if (basket()->inserterShown() && rect.intersects(basket()->inserterRect())) + basket()->drawInserter(painter3, rect.x(), rect.y()); + // Draw selection rect: + if (basket()->isSelecting() && rect.intersects(basket()->selectionRect())) { + QRect selectionRect = basket()->selectionRect(); + selectionRect.moveBy(-rect.x(), -rect.y()); + + QRect selectionRectInside(selectionRect.x() + 1, selectionRect.y() + 1, selectionRect.width() - 2, selectionRect.height() - 2); + if (selectionRectInside.width() > 0 && selectionRectInside.height() > 0) { + bufferizeSelectionPixmap(); + selectionRectInside.moveBy(rect.x(), rect.y()); + QRect rectToPaint = rect.intersect(selectionRectInside); + rectToPaint.moveBy(-x(), -y()); + painter3.drawPixmap(rectToPaint.topLeft() + QPoint(x(), y()) - rect.topLeft(), m_bufferedSelectionPixmap, rectToPaint); + //blendBackground(painter2, selectionRectInside, rect.x(), rect.y(), true, &m_selectedBackgroundPixmap); + } + + painter3.setPen(KGlobalSettings::highlightColor().dark()); + painter3.drawRect(selectionRect); + if (isGroup()) + painter3.setPen(Tools::mixColor(KGlobalSettings::highlightColor().dark(), basket()->backgroundColor())); + else { + // What are the background colors: + QColor bgColor = basket()->backgroundColor(); + if (isSelected()) + bgColor = (m_computedState.backgroundColor().isValid() ? Tools::mixColor(Tools::mixColor(m_computedState.backgroundColor(), KGlobalSettings::highlightColor()), KGlobalSettings::highlightColor()) : KGlobalSettings::highlightColor()); + else if (m_computedState.backgroundColor().isValid()) + bgColor = m_computedState.backgroundColor(); + painter3.setPen(Tools::mixColor(KGlobalSettings::highlightColor().dark(), bgColor)); + } + painter3.drawPoint(selectionRect.topLeft()); + painter3.drawPoint(selectionRect.topRight()); + painter3.drawPoint(selectionRect.bottomLeft()); + painter3.drawPoint(selectionRect.bottomRight()); + } + painter3.end(); + painter->drawPixmap(rect.x(), rect.y(), pixmap3); + // Else, draw the rect pixmap directly on screen: + } else + painter->drawPixmap(rect.x(), rect.y(), contentPixmap, rect.x() - x(), rect.y() - y(), rect.width(), rect.height()); + } +} + +void Note::setContent(NoteContent *content) +{ + m_content = content; +} + +/*const */State::List& Note::states() const +{ + return (State::List&)m_states; +} + +void Note::addState(State *state, bool orReplace) +{ + if (!content()) + return; + + Tag *tag = state->parentTag(); + State::List::iterator itStates = m_states.begin(); + // Browse all tags, see if the note has it, increment itSates if yes, and then insert the state at this position... + // For each existing tags: + for (Tag::List::iterator it = Tag::all.begin(); it != Tag::all.end(); ++it) { + // If the current tag isn't the one to assign or the current one on the note, go to the next tag: + if (*it != tag && itStates != m_states.end() && *it != (*itStates)->parentTag()) + continue; + // We found the tag to insert: + if (*it == tag) { + // And the note already have the tag: + if (itStates != m_states.end() && *it == (*itStates)->parentTag()) { + // We replace the state if wanted: + if (orReplace) { + itStates = m_states.insert(itStates, state); + ++itStates; + m_states.remove(itStates); + recomputeStyle(); + } + } else { + m_states.insert(itStates, state); + recomputeStyle(); + } + return; + } + // The note has this tag: + if (itStates != m_states.end() && *it == (*itStates)->parentTag()) + ++itStates; + } +} + +QFont Note::font() +{ + return m_computedState.font( basket()->QScrollView::font() ); +} + +QColor Note::backgroundColor() +{ + if (m_computedState.backgroundColor().isValid()) + return m_computedState.backgroundColor(); + else + return basket()->backgroundColor(); +} + +QColor Note::textColor() +{ + if (m_computedState.textColor().isValid()) + return m_computedState.textColor(); + else + return basket()->textColor(); +} + +void Note::recomputeStyle() +{ + State::merge(m_states, &m_computedState, &m_emblemsCount, &m_haveInvisibleTags, basket()->backgroundColor()); +// unsetWidth(); + if (content()) + content()->fontChanged(); +// requestRelayout(); // TODO! +} + +void Note::recomputeAllStyles() +{ + if (content()) // We do the merge ourself, without calling recomputeStyle(), so there is no infinite recursion: + //State::merge(m_states, &m_computedState, &m_emblemsCount, &m_haveInvisibleTags, basket()->backgroundColor()); + recomputeStyle(); + else if (isGroup()) + FOR_EACH_CHILD (child) + child->recomputeAllStyles(); +} + +bool Note::removedStates(const QValueList<State*> &deletedStates) +{ + bool modifiedBasket = false; + + if (!states().isEmpty()) { + for (QValueList<State*>::const_iterator it = deletedStates.begin(); it != deletedStates.end(); ++it) + if (hasState(*it)) { + removeState(*it); + modifiedBasket = true; + } + } + + FOR_EACH_CHILD (child) + if (child->removedStates(deletedStates)) + modifiedBasket = true; + + return modifiedBasket; +} + + +void Note::addTag(Tag *tag) +{ + addState(tag->states().first(), /*but do not replace:*/false); +} + +void Note::removeState(State *state) +{ + for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) + if (*it == state) { + m_states.remove(it); + recomputeStyle(); + return; + } +} + +void Note::removeTag(Tag *tag) +{ + for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) + if ((*it)->parentTag() == tag) { + m_states.remove(it); + recomputeStyle(); + return; + } +} + +void Note::removeAllTags() +{ + m_states.clear(); + recomputeStyle(); +} + +void Note::addTagToSelectedNotes(Tag *tag) +{ + if (content() && isSelected()) + addTag(tag); + + FOR_EACH_CHILD (child) + child->addTagToSelectedNotes(tag); +} + +void Note::removeTagFromSelectedNotes(Tag *tag) +{ + if (content() && isSelected()) { + if (hasTag(tag)) + setWidth(0); + removeTag(tag); + } + + FOR_EACH_CHILD (child) + child->removeTagFromSelectedNotes(tag); +} + +void Note::removeAllTagsFromSelectedNotes() +{ + if (content() && isSelected()) { + if (m_states.count() > 0) + setWidth(0); + removeAllTags(); + } + + FOR_EACH_CHILD (child) + child->removeAllTagsFromSelectedNotes(); +} + +void Note::addStateToSelectedNotes(State *state, bool orReplace) +{ + if (content() && isSelected()) + addState(state, orReplace); + + FOR_EACH_CHILD (child) + child->addStateToSelectedNotes(state, orReplace); // TODO: Basket::addStateToSelectedNotes() does not have orReplace +} + +void Note::changeStateOfSelectedNotes(State *state) +{ + if (content() && isSelected() && hasTag(state->parentTag())) + addState(state); + + FOR_EACH_CHILD (child) + child->changeStateOfSelectedNotes(state); +} + +bool Note::selectedNotesHaveTags() +{ + if (content() && isSelected() && m_states.count() > 0) + return true; + + FOR_EACH_CHILD (child) + if (child->selectedNotesHaveTags()) + return true; + return false; +} + +bool Note::hasState(State *state) +{ + for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) + if (*it == state) + return true; + return false; +} + +bool Note::hasTag(Tag *tag) +{ + for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) + if ((*it)->parentTag() == tag) + return true; + return false; +} + +State* Note::stateOfTag(Tag *tag) +{ + for (State::List::iterator it = m_states.begin(); it != m_states.end(); ++it) + if ((*it)->parentTag() == tag) + return *it; + return 0; +} + +State* Note::stateForEmblemNumber(int number) +{ + int i = -1; + for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) + if (!(*it)->emblem().isEmpty()) { + ++i; + if (i == number) + return *it; + } + return 0; +} + +bool Note::stateForTagFromSelectedNotes(Tag *tag, State **state) +{ + if (content() && isSelected()) { + // What state is the tag on this note? + State* stateOfTag = this->stateOfTag(tag); + // This tag is not assigned to this note, the action will assign it, then: + if (stateOfTag == 0) + *state = 0; + else { + // Take the LOWEST state of all the selected notes: + // Say the two selected notes have the state "Done" and "To Do". + // The first note set *state to "Done". + // When reaching the second note, we should recognize "To Do" is first in the tag states, then take it + // Because pressing the tag shortcut key should first change state before removing the tag! + if (*state == 0) + *state = stateOfTag; + else { + bool stateIsFirst = true; + for (State *nextState = stateOfTag->nextState(); nextState; nextState = nextState->nextState(/*cycle=*/false)) + if (nextState == *state) + stateIsFirst = false; + if (!stateIsFirst) + *state = stateOfTag; + } + } + return true; // We encountered a selected note + } + + bool encounteredSelectedNote = false; + FOR_EACH_CHILD (child) { + bool encountered = child->stateForTagFromSelectedNotes(tag, state); + if (encountered && *state == 0) + return true; + if (encountered) + encounteredSelectedNote = true; + } + return encounteredSelectedNote; +} + + +void Note::inheritTagsOf(Note *note) +{ + if (!note || !content()) + return; + + for (State::List::iterator it = note->states().begin(); it != note->states().end(); ++it) + if ((*it)->parentTag() && (*it)->parentTag()->inheritedBySiblings()) + addTag((*it)->parentTag()); +} + +void Note::unbufferizeAll() +{ + unbufferize(); + + if (isGroup()) { + Note *child = firstChild(); + while (child) { + child->unbufferizeAll(); + child = child->next(); + } + } +} + +void Note::bufferizeSelectionPixmap() +{ + if (m_bufferedSelectionPixmap.isNull()) { + QColor insideColor = KGlobalSettings::highlightColor(); + KPixmap kpixmap(m_bufferedPixmap); + m_bufferedSelectionPixmap = KPixmapEffect::fade(kpixmap, 0.25, insideColor); + } +} + +QRect Note::visibleRect() +{ + QValueList<QRect> areas; + areas.append(rect()); + + // When we are folding a parent group, if this note is bigger than the first real note of the group, cut the top of this: + Note *parent = parentNote(); + while (parent) { + if (parent->expandingOrCollapsing()) + substractRectOnAreas(QRect(x(), parent->y() - height(), width(), height()), areas, true); + parent = parent->parentNote(); + } + + if (areas.count() > 0) + return areas[0]; + else + return QRect(); +} + +void Note::recomputeBlankRects(QValueList<QRect> &blankAreas) +{ + if (!matching()) + return; + + // visibleRect() instead of rect() because if we are folding/expanding a smaller parent group, then some part is hidden! + // But anyway, a resizer is always a primary note and is never hidden by a parent group, so no visibleResizerRect() method! + substractRectOnAreas(visibleRect(), blankAreas, true); + if (hasResizer()) + substractRectOnAreas(resizerRect(), blankAreas, true); + + if (isGroup()) { + Note *child = firstChild(); + bool first = true; + while (child) { + if ((showSubNotes() || first) && child->matching()) + child->recomputeBlankRects(blankAreas); + child = child->next(); + first = false; + } + } +} + +void Note::linkLookChanged() +{ + if (isGroup()) { + Note *child = firstChild(); + while (child) { + child->linkLookChanged(); + child = child->next(); + } + } else + content()->linkLookChanged(); +} + +Note* Note::noteForFullPath(const QString &path) +{ + if (content() && fullPath() == path) + return this; + + Note *child = firstChild(); + Note *found; + while (child) { + found = child->noteForFullPath(path); + if (found) + return found; + child = child->next(); + } + return 0; +} + +void Note::listUsedTags(QValueList<Tag*> &list) +{ + for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) { + Tag *tag = (*it)->parentTag(); + if (!list.contains(tag)) + list.append(tag); + } + + FOR_EACH_CHILD (child) + child->listUsedTags(list); +} + + +void Note::usedStates(QValueList<State*> &states) +{ + if (content()) + for (State::List::Iterator it = m_states.begin(); it != m_states.end(); ++it) + if (!states.contains(*it)) + states.append(*it); + + FOR_EACH_CHILD (child) + child->usedStates(states); +} + +Note* Note::nextInStack() +{ + // First, search in the childs: + if (firstChild()) + if (firstChild()->content()) + return firstChild(); + else + return firstChild()->nextInStack(); + + // Then, in the next: + if (next()) + if (next()->content()) + return next(); + else + return next()->nextInStack(); + + // And finally, in the parent: + Note *note = parentNote(); + while (note) + if (note->next()) + if (note->next()->content()) + return note->next(); + else + return note->next()->nextInStack(); + else + note = note->parentNote(); + + // Not found: + return 0; +} + +Note* Note::prevInStack() +{ + // First, search in the previous: + if (prev() && prev()->content()) + return prev(); + + // Else, it's a group, get the last item in that group: + if (prev()) { + Note *note = prev()->lastRealChild(); + if (note) + return note; + } + + if (parentNote()) + return parentNote()->prevInStack(); + else + return 0; +} + +Note* Note::nextShownInStack() +{ + Note *next = nextInStack(); + while (next && !next->isShown()) + next = next->nextInStack(); + return next; +} + +Note* Note::prevShownInStack() +{ + Note *prev = prevInStack(); + while (prev && !prev->isShown()) + prev = prev->prevInStack(); + return prev; +} + +bool Note::isShown() +{ + // First, the easy one: groups are always shown: + if (isGroup()) + return true; + + // And another easy part: non-matching notes are hidden: + if (!matching()) + return false; + + if (basket()->isFiltering()) // And isMatching() because of the line above! + return true; + + // So, here we go to the complexe case: if the note is inside a collapsed group: + Note *group = parentNote(); + Note *child = this; + while (group) { + if (group->isFolded() && group->firstChild() != child) + return false; + child = group; + group = group->parentNote(); + } + return true; +} + +void Note::debug() +{ + std::cout << "Note@" << (Q_UINT64)this; + if (!this) { + std::cout << std::endl; + return; + } + + if (isColumn()) + std::cout << ": Column"; + else if (isGroup()) + std::cout << ": Group"; + else + std::cout << ": Content[" << content()->lowerTypeName() << "]: " << toText(""); + std::cout << std::endl; +} + +Note* Note::firstSelected() +{ + if (isSelected()) + return this; + + Note *first = 0; + FOR_EACH_CHILD (child) { + first = child->firstSelected(); + if (first) + break; + } + return first; +} + +Note* Note::lastSelected() +{ + if (isSelected()) + return this; + + Note *last = 0, *tmp = 0; + FOR_EACH_CHILD (child) { + tmp = child->lastSelected(); + if (tmp) + last = tmp; + } + return last; +} + +Note* Note::selectedGroup() +{ + if (isGroup() && allSelected() && count() == basket()->countSelecteds()) + return this; + + FOR_EACH_CHILD (child) { + Note *selectedGroup = child->selectedGroup(); + if (selectedGroup) + return selectedGroup; + } + + return 0; +} + +void Note::groupIn(Note *group) +{ + if (this == group) + return; + + if (allSelected() && !isColumn()) { + basket()->unplugNote(this); + basket()->insertNote(this, group, Note::BottomColumn, QPoint(), /*animateNewPosition=*/true); + } else { + Note *next; + Note *child = firstChild(); + while (child) { + next = child->next(); + child->groupIn(group); + child = next; + } + } +} + +bool Note::tryExpandParent() +{ + Note *parent = parentNote(); + Note *child = this; + while (parent) { + if (parent->firstChild() != child) + return false; + if (parent->isColumn()) + return false; + if (parent->isFolded()) { + parent->toggleFolded(true); + basket()->relayoutNotes(true); + return true; + } + child = parent; + parent = parent->parentNote(); + } + return false; +} + +bool Note::tryFoldParent() // TODO: withCtrl ? withShift ? +{ + Note *parent = parentNote(); + Note *child = this; + while (parent) { + if (parent->firstChild() != child) + return false; + if (parent->isColumn()) + return false; + if (!parent->isFolded()) { + parent->toggleFolded(true); + basket()->relayoutNotes(true); + return true; + } + child = parent; + parent = parent->parentNote(); + } + return false; +} + + +int Note::distanceOnLeftRight(Note *note, int side) +{ + if (side == Basket::RIGHT_SIDE) { + // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: + if (finalX() > note->finalX() || finalRightLimit() > note->finalRightLimit()) + return -1; + } else /*LEFT_SIDE:*/ { + // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: + if (finalX() < note->finalX() || finalRightLimit() < note->finalRightLimit()) + return -1; + } + if (finalX() == note->finalX() && finalRightLimit() == note->finalRightLimit()) + return -1; + + float thisCenterX = finalX() + (side == Basket::LEFT_SIDE ? width() : /*RIGHT_SIDE:*/ 0); + float thisCenterY = finalY() + finalHeight() / 2; + float noteCenterX = note->finalX() + note->width() / 2; + float noteCenterY = note->finalY() + note->finalHeight() / 2; + + if (thisCenterY > note->finalBottom()) + noteCenterY = note->finalBottom(); + else if (thisCenterY < note->finalY()) + noteCenterY = note->finalY(); + else + noteCenterY = thisCenterY; + + float angle = 0; + if (noteCenterX - thisCenterX != 0) + angle = 1000 * ((noteCenterY - thisCenterY) / (noteCenterX - thisCenterX)); + if (angle < 0) + angle = -angle; + + return int(sqrt(pow(noteCenterX - thisCenterX, 2) + pow(noteCenterY - thisCenterY, 2)) + angle); +} + +int Note::distanceOnTopBottom(Note *note, int side) +{ + if (side == Basket::BOTTOM_SIDE) { + // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: + if (finalY() > note->finalY() || finalBottom() > note->finalBottom()) + return -1; + } else /*TOP_SIDE:*/ { + // If 'note' is on left of 'this': cannot switch from this to note by pressing Right key: + if (finalY() < note->finalY() || finalBottom() < note->finalBottom()) + return -1; + } + if (finalY() == note->finalY() && finalBottom() == note->finalBottom()) + return -1; + + float thisCenterX = finalX() + width() / 2; + float thisCenterY = finalY() + (side == Basket::TOP_SIDE ? finalHeight() : /*BOTTOM_SIDE:*/ 0); + float noteCenterX = note->finalX() + note->width() / 2; + float noteCenterY = note->finalY() + note->finalHeight() / 2; + + if (thisCenterX > note->finalRightLimit()) + noteCenterX = note->finalRightLimit(); + else if (thisCenterX < note->finalX()) + noteCenterX = note->finalX(); + else + noteCenterX = thisCenterX; + + float angle = 0; + if (noteCenterX - thisCenterX != 0) + angle = 1000 * ((noteCenterY - thisCenterY) / (noteCenterX - thisCenterX)); + if (angle < 0) + angle = -angle; + + return int(sqrt(pow(noteCenterX - thisCenterX, 2) + pow(noteCenterY - thisCenterY, 2)) + angle); +} + +Note* Note::parentPrimaryNote() +{ + Note *primary = this; + while (primary->parentNote()) + primary = primary->parentNote(); + return primary; +} + +void Note::deleteChilds() +{ + Note *child = firstChild(); + + while (child) + { + Note *tmp = child->next(); + delete child; + child = tmp; + } +} + +bool Note::saveAgain() +{ + if(content()) + { + if(!content()->saveToFile()) + return false; + } + FOR_EACH_CHILD (child) + { + if(!child->saveAgain()) + return false; + } + return true; +} + +bool Note::convertTexts() +{ + bool convertedNotes = false; + + if (content() && content()->lowerTypeName() == "text") { + QString text = ((TextContent*)content())->text(); + QString html = "<html><head><meta name=\"qrichtext\" content=\"1\" /></head><body>" + Tools::textToHTMLWithoutP(text) + "</body></html>"; + basket()->saveToFile(fullPath(), html, /*isLocalEncoding=*/true); + setContent( new HtmlContent(this, content()->fileName()) ); + convertedNotes = true; + } + + FOR_EACH_CHILD (child) + if (child->convertTexts()) + convertedNotes = true; + + return convertedNotes; +} + +#if 0 + +/** Note */ + +QString Note::toHtml(const QString &imageName) +{ + switch (m_type) { + case Text: + /*return "<font color=" + backgroundColor().name() + + font().name() + font + ">" + + Tools::textToHTMLWithoutP(text()) + "</font>";*/ + return Tools::textToHTMLWithoutP(text()); + case Html: + return Tools::htmlToParagraph(html()); + case Image: + case Animation: + { + if ( (m_type == Image && pixmap() == 0L) || + (m_type == Animation && movie() == 0L) ) { + QMimeSourceFactory::defaultFactory()->setData(imageName, 0L); + return i18n("(Image)"); // Image or animation not yet loaded!! + } + + QImage image; + if (m_type == Image) + image = pixmap()->convertToImage(); + else + image = movie()->framePixmap().convertToImage(); + image = image.smoothScale(200, 150, QImage::ScaleMin); + QPixmap pixmap = QPixmap(image); + QMimeSourceFactory::defaultFactory()->setPixmap(imageName, pixmap); + return "<img src=" + imageName + ">"; /// + +/* // FIXME: movie isn't loaded yet: CRASH! + return i18n("(Image)"); + // Not executed, because don't work: + QImage image; + if (m_type == Image) + image = pixmap()->convertToImage(); + else + image = movie()->framePixmap().convertToImage(); + image = image.smoothScale(200, 150, QImage::ScaleMin); + QPixmap pixmap = QPixmap(image); + QMimeSourceFactory::defaultFactory()->setPixmap(imageName, pixmap); + return "<img src=" + imageName + ">"; /// + */ //TODO?: QMimeSourceFactory::defaultFactory()->setData(imageName, 0L); + } + case Sound: + case File: + { + /// FIXME: Since fullPath() doesn't exist yet, the icon rely on the extension. + /// Bad if there isn't one or if it's a wrong one. + /*QPixmap icon = DesktopIcon( + NoteFactory::iconForURL(fullPath()), + (m_type == Sound ? LinkLook::soundLook : LinkLook::fileLook)->iconSize()); + QMimeSourceFactory::defaultFactory()->setPixmap(imageName, icon); + return "<img src=" + imageName + "> " + fileName(); */ /// + return m_linkLabel->toHtml(imageName); + } + case Link: + { + QString link = m_linkLabel->toHtml(imageName); + if (!autoTitle() && title() != NoteFactory::titleForURL(url().prettyURL())) + link += "<br><i>" + url().prettyURL() + "</i>"; /// + return link; + } + case Launcher: + { + return m_linkLabel->toHtml(imageName); + //KService service(fullPath()); // service.icon() + //return service.name() + "<br><i>" + service.exec() + "</i>"; /// + } + case Color: + return "<b><font color=" + color().name() + ">" + color().name() + "</font></b>"; + case Unknown: + return text(); + } + return QString(); +} + +#endif // #if 0 diff --git a/src/note.h b/src/note.h new file mode 100644 index 0000000..cb249ec --- /dev/null +++ b/src/note.h @@ -0,0 +1,388 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef NOTE_H +#define NOTE_H + +#include <qstring.h> +#include <qpixmap.h> +#include <kpixmap.h> +#include <qdatetime.h> + +#include "notecontent.h" +#include "tag.h" + +class Basket; +class FilterData; +class HtmlExportData; + +class NoteSelection; + +class QPainter; +class QSimpleRichText; + +/** Handle basket notes and groups!\n + * After creation, the note is a group. You should create a NoteContent with this Note + * as constructor parameter to transform it to a note with content. eg: + * @code + * Note *note = new Note(basket); // note is a group! + * new TextContent(note, fileName); // note is now a note with a text content! + * new ColorContent(note, Qt::red); // Should never be done!!!!! the old Content should be deleted... + * @endcode + * @author S�astien Laot + */ +class Note +{ +/// CONSTRUCTOR AND DESTRUCTOR: + public: + Note(Basket *parent); + ~Note(); + +/// DOUBLY LINKED LIST: + private: + Note *m_prev; + Note *m_next; + public: + inline void setNext(Note *next) { m_next = next; } + inline void setPrev(Note *prev) { m_prev = prev; } + inline Note* next() { return m_next; } + inline Note* prev() { return m_prev; } + +/// GEOMETRY MANAGEMENT: + private: + int m_x; + int m_y; + int m_width; + int m_height; +// int m_minContentWidth; + public: + void setWidth(int width); + void setWidthForceRelayout(int width); + void setInitialHeight(int height) { m_height = height; } /// << Do NEVER use it unless you know what you do! + void setX(int x); + void setY(int y); + void setXRecursivly(int x); + void setYRecursivly(int y); + inline int x() { return m_x; } + inline int y() { return m_y; } + inline int width() { return (isGroup() ? (isColumn() ? 0 : GROUP_WIDTH) : m_width); } + inline int height() { return m_height; } + inline int bottom() { return m_y + m_height - 1; } + QRect rect(); + QRect resizerRect(); + QRect visibleRect(); + void relayoutAt(int x, int y, bool animate); + int contentX(); + int minWidth(); + int minRight(); + void unsetWidth(); + void requestRelayout(); + void setHeight(int height) { m_height = height; } /// << DO NEVER USE IT!!! Only available when moving notes, groups should be recreated with the exact same state as before! + +/// FREE AND COLUMN LAYOUTS MANAGEMENT: + private: + int m_groupWidth; + public: + int groupWidth(); + void setGroupWidth(int width); + int rightLimit(); + int finalRightLimit(); + bool isFree(); + bool isColumn(); + bool hasResizer(); + int resizerHeight(); + +/// GROUPS MANAGEMENT: + private: + bool m_isFolded; + Note *m_firstChild; + Note *m_parentNote; + public: + inline bool isGroup() { return m_content == 0L; } + inline bool isFolded() { return m_isFolded; } + inline Note* firstChild() { return m_firstChild; } + inline Note* parentNote() { return m_parentNote; } + /*inline*/ bool showSubNotes();// { return !m_isFolded || !m_collapseFinished; } + inline void setParentNote(Note *note) { m_parentNote = note; } + inline void setFirstChild(Note *note) { m_firstChild = note; } + bool isShown(); + void toggleFolded(bool animate); + Note* noteAt(int x, int y); + Note* firstRealChild(); + Note* lastRealChild(); + Note* lastChild(); + Note* lastSibling(); + int yExpander(); + bool isAfter(Note *note); + bool contains(Note *note); + +/// NOTES VARIOUS PROPERTIES: // CONTENT MANAGEMENT? + private: + Basket *m_basket; + NoteContent *m_content; + QDateTime m_addedDate; + QDateTime m_lastModificationDate; + public: + inline Basket* basket() { return m_basket; } + inline NoteContent* content() { return m_content; } + inline void setAddedDate(const QDateTime &dateTime) { m_addedDate = dateTime; } + inline void setLastModificationDate(const QDateTime &dateTime) { m_lastModificationDate = dateTime; } + inline void setParentBasket(Basket *basket) { m_basket = basket; } + QDateTime addedDate() { return m_addedDate; } + QDateTime lastModificationDate() { return m_lastModificationDate; } + QString addedStringDate(); + QString lastModificationStringDate(); + QString toText(const QString &cuttedFullPath); + bool saveAgain(); + void deleteChilds(); + + protected: + void setContent(NoteContent *content); + friend class NoteContent; + friend class AnimationContent; + +/// DRAWING: + private: + QPixmap m_bufferedPixmap; + KPixmap m_bufferedSelectionPixmap; + public: + void draw(QPainter *painter, const QRect &clipRect); + void drawBufferOnScreen(QPainter *painter, const QPixmap &contentPixmap); + static void getGradientColors(const QColor &originalBackground, QColor *colorTop, QColor *colorBottom); + static void drawExpander(QPainter *painter, int x, int y, const QColor &background, bool expand, Basket *basket); + void drawHandle( QPainter *painter, int x, int y, int width, int height, const QColor &background, const QColor &foreground); + void drawResizer( QPainter *painter, int x, int y, int width, int height, const QColor &background, const QColor &foreground, bool rounded); + void drawRoundings(QPainter *painter, int x, int y, int type, int width = 0, int height = 0); + void unbufferizeAll(); + void bufferizeSelectionPixmap(); + inline void unbufferize() { m_bufferedPixmap.resize(0, 0); m_bufferedSelectionPixmap.resize(0, 0); } + inline bool isBufferized() { return !m_bufferedPixmap.isNull(); } + void recomputeBlankRects(QValueList<QRect> &blankAreas); + static void drawInactiveResizer(QPainter *painter, int x, int y, int height, const QColor &background, bool column); + +/// VISIBLE AREAS COMPUTATION: + private: + QValueList<QRect> m_areas; + bool m_computedAreas; + bool m_onTop; + void recomputeAreas(); + bool recomputeAreas(Note *note, bool noteIsAfterThis); + public: + void setOnTop(bool onTop); + inline bool isOnTop() { return m_onTop; } + bool isEditing(); + +/// MANAGE ANIMATION: + private: + int m_deltaX; + int m_deltaY; + int m_deltaHeight; + bool m_collapseFinished; + bool m_expandingFinished; + public: + inline int deltaX() { return m_deltaX; } + inline int deltaY() { return m_deltaY; } + inline int deltaHeight() { return m_deltaHeight; } + inline int finalX() { return m_x + m_deltaX; } + inline int finalY() { return m_y + m_deltaY; } + inline int finalHeight() { return m_height + m_deltaHeight; } + inline int finalBottom() { return finalY() + finalHeight() - 1; } + inline void cancelAnimation() { m_deltaX = 0; m_deltaY = 0; m_deltaHeight = 0; } + inline bool expandingOrCollapsing() { return !m_collapseFinished || !m_expandingFinished; } + void addAnimation(int deltaX, int deltaY, int deltaHeight = 0); + void setFinalPosition(int x, int y); /// << Convenient method for addAnimation() + bool advance(); + void initAnimationLoad(); + void setRecursivelyUnder(Note *under, bool animate); + +/// USER INTERACTION: + public: + enum Zone { None = 0, + Handle, TagsArrow, Custom0, /*CustomContent1, CustomContent2, */Content, Link, + TopInsert, TopGroup, BottomInsert, BottomGroup, BottomColumn, + Resizer, + Group, GroupExpander, + Emblem0 }; // Emblem0 should be at end, because we add 1 to get Emblem1, 2 to get Emblem2... + inline void setHovered(bool hovered) { m_hovered = hovered; unbufferize(); } + void setHoveredZone(Zone zone); + inline bool hovered() { return m_hovered; } + inline Zone hoveredZone() { return m_hoveredZone; } + Zone zoneAt(const QPoint &pos, bool toAdd = false); + QRect zoneRect(Zone zone, const QPoint &pos); + void setCursor(Zone zone); + QString linkAt(const QPoint &pos); + private: + bool m_hovered; + Zone m_hoveredZone; + +/// SELECTION: + public: + NoteSelection* selectedNotes(); + void setSelected(bool selected); + void setSelectedRecursivly(bool selected); + void invertSelectionRecursivly(); + void selectIn(const QRect &rect, bool invertSelection, bool unselectOthers = true); + void setFocused(bool focused); + inline bool isFocused() { return m_focused; } + inline bool isSelected() { return m_selected; } + bool allSelected(); + void resetWasInLastSelectionRect(); + void unselectAllBut(Note *toSelect); + void invertSelectionOf(Note *toSelect); + Note* theSelectedNote(); + private: + bool m_focused; + bool m_selected; + bool m_wasInLastSelectionRect; + +/// TAGS: + private: + State::List m_states; + State m_computedState; + int m_emblemsCount; + bool m_haveInvisibleTags; + public: + /*const */State::List& states() const; + inline int emblemsCount() { return m_emblemsCount; } + void addState(State *state, bool orReplace = true); + void addTag(Tag *tag); + void removeState(State *state); + void removeTag(Tag *tag); + void removeAllTags(); + void addTagToSelectedNotes(Tag *tag); + void removeTagFromSelectedNotes(Tag *tag); + void removeAllTagsFromSelectedNotes(); + void addStateToSelectedNotes(State *state, bool orReplace = true); + void changeStateOfSelectedNotes(State *state); + bool selectedNotesHaveTags(); + void inheritTagsOf(Note *note); + bool hasTag(Tag *tag); + bool hasState(State *state); + State* stateOfTag(Tag *tag); + State* stateForEmblemNumber(int number); + bool stateForTagFromSelectedNotes(Tag *tag, State **state); + void recomputeStyle(); + void recomputeAllStyles(); + bool removedStates(const QValueList<State*> &deletedStates); + QFont font(); // Computed! + QColor backgroundColor(); // Computed! + QColor textColor(); // Computed! + +/// FILTERING: + private: + bool m_matching; + public: + bool computeMatching(const FilterData &data); + int newFilter(const FilterData &data); + bool matching() { return m_matching; } + +/// ADDED: + public: + void deleteSelectedNotes(bool deleteFilesToo = true); + int count(); + int countDirectChilds(); + + QString fullPath(); + Note* noteForFullPath(const QString &path); + + void update(); + void linkLookChanged(); + + void usedStates(QValueList<State*> &states); + + void listUsedTags(QValueList<Tag*> &list); + + + Note* nextInStack(); + Note* prevInStack(); + Note* nextShownInStack(); + Note* prevShownInStack(); + + Note* parentPrimaryNote(); // TODO: There are places in the code where this methods is hand-copied / hand-inlined instead of called. + + Note* firstSelected(); + Note* lastSelected(); + Note* selectedGroup(); + void groupIn(Note *group); + + bool tryExpandParent(); + bool tryFoldParent(); + + int distanceOnLeftRight(Note *note, int side); + int distanceOnTopBottom(Note *note, int side); + + bool convertTexts(); + + void debug(); + +/// SPEED OPTIMIZATION + public: + void finishLazyLoad(); + + public: + // Values are provided here as info: + // Please see Settings::setBigNotes() to know whats values are assigned. + static int NOTE_MARGIN /*= 2*/; + static int INSERTION_HEIGHT /*= 5*/; + static int EXPANDER_WIDTH /*= 9*/; + static int EXPANDER_HEIGHT /*= 9*/; + static int GROUP_WIDTH /*= 2*NOTE_MARGIN + EXPANDER_WIDTH*/; + static int HANDLE_WIDTH /*= GROUP_WIDTH*/; + static int RESIZER_WIDTH /*= GROUP_WIDTH*/; + static int TAG_ARROW_WIDTH /*= 5*/; + static int EMBLEM_SIZE /*= 16*/; + static int MIN_HEIGHT /*= 2*NOTE_MARGIN + EMBLEM_SIZE*/; +}; + +/*class InsertionData +{ + public: + enum Type { Free = 0, NoteRelative, ColumnBottom }; + Type type; + + InsertionData(int _x, int _y) + : type(Free), + x(_x), y(_y), + note(0), group(false), onTop(false), + column(0) + {} + int x; + int y; + + InsertionData(Note *_note, bool _group, bool _onTop) + : type(NoteRelative), + x(0), y(0), + note(_note), group(_group), onTop(_onTop), + column(0) + {} + Note *note; + bool group; + bool onTop; + + InsertionData(Note *_column) + : type(NoteRelative), + x(0), y(0), + note(0), group(false), onTop(false), + column(_column) + {} + Note *column; +};*/ + +#endif // NOTE_H diff --git a/src/notecontent.cpp b/src/notecontent.cpp new file mode 100644 index 0000000..39d4688 --- /dev/null +++ b/src/notecontent.cpp @@ -0,0 +1,1935 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qfile.h> +#include <qdir.h> +#include <qdom.h> +#include <qpainter.h> +#include <qstylesheet.h> +#include <qfontmetrics.h> +#include <qwidget.h> +#include <qcursor.h> +#include <qstringlist.h> +#include <qbuffer.h> +#include <ktextedit.h> +#include <kservice.h> +#include <kcolordialog.h> +#include <kmessagebox.h> +#include <klocale.h> +#include <kglobalsettings.h> +#include <kpixmapeffect.h> +#include <qbitmap.h> +#include <kurifilter.h> +//#include <kstringhandler.h> +#include <kfilemetainfo.h> +#include <qdatetime.h> +#include <kmultipledrag.h> + +#include <qfileinfo.h> +//#include <kio/kfileitem.h> +#include <kfileitem.h> +#include <kio/previewjob.h> +#include <kio/global.h> + +#include <iostream> + +#include "notecontent.h" +#include "note.h" +#include "noteedit.h" +#include "tag.h" +#include "basket.h" +#include "filter.h" +#include "xmlwork.h" +#include "tools.h" +#include "notefactory.h" +#include "linklabel.h" +#include "global.h" +#include "settings.h" +#include "focusedwidgets.h" +#include "debugwindow.h" +#include "kcolorcombo2.h" +#include "htmlexporter.h" + +#include "config.h" +#ifndef WITHOUT_ARTS + #include <arts/kplayobject.h> + #include <arts/kplayobjectfactory.h> + #include <arts/kartsserver.h> + #include <arts/kartsdispatcher.h> +#endif + +/** class NoteContent: + */ + +const int NoteContent::FEEDBACK_DARKING = 105; + +NoteContent::NoteContent(Note *parent, const QString &fileName) + : m_note(parent) +{ + parent->setContent(this); + setFileName(fileName); +} + +void NoteContent::saveToNode(QDomDocument &doc, QDomElement &content) +{ + if (useFile()) { + QDomText textNode = doc.createTextNode(fileName()); + content.appendChild(textNode); + } +} + +QRect NoteContent::zoneRect(int zone, const QPoint &/*pos*/) +{ + if (zone == Note::Content) + return QRect(0, 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() + else + return QRect(); +} + +KURL NoteContent::urlToOpen(bool /*with*/) +{ + return (useFile() ? KURL(fullPath()) : KURL()); +} + +void NoteContent::setFileName(const QString &fileName) +{ + m_fileName = fileName; +} + +bool NoteContent::trySetFileName(const QString &fileName) +{ + if (useFile() && fileName != m_fileName) { + QString newFileName = Tools::fileNameForNewFile(fileName, basket()->fullPath()); + QDir dir; + dir.rename(fullPath(), basket()->fullPathForFileName(newFileName)); + return true; + } + + return false; // !useFile() or unsuccesful rename +} + +QString NoteContent::fullPath() +{ + if (note() && useFile()) + return note()->fullPath(); + else + return ""; +} + +void NoteContent::contentChanged(int newMinWidth) +{ + m_minWidth = newMinWidth; + if (note()) { +// note()->unbufferize(); + note()->requestRelayout(); // TODO: It should re-set the width! m_width = 0 ? contentChanged: setWidth, geteight, if size havent changed, only repaint and not relayout + } +} + +Basket* NoteContent::basket() +{ + if (note()) + return note()->basket(); + else + return 0; +} + +void NoteContent::setEdited() +{ + note()->setLastModificationDate(QDateTime::currentDateTime()); + basket()->save(); +} + +/** All the Content Classes: + */ + +NoteType::Id TextContent::type() { return NoteType::Text; } +NoteType::Id HtmlContent::type() { return NoteType::Html; } +NoteType::Id ImageContent::type() { return NoteType::Image; } +NoteType::Id AnimationContent::type() { return NoteType::Animation; } +NoteType::Id SoundContent::type() { return NoteType::Sound; } +NoteType::Id FileContent::type() { return NoteType::File; } +NoteType::Id LinkContent::type() { return NoteType::Link; } +NoteType::Id LauncherContent::type() { return NoteType::Launcher; } +NoteType::Id ColorContent::type() { return NoteType::Color; } +NoteType::Id UnknownContent::type() { return NoteType::Unknown; } + +QString TextContent::typeName() { return i18n("Plain Text"); } +QString HtmlContent::typeName() { return i18n("Text"); } +QString ImageContent::typeName() { return i18n("Image"); } +QString AnimationContent::typeName() { return i18n("Animation"); } +QString SoundContent::typeName() { return i18n("Sound"); } +QString FileContent::typeName() { return i18n("File"); } +QString LinkContent::typeName() { return i18n("Link"); } +QString LauncherContent::typeName() { return i18n("Launcher"); } +QString ColorContent::typeName() { return i18n("Color"); } +QString UnknownContent::typeName() { return i18n("Unknown"); } + +QString TextContent::lowerTypeName() { return "text"; } +QString HtmlContent::lowerTypeName() { return "html"; } +QString ImageContent::lowerTypeName() { return "image"; } +QString AnimationContent::lowerTypeName() { return "animation"; } +QString SoundContent::lowerTypeName() { return "sound"; } +QString FileContent::lowerTypeName() { return "file"; } +QString LinkContent::lowerTypeName() { return "link"; } +QString LauncherContent::lowerTypeName() { return "launcher"; } +QString ColorContent::lowerTypeName() { return "color"; } +QString UnknownContent::lowerTypeName() { return "unknown"; } + +QString NoteContent::toText(const QString &cuttedFullPath) +{ + return (cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); +} + +QString TextContent::toText(const QString &/*cuttedFullPath*/) { return text(); } +QString HtmlContent::toText(const QString &/*cuttedFullPath*/) { return Tools::htmlToText(html()); } +QString LinkContent::toText(const QString &/*cuttedFullPath*/) +{ + if (autoTitle()) + return url().prettyURL(); + else if (title().isEmpty() && url().isEmpty()) + return ""; + else if (url().isEmpty()) + return title(); + else if (title().isEmpty()) + return url().prettyURL(); + else + return QString("%1 <%2>").arg(title(), url().prettyURL()); +} +QString ColorContent::toText(const QString &/*cuttedFullPath*/) { return color().name(); } +QString UnknownContent::toText(const QString &/*cuttedFullPath*/) { return ""; } + +// TODO: If imageName.isEmpty() return fullPath() because it's for external use, else return fileName() because it's to display in a tooltip +QString TextContent::toHtml(const QString &/*imageName*/, const QString &/*cuttedFullPath*/) +{ return Tools::textToHTMLWithoutP(text()); } + +QString HtmlContent::toHtml(const QString &/*imageName*/, const QString &/*cuttedFullPath*/) +{ return Tools::htmlToParagraph(html()); } + +QString ImageContent::toHtml(const QString &/*imageName*/, const QString &cuttedFullPath) +{ return QString("<img src=\"%1\">").arg(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } + +QString AnimationContent::toHtml(const QString &/*imageName*/, const QString &cuttedFullPath) +{ return QString("<img src=\"%1\">").arg(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); } + +QString SoundContent::toHtml(const QString &/*imageName*/, const QString &cuttedFullPath) +{ return QString("<a href=\"%1\">%2</a>").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), fileName()); } // With the icon? + +QString FileContent::toHtml(const QString &/*imageName*/, const QString &cuttedFullPath) +{ return QString("<a href=\"%1\">%2</a>").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), fileName()); } // With the icon? + +QString LinkContent::toHtml(const QString &/*imageName*/, const QString &/*cuttedFullPath*/) +{ return QString("<a href=\"%1\">%2</a>").arg(url().prettyURL(), title()); } // With the icon? + +QString LauncherContent::toHtml(const QString &/*imageName*/, const QString &cuttedFullPath) +{ return QString("<a href=\"%1\">%2</a>").arg((cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath), name()); } // With the icon? + +QString ColorContent::toHtml(const QString &/*imageName*/, const QString &/*cuttedFullPath*/) +{ return QString("<span style=\"color: %1\">%2</span>").arg(color().name(), color().name()); } + +QString UnknownContent::toHtml(const QString &/*imageName*/, const QString &/*cuttedFullPath*/) +{ return ""; } + +QPixmap ImageContent::toPixmap() { return pixmap(); } +QPixmap AnimationContent::toPixmap() { return movie().framePixmap(); } + +void NoteContent::toLink(KURL *url, QString *title, const QString &cuttedFullPath) +{ + if (useFile()) { + *url = KURL(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); + *title = (cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); + } else { + *url = KURL(); + *title = QString(); + } +} +void LinkContent::toLink(KURL *url, QString *title, const QString &/*cuttedFullPath*/) +{ + *url = this->url(); + *title = this->title(); +} + +void LauncherContent::toLink(KURL *url, QString *title, const QString &cuttedFullPath) +{ + *url = KURL(cuttedFullPath.isEmpty() ? fullPath() : cuttedFullPath); + *title = name(); +} +void UnknownContent::toLink(KURL *url, QString *title, const QString &/*cuttedFullPath*/) +{ + *url = KURL(); + *title = QString(); +} + +bool TextContent::useFile() { return true; } +bool HtmlContent::useFile() { return true; } +bool ImageContent::useFile() { return true; } +bool AnimationContent::useFile() { return true; } +bool SoundContent::useFile() { return true; } +bool FileContent::useFile() { return true; } +bool LinkContent::useFile() { return false; } +bool LauncherContent::useFile() { return true; } +bool ColorContent::useFile() { return false; } +bool UnknownContent::useFile() { return true; } + +bool TextContent::canBeSavedAs() { return true; } +bool HtmlContent::canBeSavedAs() { return true; } +bool ImageContent::canBeSavedAs() { return true; } +bool AnimationContent::canBeSavedAs() { return true; } +bool SoundContent::canBeSavedAs() { return true; } +bool FileContent::canBeSavedAs() { return true; } +bool LinkContent::canBeSavedAs() { return true; } +bool LauncherContent::canBeSavedAs() { return true; } +bool ColorContent::canBeSavedAs() { return false; } +bool UnknownContent::canBeSavedAs() { return false; } + +QString TextContent::saveAsFilters() { return "text/plain"; } +QString HtmlContent::saveAsFilters() { return "text/html"; } +QString ImageContent::saveAsFilters() { return "image/png"; } // TODO: Offer more types +QString AnimationContent::saveAsFilters() { return "image/gif"; } // TODO: MNG... +QString SoundContent::saveAsFilters() { return "audio/x-mp3"; } // TODO: OGG... +QString FileContent::saveAsFilters() { return "*"; } // TODO: Get MIME type of the url target +QString LinkContent::saveAsFilters() { return "*"; } // TODO: idem File + If isDir(): return +QString LauncherContent::saveAsFilters() { return "application/x-desktop"; } +QString ColorContent::saveAsFilters() { return ""; } +QString UnknownContent::saveAsFilters() { return ""; } + +bool TextContent::match(const FilterData &data) { return (text().find(data.string, /*index=*/0, /*cs=*/false) != -1); } +bool HtmlContent::match(const FilterData &data) { return (m_textEquivalent/*toText("")*/.find(data.string, /*index=*/0, /*cs=*/false) != -1); } //OPTIM_FILTER +bool ImageContent::match(const FilterData &/*data*/) { return false; } +bool AnimationContent::match(const FilterData &/*data*/) { return false; } +bool SoundContent::match(const FilterData &data) { return (fileName().find(data.string, /*index=*/0, /*cs=*/false) != -1); } +bool FileContent::match(const FilterData &data) { return (fileName().find(data.string, /*index=*/0, /*cs=*/false) != -1); } +bool LinkContent::match(const FilterData &data) { return (title().find(data.string, 0, false) != -1 || url().prettyURL().find(data.string, 0, false) != -1); } +bool LauncherContent::match(const FilterData &data) { return (exec().find(data.string, 0, false) != -1 || name().find(data.string, 0, false) != -1); } +bool ColorContent::match(const FilterData &data) { return (color().name().find(data.string, /*index=*/0, /*cs=*/false) != -1); } +bool UnknownContent::match(const FilterData &data) { return (mimeTypes().find(data.string, /*index=*/0, /*cs=*/false) != -1); } + +QString TextContent::editToolTipText() { return i18n("Edit this plain text"); } +QString HtmlContent::editToolTipText() { return i18n("Edit this text"); } +QString ImageContent::editToolTipText() { return i18n("Edit this image"); } +QString AnimationContent::editToolTipText() { return i18n("Edit this animation"); } +QString SoundContent::editToolTipText() { return i18n("Edit the file name of this sound"); } +QString FileContent::editToolTipText() { return i18n("Edit the name of this file"); } +QString LinkContent::editToolTipText() { return i18n("Edit this link"); } +QString LauncherContent::editToolTipText() { return i18n("Edit this launcher"); } +QString ColorContent::editToolTipText() { return i18n("Edit this color"); } +QString UnknownContent::editToolTipText() { return i18n("Edit this unknown object"); } + +QString TextContent::cssClass() { return ""; } +QString HtmlContent::cssClass() { return ""; } +QString ImageContent::cssClass() { return ""; } +QString AnimationContent::cssClass() { return ""; } +QString SoundContent::cssClass() { return "sound"; } +QString FileContent::cssClass() { return "file"; } +QString LinkContent::cssClass() { return (LinkLook::lookForURL(m_url) == LinkLook::localLinkLook ? "local" : "network"); } +QString LauncherContent::cssClass() { return "launcher"; } +QString ColorContent::cssClass() { return "" ; } +QString UnknownContent::cssClass() { return ""; } + +void TextContent::fontChanged() { setText(text()); } +void HtmlContent::fontChanged() { setHtml(html()); } +void ImageContent::fontChanged() { setPixmap(pixmap()); } +void AnimationContent::fontChanged() { setMovie(movie()); } +void FileContent::fontChanged() { setFileName(fileName()); } +void LinkContent::fontChanged() { setLink(url(), title(), icon(), autoTitle(), autoIcon()); } +void LauncherContent::fontChanged() { setLauncher(name(), icon(), exec()); } +void ColorContent::fontChanged() { setColor(color()); } +void UnknownContent::fontChanged() { loadFromFile(/*lazyLoad=*/false); } // TODO: Optimize: setMimeTypes() + +//QString TextContent::customOpenCommand() { return (Settings::isTextUseProg() && ! Settings::textProg().isEmpty() ? Settings::textProg() : QString()); } +QString HtmlContent::customOpenCommand() { return (Settings::isHtmlUseProg() && ! Settings::htmlProg().isEmpty() ? Settings::htmlProg() : QString()); } +QString ImageContent::customOpenCommand() { return (Settings::isImageUseProg() && ! Settings::imageProg().isEmpty() ? Settings::imageProg() : QString()); } +QString AnimationContent::customOpenCommand() { return (Settings::isAnimationUseProg() && ! Settings::animationProg().isEmpty() ? Settings::animationProg() : QString()); } +QString SoundContent::customOpenCommand() { return (Settings::isSoundUseProg() && ! Settings::soundProg().isEmpty() ? Settings::soundProg() : QString()); } + +void LinkContent::serialize(QDataStream &stream) { stream << url() << title() << icon() << (Q_UINT64)autoTitle() << (Q_UINT64)autoIcon(); } +void ColorContent::serialize(QDataStream &stream) { stream << color(); } + +QPixmap TextContent::feedbackPixmap(int width, int height) +{ + QRect textRect = QFontMetrics(note()->font()).boundingRect(0, 0, width, height, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, text()); + QPixmap pixmap( QMIN(width, textRect.width()), QMIN(height, textRect.height()) ); + pixmap.fill(note()->backgroundColor().dark(FEEDBACK_DARKING)); + QPainter painter(&pixmap); + painter.setPen(note()->textColor()); + painter.setFont(note()->font()); + painter.drawText(0, 0, pixmap.width(), pixmap.height(), Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, text()); + painter.end(); + return pixmap; +} + +QPixmap HtmlContent::feedbackPixmap(int width, int height) +{ + QSimpleRichText richText(html(), note()->font()); + richText.setWidth(width); + QColorGroup colorGroup(basket()->colorGroup()); + colorGroup.setColor(QColorGroup::Text, note()->textColor()); + colorGroup.setColor(QColorGroup::Background, note()->backgroundColor().dark(FEEDBACK_DARKING)); + QPixmap pixmap( QMIN(width, richText.widthUsed()), QMIN(height, richText.height()) ); + pixmap.fill(note()->backgroundColor().dark(FEEDBACK_DARKING)); + QPainter painter(&pixmap); + painter.setPen(note()->textColor()); + richText.draw(&painter, 0, 0, QRect(0, 0, pixmap.width(), pixmap.height()), colorGroup); + painter.end(); + return pixmap; +} + +QPixmap ImageContent::feedbackPixmap(int width, int height) +{ + if (width >= m_pixmap.width() && height >= m_pixmap.height()) { // Full size + if (m_pixmap.hasAlpha()) { + QPixmap opaque(m_pixmap.width(), m_pixmap.height()); + opaque.fill(note()->backgroundColor().dark(FEEDBACK_DARKING)); + QPainter painter(&opaque); + painter.drawPixmap(0, 0, m_pixmap); + painter.end(); + return opaque; + } else + return m_pixmap; + } else { // Scalled down + QImage imageToScale = m_pixmap.convertToImage(); + QPixmap pmScaled; + pmScaled.convertFromImage(imageToScale./*smoothScale*/scale(width, height, QImage::ScaleMin)); + if (pmScaled.hasAlpha()) { + QPixmap opaque(pmScaled.width(), pmScaled.height()); + opaque.fill(note()->backgroundColor().dark(FEEDBACK_DARKING)); + QPainter painter(&opaque); + painter.drawPixmap(0, 0, pmScaled); + painter.end(); + return opaque; + } else + return pmScaled; + } +} + +QPixmap AnimationContent::feedbackPixmap(int width, int height) +{ + QPixmap pixmap = m_movie.framePixmap(); + if (width >= pixmap.width() && height >= pixmap.height()) // Full size + return pixmap; + else { // Scalled down + QImage imageToScale = pixmap.convertToImage(); + QPixmap pmScaled; + pmScaled.convertFromImage(imageToScale./*smoothScale*/scale(width, height, QImage::ScaleMin)); + return pmScaled; + } +} + +QPixmap LinkContent::feedbackPixmap(int width, int height) +{ + QColorGroup colorGroup(basket()->colorGroup()); + colorGroup.setColor(QColorGroup::Text, note()->textColor()); + colorGroup.setColor(QColorGroup::Background, note()->backgroundColor().dark(FEEDBACK_DARKING)); + return m_linkDisplay.feedbackPixmap(width, height, colorGroup, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); +} + +QPixmap ColorContent::feedbackPixmap(int width, int height) +{ + // TODO: Duplicate code: make a rect() method! + QRect textRect = QFontMetrics(note()->font()).boundingRect(color().name()); + int rectHeight = (textRect.height() + 2)*3/2; + int rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. + + QColorGroup colorGroup(basket()->colorGroup()); + colorGroup.setColor(QColorGroup::Text, note()->textColor()); + colorGroup.setColor(QColorGroup::Background, note()->backgroundColor().dark(FEEDBACK_DARKING)); + + QPixmap pixmap( QMIN(width, rectWidth + RECT_MARGIN + textRect.width() + RECT_MARGIN), QMIN(height, rectHeight) ); + pixmap.fill(note()->backgroundColor().dark(FEEDBACK_DARKING)); + QPainter painter(&pixmap); + paint(&painter, pixmap.width(), pixmap.height(), colorGroup, false, false, false); // We don't care of the three last boolean parameters. + painter.end(); + return pixmap; +} + +QPixmap FileContent::feedbackPixmap(int width, int height) +{ + QColorGroup colorGroup(basket()->colorGroup()); + colorGroup.setColor(QColorGroup::Text, note()->textColor()); + colorGroup.setColor(QColorGroup::Background, note()->backgroundColor().dark(FEEDBACK_DARKING)); + return m_linkDisplay.feedbackPixmap(width, height, colorGroup, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); +} + +QPixmap LauncherContent::feedbackPixmap(int width, int height) +{ + QColorGroup colorGroup(basket()->colorGroup()); + colorGroup.setColor(QColorGroup::Text, note()->textColor()); + colorGroup.setColor(QColorGroup::Background, note()->backgroundColor().dark(FEEDBACK_DARKING)); + return m_linkDisplay.feedbackPixmap(width, height, colorGroup, /*isDefaultColor=*/note()->textColor() == basket()->textColor()); +} + +QPixmap UnknownContent::feedbackPixmap(int width, int height) +{ + QRect textRect = QFontMetrics(note()->font()).boundingRect(0, 0, /*width=*/1, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, m_mimeTypes); + + QColorGroup colorGroup(basket()->colorGroup()); + colorGroup.setColor(QColorGroup::Text, note()->textColor()); + colorGroup.setColor(QColorGroup::Background, note()->backgroundColor().dark(FEEDBACK_DARKING)); + + QPixmap pixmap( QMIN(width, DECORATION_MARGIN + textRect.width() + DECORATION_MARGIN), QMIN(height, DECORATION_MARGIN + textRect.height() + DECORATION_MARGIN) ); + QPainter painter(&pixmap); + paint(&painter, pixmap.width() + 1, pixmap.height(), colorGroup, false, false, false); // We don't care of the three last boolean parameters. + painter.setPen(note()->backgroundColor().dark(FEEDBACK_DARKING)); + painter.drawPoint(0, 0); + painter.drawPoint(pixmap.width() - 1, 0); + painter.drawPoint(0, pixmap.height() - 1); + painter.drawPoint(pixmap.width() - 1, pixmap.height() - 1); + painter.end(); + return pixmap; +} + + +/** class TextContent: + */ + +TextContent::TextContent(Note *parent, const QString &fileName, bool lazyLoad) + : NoteContent(parent, fileName), m_simpleRichText(0) +{ + basket()->addWatchedFile(fullPath()); + loadFromFile(lazyLoad); +} + +TextContent::~TextContent() +{ + delete m_simpleRichText; +} + +int TextContent::setWidthAndGetHeight(int width) +{ + if (m_simpleRichText) { + width -= 1; + m_simpleRichText->setWidth(width); + return m_simpleRichText->height(); + } else + return 10; // Lazy loaded +} + +void TextContent::paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool /*isDefaultColor*/, bool /*isSelected*/, bool /*isHovered*/) +{ + if (m_simpleRichText) { + width -= 1; + m_simpleRichText->draw(painter, 0, 0, QRect(0, 0, width, height), colorGroup); + } +} + +bool TextContent::loadFromFile(bool lazyLoad) +{ + DEBUG_WIN << "Loading TextContent From " + basket()->folderName() + fileName(); + + QString content; + bool success = basket()->loadFromFile(fullPath(), &content, /*isLocalEncoding=*/true); + + if (success) + setText(content, lazyLoad); + else { + std::cout << "FAILED TO LOAD TextContent: " << fullPath() << std::endl; + setText("", lazyLoad); + if (!QFile::exists(fullPath())) + saveToFile(); // Reserve the fileName so no new note will have the same name! + } + return success; +} + +bool TextContent::finishLazyLoad() +{ + int width = (m_simpleRichText ? m_simpleRichText->width() : 1); + delete m_simpleRichText; + QString html = "<html><head><meta name=\"qrichtext\" content=\"1\" /></head><body>" + Tools::tagURLs(Tools::textToHTML(m_text)); // Don't collapse multiple spaces! + m_simpleRichText = new QSimpleRichText(html, note()->font()); + m_simpleRichText->setWidth(1); // We put a width of 1 pixel, so usedWidth() is egual to the minimum width + int minWidth = m_simpleRichText->widthUsed(); + m_simpleRichText->setWidth(width); + contentChanged(minWidth + 1); + + return true; +} + +bool TextContent::saveToFile() +{ + return basket()->saveToFile(fullPath(), text(), /*isLocalEncoding=*/true); +} + +QString TextContent::linkAt(const QPoint &pos) +{ + if (m_simpleRichText) + return m_simpleRichText->anchorAt(pos); + else + return ""; // Lazy loaded +} + + +QString TextContent::messageWhenOpenning(OpenMessage where) +{ + switch (where) { + case OpenOne: return i18n("Opening plain text..."); + case OpenSeveral: return i18n("Opening plain texts..."); + case OpenOneWith: return i18n("Opening plain text with..."); + case OpenSeveralWith: return i18n("Opening plain texts with..."); + case OpenOneWithDialog: return i18n("Open plain text with:"); + case OpenSeveralWithDialog: return i18n("Open plain texts with:"); + default: return ""; + } +} + +void TextContent::setText(const QString &text, bool lazyLoad) +{ + m_text = text; + if (!lazyLoad) + finishLazyLoad(); + else + contentChanged(10); +} + +void TextContent::exportToHTML(HTMLExporter *exporter, int indent) +{ + QString spaces; + QString html = "<html><head><meta name=\"qrichtext\" content=\"1\" /></head><body>" + + Tools::tagURLs(Tools::textToHTMLWithoutP(text().replace("\t", " "))); // Don't collapse multiple spaces! + exporter->stream << html.replace(" ", " ").replace("\n", "\n" + spaces.fill(' ', indent + 1)); +} + +/** class HtmlContent: + */ + +HtmlContent::HtmlContent(Note *parent, const QString &fileName, bool lazyLoad) + : NoteContent(parent, fileName), m_simpleRichText(0) +{ + basket()->addWatchedFile(fullPath()); + loadFromFile(lazyLoad); +} + +HtmlContent::~HtmlContent() +{ + delete m_simpleRichText; +} + +int HtmlContent::setWidthAndGetHeight(int width) +{ + if (m_simpleRichText) { + width -= 1; + m_simpleRichText->setWidth(width); + return m_simpleRichText->height(); + } else + return 10; // Lazy loaded +} + +void HtmlContent::paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool /*isDefaultColor*/, bool /*isSelected*/, bool /*isHovered*/) +{ + if (m_simpleRichText) { + width -= 1; + m_simpleRichText->draw(painter, 0, 0, QRect(0, 0, width, height), colorGroup); + } +} + +bool HtmlContent::loadFromFile(bool lazyLoad) +{ + DEBUG_WIN << "Loading HtmlContent From " + basket()->folderName() + fileName(); + + QString content; + bool success = basket()->loadFromFile(fullPath(), &content, /*isLocalEncoding=*/true); + + if (success) + setHtml(content, lazyLoad); + else { + std::cout << "FAILED TO LOAD HtmlContent: " << fullPath() << std::endl; + setHtml("", lazyLoad); + if (!QFile::exists(fullPath())) + saveToFile(); // Reserve the fileName so no new note will have the same name! + } + return success; +} + +bool HtmlContent::finishLazyLoad() +{ + int width = (m_simpleRichText ? m_simpleRichText->width() : 1); + delete m_simpleRichText; + m_simpleRichText = new QSimpleRichText(Tools::tagURLs(m_html), note()->font()); + m_simpleRichText->setWidth(1); // We put a width of 1 pixel, so usedWidth() is egual to the minimum width + int minWidth = m_simpleRichText->widthUsed(); + m_simpleRichText->setWidth(width); + contentChanged(minWidth + 1); + + return true; +} + +bool HtmlContent::saveToFile() +{ + return basket()->saveToFile(fullPath(), html(), /*isLocalEncoding=*/true); +} + +QString HtmlContent::linkAt(const QPoint &pos) +{ + if (m_simpleRichText) + return m_simpleRichText->anchorAt(pos); + else + return ""; // Lazy loaded +} + + +QString HtmlContent::messageWhenOpenning(OpenMessage where) +{ + switch (where) { + case OpenOne: return i18n("Opening text..."); + case OpenSeveral: return i18n("Opening texts..."); + case OpenOneWith: return i18n("Opening text with..."); + case OpenSeveralWith: return i18n("Opening texts with..."); + case OpenOneWithDialog: return i18n("Open text with:"); + case OpenSeveralWithDialog: return i18n("Open texts with:"); + default: return ""; + } +} + +void HtmlContent::setHtml(const QString &html, bool lazyLoad) +{ + m_html = html; + m_textEquivalent = toText(""); //OPTIM_FILTER + if (!lazyLoad) + finishLazyLoad(); + else + contentChanged(10); +} + +void HtmlContent::exportToHTML(HTMLExporter *exporter, int indent) +{ + QString spaces; + exporter->stream << Tools::htmlToParagraph(Tools::tagURLs(html().replace("\t", " "))) + .replace(" ", " ") + .replace("\n", "\n" + spaces.fill(' ', indent + 1)); +} + +/** class ImageContent: + */ + +ImageContent::ImageContent(Note *parent, const QString &fileName, bool lazyLoad) + : NoteContent(parent, fileName), m_format(0) +{ + basket()->addWatchedFile(fullPath()); + loadFromFile(lazyLoad); +} + +int ImageContent::setWidthAndGetHeight(int width) +{ + width -= 1; + // Don't store width: we will get it on paint! + if (width >= m_pixmap.width()) // Full size + return m_pixmap.height(); + else { // Scalled down + double height = m_pixmap.height() * (double)width / m_pixmap.width(); + return int((double)(int)height <= (height - 0.5) ? height + 1 : height); + } +} + +void ImageContent::paint(QPainter *painter, int width, int /*height*/, const QColorGroup &/*colorGroup*/, bool /*isDefaultColor*/, bool /*isSelected*/, bool /*isHovered*/) +{ + width -= 1; +// KPixmap pixmap = m_pixmap; +// if (note()->isSelected()) +// pixmap = KPixmapEffect::selectedPixmap(m_pixmap, KGlobalSettings::highlightColor()); + + if (width >= m_pixmap.width()) // Full size + painter->drawPixmap(0, 0, m_pixmap); + else { // Scalled down + double scale = ((double)width) / m_pixmap.width(); + painter->scale(scale, scale); + painter->drawPixmap(0, 0, m_pixmap); // TODO: Smooth !!! + } +} + +bool ImageContent::loadFromFile(bool lazyLoad) +{ + if (lazyLoad) + return true; + else + return finishLazyLoad(); +} + +bool ImageContent::finishLazyLoad() +{ + DEBUG_WIN << "Loading ImageContent From " + basket()->folderName() + fileName(); + + QByteArray content; + + if (basket()->loadFromFile(fullPath(), &content)) + { + QBuffer buffer(content); + + buffer.open(IO_ReadOnly); + m_format = (char* /* from const char* */)QImageIO::imageFormat(&buffer); // See QImageIO to know what formats can be supported. + buffer.close(); + if (m_format) { + m_pixmap.loadFromData(content); + setPixmap(m_pixmap); + return true; + } + } + + std::cout << "FAILED TO LOAD ImageContent: " << fullPath() << std::endl; + m_format = (char*)"PNG"; // If the image is set later, it should be saved without destruction, so we use PNG by default. + m_pixmap.resize(1, 1); // Create a 1x1 pixels image instead of an undefined one. + m_pixmap.fill(); + m_pixmap.setMask(m_pixmap.createHeuristicMask()); + setPixmap(m_pixmap); + if (!QFile::exists(fullPath())) + saveToFile(); // Reserve the fileName so no new note will have the same name! + return false; +} + +bool ImageContent::saveToFile() +{ + QByteArray ba; + QBuffer buffer(ba); + + buffer.open(IO_WriteOnly); + m_pixmap.save(&buffer, m_format); + return basket()->saveToFile(fullPath(), ba); +} + + +void ImageContent::toolTipInfos(QStringList *keys, QStringList *values) +{ + keys->append(i18n("Size")); + values->append(i18n("%1 by %2 pixels").arg(QString::number(m_pixmap.width()), QString::number(m_pixmap.height()))); +} + +QString ImageContent::messageWhenOpenning(OpenMessage where) +{ + switch (where) { + case OpenOne: return i18n("Opening image..."); + case OpenSeveral: return i18n("Opening images..."); + case OpenOneWith: return i18n("Opening image with..."); + case OpenSeveralWith: return i18n("Opening images with..."); + case OpenOneWithDialog: return i18n("Open image with:"); + case OpenSeveralWithDialog: return i18n("Open images with:"); + default: return ""; + } +} + +void ImageContent::setPixmap(const QPixmap &pixmap) +{ + m_pixmap = pixmap; + // Since it's scalled, the height is always greater or equal to the size of the tag emblems (16) + contentChanged(16 + 1); // TODO: always good? I don't think... +} + +void ImageContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) +{ + int width = m_pixmap.width(); + int height = m_pixmap.height(); + int contentWidth = note()->width() - note()->contentX() - 1 - Note::NOTE_MARGIN; + + QString imageName = exporter->copyFile(fullPath(), /*createIt=*/true); + + if (contentWidth <= m_pixmap.width()) { // Scalled down + double scale = ((double)contentWidth) / m_pixmap.width(); + width = (int)(m_pixmap.width() * scale); + height = (int)(m_pixmap.height() * scale); + exporter->stream << "<a href=\"" << exporter->dataFolderName << imageName << "\" title=\"" << i18n("Click for full size view") << "\">"; + } + + exporter->stream << "<img src=\"" << exporter->dataFolderName << imageName + << "\" width=\"" << width << "\" height=\"" << height << "\" alt=\"\">"; + + if (contentWidth <= m_pixmap.width()) // Scalled down + exporter->stream << "</a>"; +} + +/** class AnimationContent: + */ + +int AnimationContent::INVALID_STATUS = -100; + +AnimationContent::AnimationContent(Note *parent, const QString &fileName, bool lazyLoad) + : NoteContent(parent, fileName), m_oldStatus(INVALID_STATUS) +{ + basket()->addWatchedFile(fullPath()); + loadFromFile(lazyLoad); +} + +int AnimationContent::setWidthAndGetHeight(int /*width*/) +{ + /*width -= 1*/; + return m_movie.framePixmap().height() ; // TODO!!! +} + +void AnimationContent::paint(QPainter *painter, int width, int /*height*/, const QColorGroup &/*colorGroup*/, bool /*isDefaultColor*/, bool /*isSelected*/, bool /*isHovered*/) +{ + /*width -= 1*/; +// DEBUG_WIN << "AnimationContent::paint()"; + const QPixmap &frame = m_movie.framePixmap(); + if (width >= frame.width()) // Full size + painter->drawPixmap(0, 0, frame); + else // Scalled down + painter->drawPixmap(0, 0, frame); // TODO: Scall down +} + +bool AnimationContent::loadFromFile(bool lazyLoad) +{ + if (lazyLoad) + return true; + else + return finishLazyLoad(); +} + +bool AnimationContent::finishLazyLoad() +{ + DEBUG_WIN << "Loading MovieContent From " + basket()->folderName() + fileName(); +// return setMovie(QMovie(fullPath())); + + bool success = false; + QByteArray content; + if (basket()->loadFromFile(fullPath(), &content)) + success = setMovie(QMovie(content, content.size())); + if (!success) + setMovie(QMovie()); + return success; +} + +bool AnimationContent::saveToFile() +{ + // Impossible! + return false; +} + + +QString AnimationContent::messageWhenOpenning(OpenMessage where) +{ + switch (where) { + case OpenOne: return i18n("Opening animation..."); + case OpenSeveral: return i18n("Opening animations..."); + case OpenOneWith: return i18n("Opening animation with..."); + case OpenSeveralWith: return i18n("Opening animations with..."); + case OpenOneWithDialog: return i18n("Open animation with:"); + case OpenSeveralWithDialog: return i18n("Open animations with:"); + default: return ""; + } +} + +bool AnimationContent::setMovie(const QMovie &movie) +{ + if (!m_movie.isNull()) { + // Disconnect? + return false; + } + m_movie = movie; + m_movie.connectUpdate( this, SLOT(movieUpdated(const QRect&)) ); + m_movie.connectResize( this, SLOT(movieResized(const QSize&)) ); + m_movie.connectStatus( this, SLOT(movieStatus(int)) ); + contentChanged( m_movie.framePixmap().width() + 1 ); // TODO + return true; +} + +void AnimationContent::movieUpdated(const QRect&) +{ + note()->unbufferize(); + note()->update(); +} + +void AnimationContent::movieResized(const QSize&) +{ + note()->requestRelayout(); // ? +} + +/** When a user drop a .gif file, for instance, we don't know if it is an image + * or an animtion (gif file contain multiple images). + * To determin that, we assume this is an animation and count the number of images. + * QMovie send, in this order: + * - For a unique image: QMovie::EndOfFrame, QMovie::EndOfLoop, QMovie::EndOfMovie. + * - For animation: QMovie::EndOfFrame... (for each image), QMovie::EndOfLoop, + * and it then restart that for each loop. + */ +void AnimationContent::movieStatus(int status) +{ + DEBUG_WIN << "movieStatus()"; + + // At least two frames: it's an animation, everything is OK + if (m_oldStatus == QMovie::EndOfFrame && status == QMovie::EndOfFrame) { + movie().disconnectStatus(this); + m_oldStatus = INVALID_STATUS; +// if (note()->isFocused()) // When inserting a new note we ensure it visble +// basket()->ensureNoteVisible(note()); // But after loading it has certainly grown and if it was + } + // Only one image: it's an image, change note's type + else if (m_oldStatus == QMovie::EndOfFrame && status == QMovie::EndOfLoop) { + movie().disconnectStatus(this); + m_oldStatus = INVALID_STATUS; + note()->setContent(new ImageContent(note(), fileName())); + basket()->save(); + //delete this; // CRASH, as always !!!!!!!!! + //QTimer::singleShot(0, this, SLOT(loadContent())); // Delayed to avoid crash! + //QTimer::singleShot(100, this, SLOT(saveProperties())); // We should save it's an image and not an animation +// if (note()->isFocused()) +// QTimer::singleShot(25, note(), SLOT(delayedEnsureVisible())); + } + else + m_oldStatus = status; +} + +void AnimationContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) +{ + exporter->stream << QString("<img src=\"%1\" width=\"%2\" height=\"%3\" alt=\"\">") + .arg( exporter->dataFolderName + exporter->copyFile(fullPath(), /*createIt=*/true), + QString::number(movie().framePixmap().size().width()), + QString::number(movie().framePixmap().size().height()) ); +} + +/** class FileContent: + */ + +FileContent::FileContent(Note *parent, const QString &fileName) + : NoteContent(parent, fileName), m_previewJob(0) +{ + basket()->addWatchedFile(fullPath()); + setFileName(fileName); // FIXME: TO THAT HERE BECAUSE NoteContent() constructor seems to don't be able to call virtual methods??? +} + +int FileContent::setWidthAndGetHeight(int width) +{ + m_linkDisplay.setWidth(width); + return m_linkDisplay.height(); +} + +void FileContent::paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered) +{ + m_linkDisplay.paint(painter, 0, 0, width, height, colorGroup, isDefaultColor, isSelected, isHovered, isHovered && note()->hoveredZone() == Note::Custom0); +} + +bool FileContent::loadFromFile(bool /*lazyLoad*/) +{ + setFileName(fileName()); // File changed: get new file preview! + return true; +} + +void FileContent::toolTipInfos(QStringList *keys, QStringList *values) +{ + // Get the size of the file: + uint size = QFileInfo(fullPath()).size(); + QString humanFileSize = KIO::convertSize((KIO::filesize_t)size); + + keys->append(i18n("Size")); + values->append(humanFileSize); + + KMimeType::Ptr mime = KMimeType::findByURL(KURL(fullPath())); + if (mime) { + keys->append(i18n("Type")); + values->append(mime->comment()); + } + + KFileMetaInfo infos = KFileMetaInfo(KURL(fullPath())); + if (infos.isValid() && !infos.isEmpty()) { + QStringList groups = infos.preferredKeys(); + int i = 0; + for (QStringList::Iterator it = groups.begin(); i < 6 && it != groups.end(); ++it) { + KFileMetaInfoItem metaInfoItem = infos.item(*it); + if (!metaInfoItem.string().isEmpty()) { + keys->append(metaInfoItem.translatedKey()); + values->append(metaInfoItem.string()); + ++i; + } + } + } +} + +int FileContent::zoneAt(const QPoint &pos) +{ + return (m_linkDisplay.iconButtonAt(pos) ? 0 : Note::Custom0); +} + +QRect FileContent::zoneRect(int zone, const QPoint &/*pos*/) +{ + QRect linkRect = m_linkDisplay.iconButtonRect(); + + if (zone == Note::Custom0) + return QRect(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() + else if (zone == Note::Content) + return linkRect; + else + return QRect(); +} + +QString FileContent::zoneTip(int zone) +{ + return (zone == Note::Custom0 ? i18n("Open this file") : QString()); +} + +void FileContent::setCursor(QWidget *widget, int zone) +{ + if (zone == Note::Custom0) + widget->setCursor(Qt::PointingHandCursor); +} + + +int FileContent::xEditorIndent() +{ + return m_linkDisplay.iconButtonRect().width() + 2; +} + + +QString FileContent::messageWhenOpenning(OpenMessage where) +{ + switch (where) { + case OpenOne: return i18n("Opening file..."); + case OpenSeveral: return i18n("Opening files..."); + case OpenOneWith: return i18n("Opening file with..."); + case OpenSeveralWith: return i18n("Opening files with..."); + case OpenOneWithDialog: return i18n("Open file with:"); + case OpenSeveralWithDialog: return i18n("Open files with:"); + default: return ""; + } +} + +void FileContent::setFileName(const QString &fileName) +{ + NoteContent::setFileName(fileName); + KURL url = KURL(fullPath()); + if (linkLook()->previewEnabled()) + m_linkDisplay.setLink(fileName, NoteFactory::iconForURL(url), linkLook(), note()->font()); // FIXME: move iconForURL outside of NoteFactory !!!!! + else + m_linkDisplay.setLink(fileName, NoteFactory::iconForURL(url), QPixmap(), linkLook(), note()->font()); + startFetchingUrlPreview(); + contentChanged(m_linkDisplay.minWidth()); +} + +void FileContent::linkLookChanged() +{ + fontChanged(); + //setFileName(fileName()); + //startFetchingUrlPreview(); +} + +void FileContent::newPreview(const KFileItem*, const QPixmap &preview) +{ + LinkLook *linkLook = this->linkLook(); + m_linkDisplay.setLink(fileName(), NoteFactory::iconForURL(KURL(fullPath())), (linkLook->previewEnabled() ? preview : QPixmap()), linkLook, note()->font()); + contentChanged(m_linkDisplay.minWidth()); +} + +void FileContent::removePreview(const KFileItem*) +{ + newPreview(0, QPixmap()); +} + +void FileContent::startFetchingUrlPreview() +{ + KURL url(fullPath()); + LinkLook *linkLook = this->linkLook(); + +// delete m_previewJob; + if (!url.isEmpty() && linkLook->previewSize() > 0) { + KURL filteredUrl = NoteFactory::filteredURL(url);//KURIFilter::self()->filteredURI(url); + KURL::List urlList; + urlList.append(filteredUrl); + m_previewJob = KIO::filePreview(urlList, linkLook->previewSize(), linkLook->previewSize(), linkLook->iconSize()); + connect( m_previewJob, SIGNAL(gotPreview(const KFileItem*, const QPixmap&)), this, SLOT(newPreview(const KFileItem*, const QPixmap&)) ); + connect( m_previewJob, SIGNAL(failed(const KFileItem*)), this, SLOT(removePreview(const KFileItem*)) ); + } +} + +void FileContent::exportToHTML(HTMLExporter *exporter, int indent) +{ + QString spaces; + QString fileName = exporter->copyFile(fullPath(), true); + exporter->stream << m_linkDisplay.toHtml(exporter, KURL(exporter->dataFolderName + fileName), "").replace("\n", "\n" + spaces.fill(' ', indent + 1)); +} + +/** class SoundContent: + */ + +SoundContent::SoundContent(Note *parent, const QString &fileName) + : FileContent(parent, fileName) +{ + setFileName(fileName); // FIXME: TO THAT HERE BECAUSE NoteContent() constructor seems to don't be able to call virtual methods??? +} + + +QString SoundContent::zoneTip(int zone) +{ + return (zone == Note::Custom0 ? i18n("Open this sound") : QString()); +} + +void SoundContent::setHoveredZone(int oldZone, int newZone) +{ +#ifdef WITHOUT_ARTS + Q_UNUSED(oldZone); + if (newZone == Note::Custom0 || newZone == Note::Content) + std::cout << "Compiled without aRts: sound is not played." << std::endl; +#else + static KArtsDispatcher *s_dispatcher = new KArtsDispatcher(); // Needed for s_playObj (we don't use it directly) + static KArtsServer *s_playServer = new KArtsServer(); + static KDE::PlayObjectFactory *s_playFactory = new KDE::PlayObjectFactory(s_playServer); + static KDE::PlayObject *s_playObj = 0; + + Q_UNUSED(s_dispatcher); // Avoid the compiler to tell us it is not used! + if (newZone == Note::Custom0 || newZone == Note::Content) { + // Start the sound preview: + if (oldZone != Note::Custom0 && oldZone != Note::Content) { // Don't restart if it was already in one of those zones + s_playObj = s_playFactory->createPlayObject(fullPath(), true); + s_playObj->play(); + } + } else { + // Stop the sound preview, if it was started: + if (s_playObj) { + s_playObj->halt(); + delete s_playObj; + s_playObj = 0; + } + } +#endif +} + + +QString SoundContent::messageWhenOpenning(OpenMessage where) +{ + switch (where) { + case OpenOne: return i18n("Opening sound..."); + case OpenSeveral: return i18n("Opening sounds..."); + case OpenOneWith: return i18n("Opening sound with..."); + case OpenSeveralWith: return i18n("Opening sounds with..."); + case OpenOneWithDialog: return i18n("Open sound with:"); + case OpenSeveralWithDialog: return i18n("Open sounds with:"); + default: return ""; + } +} + +/** class LinkContent: + */ + +LinkContent::LinkContent(Note *parent, const KURL &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon) + : NoteContent(parent), m_previewJob(0) +{ + setLink(url, title, icon, autoTitle, autoIcon); +} + +int LinkContent::setWidthAndGetHeight(int width) +{ + m_linkDisplay.setWidth(width); + return m_linkDisplay.height(); +} + +void LinkContent::paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered) +{ + m_linkDisplay.paint(painter, 0, 0, width, height, colorGroup, isDefaultColor, isSelected, isHovered, isHovered && note()->hoveredZone() == Note::Custom0); +} + +void LinkContent::saveToNode(QDomDocument &doc, QDomElement &content) +{ + content.setAttribute("title", title() ); + content.setAttribute("icon", icon() ); + content.setAttribute("autoTitle", (autoTitle() ? "true" : "false")); + content.setAttribute("autoIcon", (autoIcon() ? "true" : "false")); + QDomText textNode = doc.createTextNode(url().prettyURL()); + content.appendChild(textNode); +} + + +void LinkContent::toolTipInfos(QStringList *keys, QStringList *values) +{ + keys->append(i18n("Target")); + values->append(m_url.prettyURL()); +} + +int LinkContent::zoneAt(const QPoint &pos) +{ + return (m_linkDisplay.iconButtonAt(pos) ? 0 : Note::Custom0); +} + +QRect LinkContent::zoneRect(int zone, const QPoint &/*pos*/) +{ + QRect linkRect = m_linkDisplay.iconButtonRect(); + + if (zone == Note::Custom0) + return QRect(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() + else if (zone == Note::Content) + return linkRect; + else + return QRect(); +} + +QString LinkContent::zoneTip(int zone) +{ + return (zone == Note::Custom0 ? i18n("Open this link") : QString()); +} + +void LinkContent::setCursor(QWidget *widget, int zone) +{ + if (zone == Note::Custom0) + widget->setCursor(Qt::PointingHandCursor); +} + +QString LinkContent::statusBarMessage(int zone) +{ + if (zone == Note::Custom0 || zone == Note::Content) + return m_url.prettyURL(); + else + return ""; +} + + +KURL LinkContent::urlToOpen(bool /*with*/) +{ + return NoteFactory::filteredURL(url());//KURIFilter::self()->filteredURI(url()); +} + +QString LinkContent::messageWhenOpenning(OpenMessage where) +{ + if (url().isEmpty()) + return i18n("Link have no URL to open."); + + switch (where) { + case OpenOne: return i18n("Opening link target..."); + case OpenSeveral: return i18n("Opening link targets..."); + case OpenOneWith: return i18n("Opening link target with..."); + case OpenSeveralWith: return i18n("Opening link targets with..."); + case OpenOneWithDialog: return i18n("Open link target with:"); + case OpenSeveralWithDialog: return i18n("Open link targets with:"); + default: return ""; + } +} + +void LinkContent::setLink(const KURL &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon) +{ + m_autoTitle = autoTitle; + m_autoIcon = autoIcon; + m_url = NoteFactory::filteredURL(KURL(url));//KURIFilter::self()->filteredURI(url); + m_title = (autoTitle ? NoteFactory::titleForURL(m_url) : title); + m_icon = (autoIcon ? NoteFactory::iconForURL(m_url) : icon); + + LinkLook *look = LinkLook::lookForURL(m_url); + if (look->previewEnabled()) + m_linkDisplay.setLink(m_title, m_icon, look, note()->font()); + else + m_linkDisplay.setLink(m_title, m_icon, QPixmap(), look, note()->font()); + startFetchingUrlPreview(); + contentChanged(m_linkDisplay.minWidth()); +} + +void LinkContent::linkLookChanged() +{ + fontChanged(); +} + +void LinkContent::newPreview(const KFileItem*, const QPixmap &preview) +{ + LinkLook *linkLook = LinkLook::lookForURL(url()); + m_linkDisplay.setLink(title(), icon(), (linkLook->previewEnabled() ? preview : QPixmap()), linkLook, note()->font()); + contentChanged(m_linkDisplay.minWidth()); +} + +void LinkContent::removePreview(const KFileItem*) +{ + newPreview(0, QPixmap()); +} + +// Code dupicated from FileContent::startFetchingUrlPreview() +void LinkContent::startFetchingUrlPreview() +{ + KURL url = this->url(); + LinkLook *linkLook = LinkLook::lookForURL(this->url()); + +// delete m_previewJob; + if (!url.isEmpty() && linkLook->previewSize() > 0) { + KURL filteredUrl = NoteFactory::filteredURL(url);//KURIFilter::self()->filteredURI(url); + KURL::List urlList; + urlList.append(filteredUrl); + m_previewJob = KIO::filePreview(urlList, linkLook->previewSize(), linkLook->previewSize(), linkLook->iconSize()); + connect( m_previewJob, SIGNAL(gotPreview(const KFileItem*, const QPixmap&)), this, SLOT(newPreview(const KFileItem*, const QPixmap&)) ); + connect( m_previewJob, SIGNAL(failed(const KFileItem*)), this, SLOT(removePreview(const KFileItem*)) ); + } +} + +void LinkContent::exportToHTML(HTMLExporter *exporter, int indent) +{ + QString linkTitle = title(); + +// TODO: +// // Append address (useful for print version of the page/basket): +// if (exportData.formatForImpression && (!autoTitle() && title() != NoteFactory::titleForURL(url().prettyURL()))) { +// // The address is on a new line, unless title is empty (empty lines was replaced by ): +// if (linkTitle == " "/*" "*/) +// linkTitle = url().prettyURL()/*""*/; +// else +// linkTitle = linkTitle + " <" + url().prettyURL() + ">"/*+ "<br>"*/; +// //linkTitle += "<i>" + url().prettyURL() + "</i>"; +// } + + KURL linkURL; +/* + QFileInfo fInfo(url().path()); +// DEBUG_WIN << url().path() +// << "IsFile:" + QString::number(fInfo.isFile()) +// << "IsDir:" + QString::number(fInfo.isDir()); + if (exportData.embedLinkedFiles && fInfo.isFile()) { +// DEBUG_WIN << "Embed file"; + linkURL = exportData.dataFolderName + Basket::copyFile(url().path(), exportData.dataFolderPath, true); + } else if (exportData.embedLinkedFolders && fInfo.isDir()) { +// DEBUG_WIN << "Embed folder"; + linkURL = exportData.dataFolderName + Basket::copyFile(url().path(), exportData.dataFolderPath, true); + } else { +// DEBUG_WIN << "Embed LINK"; +*/ + linkURL = url(); +/* + } +*/ + + QString spaces; + exporter->stream << m_linkDisplay.toHtml(exporter, linkURL, linkTitle).replace("\n", "\n" + spaces.fill(' ', indent + 1)); +} + +/** class LauncherContent: + */ + +LauncherContent::LauncherContent(Note *parent, const QString &fileName) + : NoteContent(parent, fileName) +{ + basket()->addWatchedFile(fullPath()); + loadFromFile(/*lazyLoad=*/false); +} + +int LauncherContent::setWidthAndGetHeight(int width) +{ + m_linkDisplay.setWidth(width); + return m_linkDisplay.height(); +} + +void LauncherContent::paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered) +{ + m_linkDisplay.paint(painter, 0, 0, width, height, colorGroup, isDefaultColor, isSelected, isHovered, isHovered && note()->hoveredZone() == Note::Custom0); +} + +bool LauncherContent::loadFromFile(bool /*lazyLoad*/) // TODO: saveToFile() ?? Is it possible? +{ + DEBUG_WIN << "Loading LauncherContent From " + basket()->folderName() + fileName(); + KService service(fullPath()); + setLauncher(service.name(), service.icon(), service.exec()); + return true; +} + + +void LauncherContent::toolTipInfos(QStringList *keys, QStringList *values) +{ + KService service(fullPath()); + + QString exec = service.exec(); + if (service.terminal()) + exec = i18n("%1 <i>(run in terminal)</i>").arg(exec); + + if (!service.comment().isEmpty() && service.comment() != service.name()) { + keys->append(i18n("Comment")); + values->append(service.comment()); + } + + keys->append(i18n("Command")); + values->append(exec); +} + +int LauncherContent::zoneAt(const QPoint &pos) +{ + return (m_linkDisplay.iconButtonAt(pos) ? 0 : Note::Custom0); +} + +QRect LauncherContent::zoneRect(int zone, const QPoint &/*pos*/) +{ + QRect linkRect = m_linkDisplay.iconButtonRect(); + + if (zone == Note::Custom0) + return QRect(linkRect.width(), 0, note()->width(), note()->height()); // Too wide and height, but it will be clipped by Note::zoneRect() + else if (zone == Note::Content) + return linkRect; + else + return QRect(); +} + +QString LauncherContent::zoneTip(int zone) +{ + return (zone == Note::Custom0 ? i18n("Launch this application") : QString()); +} + +void LauncherContent::setCursor(QWidget *widget, int zone) +{ + if (zone == Note::Custom0) + widget->setCursor(Qt::PointingHandCursor); +} + + +KURL LauncherContent::urlToOpen(bool with) +{ + if (KService(fullPath()).exec().isEmpty()) + return KURL(); + + return (with ? KURL() : KURL(fullPath())); // Can open the appliation, but not with another application :-) +} + +QString LauncherContent::messageWhenOpenning(OpenMessage where) +{ + if (KService(fullPath()).exec().isEmpty()) + return i18n("The launcher have no command to run."); + + switch (where) { + case OpenOne: return i18n("Launching application..."); + case OpenSeveral: return i18n("Launching applications..."); + case OpenOneWith: + case OpenSeveralWith: + case OpenOneWithDialog: + case OpenSeveralWithDialog: // TODO: "Open this application with this file as parameter"? + default: return ""; + } +} + +void LauncherContent::setLauncher(const QString &name, const QString &icon, const QString &exec) +{ + m_name = name; + m_icon = icon; + m_exec = exec; + + m_linkDisplay.setLink(name, icon, LinkLook::launcherLook, note()->font()); + contentChanged(m_linkDisplay.minWidth()); +} + +void LauncherContent::exportToHTML(HTMLExporter *exporter, int indent) +{ + QString spaces; + QString fileName = exporter->copyFile(fullPath(), /*createIt=*/true); + exporter->stream << m_linkDisplay.toHtml(exporter, KURL(exporter->dataFolderName + fileName), "").replace("\n", "\n" + spaces.fill(' ', indent + 1)); +} + +/** class ColorContent: + */ + +const int ColorContent::RECT_MARGIN = 2; + +ColorContent::ColorContent(Note *parent, const QColor &color) + : NoteContent(parent) +{ + setColor(color); +} + +int ColorContent::setWidthAndGetHeight(int /*width*/) // We do not need width because we can't word-break, and width is always >= minWidth() +{ + // FIXME: Duplicate from setColor(): + QRect textRect = QFontMetrics(note()->font()).boundingRect(color().name()); + int rectHeight = (textRect.height() + 2)*3/2; + return rectHeight; +} + +void ColorContent::paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool /*isDefaultColor*/, bool /*isSelected*/, bool /*isHovered*/) +{ + // FIXME: Duplicate from setColor(): + QRect textRect = QFontMetrics(note()->font()).boundingRect(color().name()); + int rectHeight = (textRect.height() + 2)*3/2; + int rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. + + // FIXME: Duplicate from CommonColorSelector::drawColorRect: + // Fill: + painter->fillRect(1, 1, rectWidth - 2, rectHeight - 2, color()); + // Stroke: + QColor stroke = color().dark(125); + painter->setPen(stroke); + painter->drawLine(1, 0, rectWidth - 2, 0); + painter->drawLine(0, 1, 0, rectHeight - 2); + painter->drawLine(1, rectHeight - 1, rectWidth - 2, rectHeight - 1); + painter->drawLine(rectWidth - 1, 1, rectWidth - 1, rectHeight - 2); + // Round corners: + painter->setPen(Tools::mixColor(color(), stroke)); + painter->drawPoint(1, 1); + painter->drawPoint(1, rectHeight - 2); + painter->drawPoint(rectWidth - 2, rectHeight - 2); + painter->drawPoint(rectWidth - 2, 1); + + // Draw the text: + painter->setFont(note()->font()); + painter->setPen(colorGroup.text()); + painter->drawText(rectWidth + RECT_MARGIN, 0, width - rectWidth - RECT_MARGIN, height, Qt::AlignAuto | Qt::AlignVCenter, color().name()); +} + +void ColorContent::saveToNode(QDomDocument &doc, QDomElement &content) +{ + QDomText textNode = doc.createTextNode(color().name()); + content.appendChild(textNode); +} + + +void ColorContent::toolTipInfos(QStringList *keys, QStringList *values) +{ + int hue, saturation, value; + m_color.getHsv(hue, saturation, value); + + keys->append(i18n("RGB Colorspace: Red/Green/Blue", "RGB")); + values->append(i18n("<i>Red</i>: %1, <i>Green</i>: %2, <i>Blue</i>: %3,").arg(QString::number(m_color.red()), QString::number(m_color.green()), QString::number(m_color.blue()))); + + keys->append(i18n("HSV Colorspace: Hue/Saturation/Value", "HSV")); + values->append(i18n("<i>Hue</i>: %1, <i>Saturation</i>: %2, <i>Value</i>: %3,").arg(QString::number(hue), QString::number(saturation), QString::number(value))); + + static QString cssColors[] = { + "aqua", "00ffff", + "black", "000000", + "blue", "0000ff", + "fuchsia", "ff00ff", + "gray", "808080", + "green", "008000", + "lime", "00ff00", + "maroon", "800000", + "navy", "000080", + "olive", "808000", + "purple", "800080", + "red", "ff0000", + "silver", "c0c0c0", + "teal", "008080", + "white", "ffffff", + "yellow", "ffff00" + }; + + static QString cssExtendedColors[] = { + "aliceblue", "f0f8ff", + "antiquewhite", "faebd7", + "aquamarine", "7fffd4", + "azure", "f0ffff", + "beige", "f5f5dc", + "bisque", "ffe4c4", + "blanchedalmond", "ffebcd", + "blueviolet", "8a2be2", + "brown", "a52a2a", + "burlywood", "deb887", + "cadetblue", "5f9ea0", + "chartreuse", "7fff00", + "chocolate", "d2691e", + "coral", "ff7f50", + "cornflowerblue", "6495ed", + "cornsilk", "fff8dc", + "crimson", "dc1436", + "cyan", "00ffff", + "darkblue", "00008b", + "darkcyan", "008b8b", + "darkgoldenrod", "b8860b", + "darkgray", "a9a9a9", + "darkgreen", "006400", + "darkkhaki", "bdb76b", + "darkmagenta", "8b008b", + "darkolivegreen", "556b2f", + "darkorange", "ff8c00", + "darkorchid", "9932cc", + "darkred", "8b0000", + "darksalmon", "e9967a", + "darkseagreen", "8fbc8f", + "darkslateblue", "483d8b", + "darkslategray", "2f4f4f", + "darkturquoise", "00ced1", + "darkviolet", "9400d3", + "deeppink", "ff1493", + "deepskyblue", "00bfff", + "dimgray", "696969", + "dodgerblue", "1e90ff", + "firebrick", "b22222", + "floralwhite", "fffaf0", + "forestgreen", "228b22", + "gainsboro", "dcdcdc", + "ghostwhite", "f8f8ff", + "gold", "ffd700", + "goldenrod", "daa520", + "greenyellow", "adff2f", + "honeydew", "f0fff0", + "hotpink", "ff69b4", + "indianred", "cd5c5c", + "indigo", "4b0082", + "ivory", "fffff0", + "khaki", "f0e68c", + "lavender", "e6e6fa", + "lavenderblush", "fff0f5", + "lawngreen", "7cfc00", + "lemonchiffon", "fffacd", + "lightblue", "add8e6", + "lightcoral", "f08080", + "lightcyan", "e0ffff", + "lightgoldenrodyellow", "fafad2", + "lightgreen", "90ee90", + "lightgrey", "d3d3d3", + "lightpink", "ffb6c1", + "lightsalmon", "ffa07a", + "lightseagreen", "20b2aa", + "lightskyblue", "87cefa", + "lightslategray", "778899", + "lightsteelblue", "b0c4de", + "lightyellow", "ffffe0", + "limegreen", "32cd32", + "linen", "faf0e6", + "magenta", "ff00ff", + "mediumaquamarine", "66cdaa", + "mediumblue", "0000cd", + "mediumorchid", "ba55d3", + "mediumpurple", "9370db", + "mediumseagreen", "3cb371", + "mediumslateblue", "7b68ee", + "mediumspringgreen", "00fa9a", + "mediumturquoise", "48d1cc", + "mediumvioletred", "c71585", + "midnightblue", "191970", + "mintcream", "f5fffa", + "mistyrose", "ffe4e1", + "moccasin", "ffe4b5", + "navajowhite", "ffdead", + "oldlace", "fdf5e6", + "olivedrab", "6b8e23", + "orange", "ffa500", + "orangered", "ff4500", + "orchid", "da70d6", + "palegoldenrod", "eee8aa", + "palegreen", "98fb98", + "paleturquoise", "afeeee", + "palevioletred", "db7093", + "papayawhip", "ffefd5", + "peachpuff", "ffdab9", + "peru", "cd853f", + "pink", "ffc0cb", + "plum", "dda0dd", + "powderblue", "b0e0e6", + "rosybrown", "bc8f8f", + "royalblue", "4169e1", + "saddlebrown", "8b4513", + "salmon", "fa8072", + "sandybrown", "f4a460", + "seagreen", "2e8b57", + "seashell", "fff5ee", + "sienna", "a0522d", + "skyblue", "87ceeb", + "slateblue", "6a5acd", + "slategray", "708090", + "snow", "fffafa", + "springgreen", "00ff7f", + "steelblue", "4682b4", + "tan", "d2b48c", + "thistle", "d8bfd8", + "tomato", "ff6347", + "turquoise", "40e0d0", + "violet", "ee82ee", + "wheat", "f5deb3", + "whitesmoke", "f5f5f5", + "yellowgreen", "9acd32" + }; + + QString colorHex = color().name().mid(1); // Take the hexadecimal name of the color, without the '#'. + + bool cssColorFound = false; + for (int i = 0; i < 2*16; i += 2) { + if (colorHex == cssColors[i+1]) { + keys->append(i18n("CSS Color Name")); + values->append(cssColors[i]); + cssColorFound = true; + break; + } + } + + if (!cssColorFound) + for (int i = 0; i < 2*124; i += 2) { + if (colorHex == cssExtendedColors[i+1]) { + keys->append(i18n("CSS Extended Color Name")); + values->append(cssExtendedColors[i]); + break; + } + } + + keys->append(i18n("Is Web Color")); + values->append(Tools::isWebColor(color()) ? i18n("Yes") : i18n("No")); + +} + +void ColorContent::setColor(const QColor &color) +{ + m_color = color; + + QRect textRect = QFontMetrics(note()->font()).boundingRect(color.name()); + int rectHeight = (textRect.height() + 2)*3/2; + int rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. + contentChanged(rectWidth + RECT_MARGIN + textRect.width() + RECT_MARGIN); // The second RECT_MARGIN is because textRect.width() is too short. I done a bug? Can't figure out. +} + +void ColorContent::addAlternateDragObjects(KMultipleDrag *dragObject) +{ + dragObject->addDragObject( new QColorDrag(color()) ); + +// addDragObject(new KColorDrag( note->color(), 0 )); +// addDragObject(new QTextDrag( note->color().name(), 0 )); + +/* // Creata and add the QDragObject: + storedDrag = new QStoredDrag("application/x-color"); + storedDrag->setEncodedData(*array); + dragObject->addDragObject(storedDrag); + delete array;*/ +} + +void ColorContent::exportToHTML(HTMLExporter *exporter, int /*indent*/) +{ + // FIXME: Duplicate from setColor(): TODO: rectSize() + QRect textRect = QFontMetrics(note()->font()).boundingRect(color().name()); + int rectHeight = (textRect.height() + 2)*3/2; + int rectWidth = rectHeight * 14 / 10; // 1.4 times the height, like A4 papers. + + QString fileName = /*Tools::fileNameForNewFile(*/QString("color_%1.png").arg(color().name().lower().mid(1))/*, exportData.iconsFolderPath)*/; + QString fullPath = exporter->iconsFolderPath + fileName; + QPixmap colorIcon = KColorCombo2::colorRectPixmap(color(), /*isDefault=*/false, rectWidth, rectHeight); + colorIcon.save(fullPath, "PNG"); + QString iconHtml = QString("<img src=\"%1\" width=\"%2\" height=\"%3\" alt=\"\">") + .arg(exporter->iconsFolderName + fileName, QString::number(colorIcon.width()), QString::number(colorIcon.height())); + + exporter->stream << iconHtml + " " + color().name(); +} + + + +/** class UnknownContent: + */ + +const int UnknownContent::DECORATION_MARGIN = 2; + +UnknownContent::UnknownContent(Note *parent, const QString &fileName) + : NoteContent(parent, fileName) +{ + basket()->addWatchedFile(fullPath()); + loadFromFile(/*lazyLoad=*/false); +} + +int UnknownContent::setWidthAndGetHeight(int width) +{ + width -= 1; + QRect textRect = QFontMetrics(note()->font()).boundingRect(0, 0, width, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, m_mimeTypes); + return DECORATION_MARGIN + textRect.height() + DECORATION_MARGIN; +} + +// TODO: Move this function from note.cpp to class Tools: +extern void drawGradient( QPainter *p, const QColor &colorTop, const QColor & colorBottom, + int x, int y, int w, int h, + bool sunken, bool horz, bool flat ); /*const*/ + +void UnknownContent::paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool /*isDefaultColor*/, bool /*isSelected*/, bool /*isHovered*/) +{ + width -= 1; + painter->setPen(colorGroup.text()); + + // FIXME: Duplicate from ColorContent::paint() and CommonColorSelector::drawColorRect: + // Fill with gradient: + drawGradient(painter, colorGroup.background(), colorGroup.background().dark(110), 1, 1, width - 2, height - 2, /*sunken=*/false, /*horz=*/true, /*flat=*/false); + // Stroke: + QColor stroke = Tools::mixColor(colorGroup.background(), colorGroup.text()); + painter->setPen(stroke); + painter->drawLine(1, 0, width - 2, 0); + painter->drawLine(0, 1, 0, height - 2); + painter->drawLine(1, height - 1, width - 2, height - 1); + painter->drawLine(width - 1, 1, width - 1, height - 2); + // Round corners: + painter->setPen(Tools::mixColor(colorGroup.background(), stroke)); + painter->drawPoint(1, 1); + painter->drawPoint(1, height - 2); + painter->drawPoint(width - 2, height - 2); + painter->drawPoint(width - 2, 1); + + painter->setPen(colorGroup.text()); + painter->drawText(DECORATION_MARGIN, DECORATION_MARGIN, width - 2*DECORATION_MARGIN, height - 2*DECORATION_MARGIN, + Qt::AlignAuto | Qt::AlignVCenter | Qt::WordBreak, m_mimeTypes); +} + +bool UnknownContent::loadFromFile(bool /*lazyLoad*/) +{ + DEBUG_WIN << "Loading UnknownContent From " + basket()->folderName() + fileName(); + QFile file(fullPath()); + if (file.open(IO_ReadOnly)) { + QDataStream stream(&file); + QString line; + m_mimeTypes = ""; + // Get the MIME-types names: + do { + if (!stream.atEnd()) { + stream >> line; + if (!line.isEmpty()) { + if (m_mimeTypes.isEmpty()) + m_mimeTypes += line; + else + m_mimeTypes += QString("\n") + line; + } + } + } while (!line.isEmpty() && !stream.atEnd()); + file.close(); + } + + QRect textRect = QFontMetrics(note()->font()).boundingRect(0, 0, /*width=*/1, 500000, Qt::AlignAuto | Qt::AlignTop | Qt::WordBreak, m_mimeTypes); + contentChanged(DECORATION_MARGIN + textRect.width() + DECORATION_MARGIN + 1); + return true; +} + +void UnknownContent::addAlternateDragObjects(KMultipleDrag *dragObject) +{ + QFile file(fullPath()); + if (file.open(IO_ReadOnly)) { + QDataStream stream(&file); + // Get the MIME types names: + QValueList<QString> mimes; + QString line; + do { + if (!stream.atEnd()) { + stream >> line; + if (!line.isEmpty()) + mimes.append(line); + } + } while (!line.isEmpty() && !stream.atEnd()); + // Add the streams: + Q_UINT64 size; // TODO: It was Q_UINT32 in version 0.5.0 ! + QByteArray *array; + QStoredDrag *storedDrag; + for (uint i = 0; i < mimes.count(); ++i) { + // Get the size: + stream >> size; + // Allocate memory to retreive size bytes and store them: + array = new QByteArray(size); + stream.readRawBytes(array->data(), size); + // Creata and add the QDragObject: + storedDrag = new QStoredDrag(*(mimes.at(i))); + storedDrag->setEncodedData(*array); + dragObject->addDragObject(storedDrag); + delete array; // FIXME: Should we? + } + file.close(); + } +} + +void UnknownContent::exportToHTML(HTMLExporter *exporter, int indent) +{ + QString spaces; + exporter->stream << "<div class=\"unknown\">" << mimeTypes().replace("\n", "\n" + spaces.fill(' ', indent + 1 + 1)) << "</div>"; +} + + + + +void NoteFactory__loadNode(const QDomElement &content, const QString &lowerTypeName, Note *parent, bool lazyLoad) +{ + if (lowerTypeName == "text") new TextContent( parent, content.text(), lazyLoad ); + else if (lowerTypeName == "html") new HtmlContent( parent, content.text(), lazyLoad ); + else if (lowerTypeName == "image") new ImageContent( parent, content.text(), lazyLoad ); + else if (lowerTypeName == "animation") new AnimationContent( parent, content.text(), lazyLoad ); + else if (lowerTypeName == "sound") new SoundContent( parent, content.text() ); + else if (lowerTypeName == "file") new FileContent( parent, content.text() ); + else if (lowerTypeName == "link") { + bool autoTitle = content.attribute("title") == content.text(); + bool autoIcon = content.attribute("icon") == NoteFactory::iconForURL(KURL(content.text())); + autoTitle = XMLWork::trueOrFalse( content.attribute("autoTitle"), autoTitle); + autoIcon = XMLWork::trueOrFalse( content.attribute("autoIcon"), autoIcon ); + new LinkContent( parent, KURL(content.text()), content.attribute("title"), content.attribute("icon"), autoTitle, autoIcon ); + } else if (lowerTypeName == "launcher") new LauncherContent( parent, content.text() ); + else if (lowerTypeName == "color") new ColorContent( parent, QColor(content.text()) ); + else if (lowerTypeName == "unknown") new UnknownContent( parent, content.text() ); +} + +#include "notecontent.moc" diff --git a/src/notecontent.h b/src/notecontent.h new file mode 100644 index 0000000..08c3db4 --- /dev/null +++ b/src/notecontent.h @@ -0,0 +1,603 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef NOTECONTENT_H +#define NOTECONTENT_H + +#include <qobject.h> +#include <qstring.h> +#include <qsimplerichtext.h> +#include <qpixmap.h> +#include <qmovie.h> +#include <qcolor.h> +#include <kurl.h> + +#include "linklabel.h" + +class QDomDocument; +class QDomElement; +class QPainter; +class QWidget; +class QPoint; +class QRect; +class QStringList; +class KMultipleDrag; + +class KFileItem; +namespace KIO { class PreviewJob; } + +class Note; +class Basket; +class FilterData; +class HtmlExporter; + +/** A list of numeric identifier for each note type. + * Declare a varible with the type NoteType::Id and assign a value like NoteType::Text... + * @author S�astien Laot + */ +namespace NoteType +{ + enum Id { Group = 255, Text = 1, Html, Image, Animation, Sound, File, Link, Launcher, Color, Unknown }; // Always positive +} + +/** Abstract base class for every content type of basket note. + * It's a base class to represent those types: Text, Html, Image, Animation, Sound, File, Link, Launcher, Color, Unknown. + * @author S�astien Laot + */ +class NoteContent // TODO: Mark some methods as const! and some (like typeName() as static! +{ + public: + // Constructor and destructor: + NoteContent(Note *parent, const QString &fileName = ""); /// << Constructor. Inherited notes should call it to initialize the parent note. + virtual ~NoteContent() {} /// << Virtual destructor. Reimplement it if you should destroy some data your custom types. + // Simple Abstract Generic Methods: + virtual NoteType::Id type() = 0; /// << @return the internal number that identify that note type. + virtual QString typeName() = 0; /// << @return the translated type name to display in the user interface. + virtual QString lowerTypeName() = 0; /// << @return the type name in lowercase without space, for eg. saving. + virtual QString toText(const QString &cuttedFullPath); /// << @return a plain text equivalent of the content. + virtual QString toHtml(const QString &imageName, const QString &cuttedFullPath) = 0; /// << @return an HTML text equivalent of the content. @param imageName Save image in this Qt ressource. + virtual QPixmap toPixmap() { return QPixmap(); } /// << @return an image equivalent of the content. + virtual void toLink(KURL *url, QString *title, const QString &cuttedFullPath); /// << Set the link to the content. By default, it set them to fullPath() if useFile(). + virtual bool useFile() = 0; /// << @return true if it use a file to store the content. + virtual bool canBeSavedAs() = 0; /// << @return true if the content can be saved as a file by the user. + virtual QString saveAsFilters() = 0; /// << @return the filters for the user to choose a file destination to save the note as. + virtual bool match(const FilterData &data) = 0; /// << @return true if the content match the filter criterias. + // Complexe Abstract Generic Methods: + virtual void exportToHTML(HTMLExporter *exporter, int indent) = 0; /// << Export the note in an HTML file. + virtual QString cssClass() = 0; /// << @return the CSS class of the note when exported to HTML + virtual int setWidthAndGetHeight(int width) = 0; /// << Relayout content with @p width (never less than minWidth()). @return its new height. + virtual void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered) = 0; /// << Paint the content on @p painter, at coordinate (0, 0) and with the size (@p width, @p height). + virtual bool loadFromFile(bool /*lazyLoad*/) { return false; } /// << Load the content from the file. The default implementation does nothing. @see fileName(). + virtual bool finishLazyLoad() { return false; } /// << Load what was not loaded by loadFromFile() if it was lazy-loaded + virtual bool saveToFile() { return false; } /// << Save the content to the file. The default implementation does nothing. @see fileName(). + virtual QString linkAt(const QPoint &/*pos*/) { return ""; } /// << @return the link anchor at position @p pos or "" if there is no link. + virtual void saveToNode(QDomDocument &doc, QDomElement &content); /// << Save the note in the basket XML file. By default it store the filename if a file is used. + virtual void fontChanged() = 0; /// << If your content display textual data, called when the font have changed (from tags or basket font) + virtual void linkLookChanged() {} /// << If your content use LinkDisplay with preview enabled, reload the preview (can have changed size) + virtual QString editToolTipText() = 0; /// << @return "Edit this [text|image|...]" to put in the tooltip for the note's content zone. + virtual void toolTipInfos(QStringList */*keys*/, QStringList */*values*/) {} /// << Get "key: value" couples to put in the tooltip for the note's content zone. + // Custom Zones: /// Implement this if you want to store custom data. + virtual int zoneAt(const QPoint &/*pos*/) { return 0; } /// << If your note-type have custom zones, @return the zone at @p pos or 0 if it's not a custom zone! + virtual QRect zoneRect(int zone, const QPoint &/*pos*/); /// << Idem, @return the rect of the custom zone + virtual QString zoneTip(int /*zone*/) { return ""; } /// << Idem, @return the toolTip of the custom zone + virtual void setCursor(QWidget */*widget*/, int /*zone*/) {} /// << Idem, set the mouse cursor for widget @p widget when it is over zone @p zone! + virtual void setHoveredZone(int /*oldZone*/, int /*newZone*/) {} /// << If your note type need some feedback, you get notified of hovering changes here. + virtual QString statusBarMessage(int /*zone*/) { return ""; } /// << @return the statusBar message to show for zone @p zone, or "" if nothing special have to be said. + // Drag and Drop Content: + virtual void serialize(QDataStream &/*stream*/) {} /// << Serialize the content in a QDragObject. If it consists of a file, it can be serialized for you. + virtual bool shouldSerializeFile() { return useFile(); } /// << @return true if the dragging process should serialize the filename (and move the file if cutting). + virtual void addAlternateDragObjects(KMultipleDrag*/*dragObj*/) {} /// << If you offer more than toText/Html/Image/Link(), this will be called if this is the only selected. + virtual QPixmap feedbackPixmap(int width, int height) = 0; /// << @return the pixmap to put under the cursor while dragging this object. + virtual bool needSpaceForFeedbackPixmap() { return false; } /// << @return true if a space must be inserted before and after the DND feedback pixmap. + // Content Edition: + virtual int xEditorIndent() { return 0; } /// << If the editor should be indented (eg. to not cover an icon), return the number of pixels. + // Open Content or File: + virtual KURL urlToOpen(bool /*with*/); /// << @return the URL to open the note, or an invalid KURL if it's not openable. If @p with if false, it's a normal "Open". If it's true, it's for an "Open with..." action. The default implementation return the fullPath() if the note useFile() and nothing if not. + enum OpenMessage { + OpenOne, /// << Message to send to the statusbar when opening this note. + OpenSeveral, /// << Message to send to the statusbar when opening several notes of this type. + OpenOneWith, /// << Message to send to the statusbar when doing "Open With..." on this note. + OpenSeveralWith, /// << Message to send to the statusbar when doing "Open With..." several notes of this type. + OpenOneWithDialog, /// << Prompt-message of the "Open With..." dialog for this note. + OpenSeveralWithDialog /// << Prompt-message of the "Open With..." dialog for several notes of this type. + }; + virtual QString messageWhenOpenning(OpenMessage /*where*/) { return QString(); } /// << @return the message to display according to @p where or nothing if it can't be done. @see OpenMessage describing the nature of the message that should be returned... The default implementation return an empty string. NOTE: If urlToOpen() is invalid and messageWhenOpenning() is not empty, then the user will be prompted to edit the note (with the message returned by messageWhenOpenning()) for eg. being able to edit URL of a link if it's empty when opening it... + virtual QString customOpenCommand() { return QString(); } /// << Reimplement this if your urlToOpen() should be opened with another application instead of the default KDE one. This choice should be left to the users in the setting (choice to use a custom app or not, and which app). + // Common File Management: /// (and do save changes) and optionnaly hide the toolbar. + virtual void setFileName(const QString &fileName); /// << Set the filename. Reimplement it if you eg. want to update the view when the filename is changed. + bool trySetFileName(const QString &fileName); /// << Set the new filename and return true. Can fail and return false if a file with this fileName already exists. + QString fullPath(); /// << Get the absolute path of the file where this content is stored on disk. + QString fileName() { return m_fileName; } /// << Get the file name where this content is stored (relative to the basket folder). @see fullPath(). + int minWidth() { return m_minWidth; } /// << Get the minimum width for this content. + Note *note() { return m_note; } /// << Get the note managing this content. + Basket *basket(); /// << Get the basket containing the note managing this content. + public: + void setEdited(); /// << Mark the note as edited NOW: change the "last modification time and time" AND save the basket to XML file. + protected: + void contentChanged(int newMinWidth); /// << When the content has changed, inherited classes should call this to specify its new minimum size and trigger a basket relayout. + private: + Note *m_note; + QString m_fileName; + int m_minWidth; + public: + static const int FEEDBACK_DARKING; +}; + +/** Real implementation of plain text notes: + * @author S�astien Laot + */ +class TextContent : public NoteContent +{ + public: + // Constructor and destructor: + TextContent(Note *parent, const QString &fileName, bool lazyLoad = false); + ~TextContent(); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toText(const QString &/*cuttedFullPath*/); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + bool loadFromFile(bool lazyLoad); + bool finishLazyLoad(); + bool saveToFile(); + QString linkAt(const QPoint &pos); + void fontChanged(); + QString editToolTipText(); + // Drag and Drop Content: + QPixmap feedbackPixmap(int width, int height); + // Open Content or File: + QString messageWhenOpenning(OpenMessage where); +// QString customOpenCommand(); + // Content-Specific Methods: + void setText(const QString &text, bool lazyLoad = false); /// << Change the text note-content and relayout the note. + QString text() { return m_text; } /// << @return the text note-content. + protected: + QString m_text; + QSimpleRichText *m_simpleRichText; +}; + +/** Real implementation of rich text (HTML) notes: + * @author S�astien Laot + */ +class HtmlContent : public NoteContent +{ + public: + // Constructor and destructor: + HtmlContent(Note *parent, const QString &fileName, bool lazyLoad = false); + ~HtmlContent(); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toText(const QString &/*cuttedFullPath*/); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + bool loadFromFile(bool lazyLoad); + bool finishLazyLoad(); + bool saveToFile(); + QString linkAt(const QPoint &pos); + void fontChanged(); + QString editToolTipText(); + // Drag and Drop Content: + QPixmap feedbackPixmap(int width, int height); + // Open Content or File: + QString messageWhenOpenning(OpenMessage where); + QString customOpenCommand(); + // Content-Specific Methods: + void setHtml(const QString &html, bool lazyLoad = false); /// << Change the HTML note-content and relayout the note. + QString html() { return m_html; } /// << @return the HTML note-content. + protected: + QString m_html; + QString m_textEquivalent; //OPTIM_FILTER + QSimpleRichText *m_simpleRichText; +}; + +/** Real implementation of image notes: + * @author S�astien Laot + */ +class ImageContent : public NoteContent +{ + public: + // Constructor and destructor: + ImageContent(Note *parent, const QString &fileName, bool lazyLoad = false); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + QPixmap toPixmap(); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + bool loadFromFile(bool lazyLoad); + bool finishLazyLoad(); + bool saveToFile(); + void fontChanged(); + QString editToolTipText(); + void toolTipInfos(QStringList *keys, QStringList *values); + // Drag and Drop Content: + QPixmap feedbackPixmap(int width, int height); + bool needSpaceForFeedbackPixmap() { return true; } + // Open Content or File: + QString messageWhenOpenning(OpenMessage where); + QString customOpenCommand(); + // Content-Specific Methods: + void setPixmap(const QPixmap &pixmap); /// << Change the pixmap note-content and relayout the note. + QPixmap pixmap() { return m_pixmap; } /// << @return the pixmap note-content. + protected: + QPixmap m_pixmap; + char *m_format; +}; + +/** Real implementation of animated image (GIF, MNG) notes: + * @author S�astien Laot + */ +class AnimationContent : public QObject, public NoteContent // QObject to be able to receive QMovie signals +{ + Q_OBJECT + public: + // Constructor and destructor: + AnimationContent(Note *parent, const QString &fileName, bool lazyLoad = false); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + QPixmap toPixmap(); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + void fontChanged(); + QString editToolTipText(); + // Drag and Drop Content: + QPixmap feedbackPixmap(int width, int height); + bool needSpaceForFeedbackPixmap() { return true; } + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + bool loadFromFile(bool lazyLoad); + bool finishLazyLoad(); + bool saveToFile(); + // Open Content or File: + QString messageWhenOpenning(OpenMessage where); + QString customOpenCommand(); + // Content-Specific Methods: + bool setMovie(const QMovie &movie); /// << Change the movie note-content and relayout the note. + QMovie movie() { return m_movie; } /// << @return the movie note-content. + protected slots: + void movieUpdated(const QRect&); + void movieResized(const QSize&); + void movieStatus(int status); + protected: + QMovie m_movie; + int m_oldStatus; + static int INVALID_STATUS; +}; + +/** Real implementation of file notes: + * @author S�astien Laot + */ +class FileContent : public QObject, public NoteContent +{ + Q_OBJECT + public: + // Constructor and destructor: + FileContent(Note *parent, const QString &fileName); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + bool loadFromFile(bool /*lazyLoad*/); + void fontChanged(); + void linkLookChanged(); + QString editToolTipText(); + void toolTipInfos(QStringList *keys, QStringList *values); + // Drag and Drop Content: + QPixmap feedbackPixmap(int width, int height); + // Custom Zones: + int zoneAt(const QPoint &pos); + QRect zoneRect(int zone, const QPoint &/*pos*/); + QString zoneTip(int zone); + void setCursor(QWidget *widget, int zone); + // Content Edition: + int xEditorIndent(); + // Open Content or File: + QString messageWhenOpenning(OpenMessage where); + // Content-Specific Methods: + void setFileName(const QString &fileName); /// << Reimplemented to be able to relayout the note. + virtual LinkLook* linkLook() { return LinkLook::fileLook; } + protected: + LinkDisplay m_linkDisplay; + // File Preview Management: + protected slots: + void newPreview(const KFileItem*, const QPixmap &preview); + void removePreview(const KFileItem*); + void startFetchingUrlPreview(); + protected: + KIO::PreviewJob *m_previewJob; +}; + +/** Real implementation of sound notes: + * @author S�astien Laot + */ +class SoundContent : public FileContent // A sound is a file with just a bit different user interaction +{ + Q_OBJECT + public: + // Constructor and destructor: + SoundContent(Note *parent, const QString &fileName); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + QString editToolTipText(); + // Complexe Generic Methods: + QString cssClass(); + // Custom Zones: + QString zoneTip(int zone); + void setHoveredZone(int oldZone, int newZone); + // Open Content or File: + QString messageWhenOpenning(OpenMessage where); + QString customOpenCommand(); + // Content-Specific Methods: + LinkLook* linkLook() { return LinkLook::soundLook; } +}; + +/** Real implementation of link notes: + * @author S�astien Laot + */ +class LinkContent : public QObject, public NoteContent +{ + Q_OBJECT + public: + // Constructor and destructor: + LinkContent(Note *parent, const KURL &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toText(const QString &/*cuttedFullPath*/); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + void toLink(KURL *url, QString *title, const QString &cuttedFullPath); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + void saveToNode(QDomDocument &doc, QDomElement &content); + void fontChanged(); + void linkLookChanged(); + QString editToolTipText(); + void toolTipInfos(QStringList *keys, QStringList *values); + // Drag and Drop Content: + void serialize(QDataStream &stream); + QPixmap feedbackPixmap(int width, int height); + // Custom Zones: + int zoneAt(const QPoint &pos); + QRect zoneRect(int zone, const QPoint &/*pos*/); + QString zoneTip(int zone); + void setCursor(QWidget *widget, int zone); + QString statusBarMessage(int zone); + // Open Content or File: + KURL urlToOpen(bool /*with*/); + QString messageWhenOpenning(OpenMessage where); + // Content-Specific Methods: + void setLink(const KURL &url, const QString &title, const QString &icon, bool autoTitle, bool autoIcon); /// << Change the link and relayout the note. + KURL url() { return m_url; } /// << @return the URL of the link note-content. + QString title() { return m_title; } /// << @return the displayed title of the link note-content. + QString icon() { return m_icon; } /// << @return the displayed icon of the link note-content. + bool autoTitle() { return m_autoTitle; } /// << @return if the title is auto-computed from the URL. + bool autoIcon() { return m_autoIcon; } /// << @return if the icon is auto-computed from the URL. + protected: + KURL m_url; + QString m_title; + QString m_icon; + bool m_autoTitle; + bool m_autoIcon; + LinkDisplay m_linkDisplay; + // File Preview Management: + protected slots: + void newPreview(const KFileItem*, const QPixmap &preview); + void removePreview(const KFileItem*); + void startFetchingUrlPreview(); + protected: + KIO::PreviewJob *m_previewJob; +}; + +/** Real implementation of launcher notes: + * @author S�astien Laot + */ +class LauncherContent : public NoteContent +{ + public: + // Constructor and destructor: + LauncherContent(Note *parent, const QString &fileName); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + void toLink(KURL *url, QString *title, const QString &cuttedFullPath); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + bool loadFromFile(bool /*lazyLoad*/); + void fontChanged(); + QString editToolTipText(); + void toolTipInfos(QStringList *keys, QStringList *values); + // Drag and Drop Content: + QPixmap feedbackPixmap(int width, int height); + // Custom Zones: + int zoneAt(const QPoint &pos); + QRect zoneRect(int zone, const QPoint &/*pos*/); + QString zoneTip(int zone); + void setCursor(QWidget *widget, int zone); + // Open Content or File: + KURL urlToOpen(bool with); + QString messageWhenOpenning(OpenMessage where); + // Content-Specific Methods: + void setLauncher(const QString &name, const QString &icon, const QString &exec); /// << Change the launcher note-content and relayout the note. Normally called by loadFromFile (no save done). + QString name() { return m_name; } /// << @return the URL of the launcher note-content. + QString icon() { return m_icon; } /// << @return the displayed icon of the launcher note-content. + QString exec() { return m_exec; } /// << @return the execute command line of the launcher note-content. + // TODO: KService *service() ??? And store everything in thta service ? + protected: + QString m_name; // TODO: Store them in linkDisplay to gain place (idem for Link notes) + QString m_icon; + QString m_exec; + LinkDisplay m_linkDisplay; +}; + +/** Real implementation of color notes: + * @author S�astien Laot + */ +class ColorContent : public NoteContent +{ + public: + // Constructor and destructor: + ColorContent(Note *parent, const QColor &color); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toText(const QString &/*cuttedFullPath*/); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + void saveToNode(QDomDocument &doc, QDomElement &content); + void fontChanged(); + QString editToolTipText(); + void toolTipInfos(QStringList *keys, QStringList *values); + // Drag and Drop Content: + void serialize(QDataStream &stream); + QPixmap feedbackPixmap(int width, int height); + bool needSpaceForFeedbackPixmap() { return true; } + void addAlternateDragObjects(KMultipleDrag *dragObject); + // Content-Specific Methods: + void setColor(const QColor &color); /// << Change the color note-content and relayout the note. + QColor color() { return m_color; } /// << @return the color note-content. + protected: + QColor m_color; + static const int RECT_MARGIN; +}; + +/** Real implementation of unknown MIME-types dropped notes: + * @author S�astien Laot + */ +class UnknownContent : public NoteContent +{ + public: + // Constructor and destructor: + UnknownContent(Note *parent, const QString &fileName); + // Simple Generic Methods: + NoteType::Id type(); + QString typeName(); + QString lowerTypeName(); + QString toText(const QString &/*cuttedFullPath*/); + QString toHtml(const QString &imageName, const QString &cuttedFullPath); + void toLink(KURL *url, QString *title, const QString &cuttedFullPath); + bool useFile(); + bool canBeSavedAs(); + QString saveAsFilters(); + bool match(const FilterData &data); + // Complexe Generic Methods: + void exportToHTML(HTMLExporter *exporter, int indent); + QString cssClass(); + int setWidthAndGetHeight(int width); + void paint(QPainter *painter, int width, int height, const QColorGroup &colorGroup, bool isDefaultColor, bool isSelected, bool isHovered); + bool loadFromFile(bool /*lazyLoad*/); + void fontChanged(); + QString editToolTipText(); + // Drag and Drop Content: + bool shouldSerializeFile() { return false; } + void addAlternateDragObjects(KMultipleDrag *dragObject); + QPixmap feedbackPixmap(int width, int height); + bool needSpaceForFeedbackPixmap() { return true; } + // Open Content or File: + KURL urlToOpen(bool /*with*/) { return KURL(); } + // Content-Specific Methods: + QString mimeTypes() { return m_mimeTypes; } /// << @return the list of MIME types this note-content contains. + protected: + QString m_mimeTypes; + static const int DECORATION_MARGIN; +}; + +void NoteFactory__loadNode(const QDomElement &content, const QString &lowerTypeName, Note *parent, bool lazyLoad); + +#endif // NOTECONTENT_H diff --git a/src/notedrag.cpp b/src/notedrag.cpp new file mode 100644 index 0000000..633dec2 --- /dev/null +++ b/src/notedrag.cpp @@ -0,0 +1,593 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qdragobject.h> +#include <qdir.h> +#include <qpainter.h> +#include <qtextcodec.h> +#include <qbuffer.h> +#include <kurldrag.h> +#include <kdeversion.h> +#include <kapplication.h> +#include <qdesktopwidget.h> + +#include "basket.h" +#include "notedrag.h" +#include "notefactory.h" +#include "tools.h" +#include "global.h" + +#include <iostream> + +/** NoteDrag */ + +const char * NoteDrag::NOTE_MIME_STRING = "application/x-basket-note"; + +void NoteDrag::createAndEmptyCuttingTmpFolder() +{ + Tools::deleteRecursively(Global::tempCutFolder()); + QDir dir; + dir.mkdir(Global::tempCutFolder()); +} + +QDragObject* NoteDrag::dragObject(NoteSelection *noteList, bool cutting, QWidget *source) +{ + if (noteList->count() <= 0) + return 0; + + // The MimeSource: + KMultipleDrag *multipleDrag = new KMultipleDrag(source); + + // Make sure the temporary folder exists and is empty (we delete previously moved file(s) (if exists) + // since we override the content of the clipboard and previous file willn't be accessable anymore): + createAndEmptyCuttingTmpFolder(); + + // The "Native Format" Serialization: + QBuffer buffer; + if (buffer.open(IO_WriteOnly)) { + QDataStream stream(&buffer); + // First append a pointer to the basket: + stream << (Q_UINT64)(noteList->firstStacked()->note->basket()); + // Then a list of pointers to all notes, and parent groups: + for (NoteSelection *node = noteList->firstStacked(); node; node = node->nextStacked()) + stream << (Q_UINT64)(node->note); + QValueList<Note*> groups = noteList->parentGroups(); + for (QValueList<Note*>::iterator it = groups.begin(); it != groups.end(); ++it) + stream << (Q_UINT64)(*it); + stream << (Q_UINT64)0; + // And finally the notes themselves: + serializeNotes(noteList, stream, cutting); + // Append the object: + buffer.close(); + QStoredDrag *dragObject = new QStoredDrag(NOTE_MIME_STRING, source); + dragObject->setEncodedData(buffer.buffer()); + multipleDrag->addDragObject(dragObject); + } + + // The "Other Flavours" Serialization: + serializeText( noteList, multipleDrag ); + serializeHtml( noteList, multipleDrag ); + serializeImage( noteList, multipleDrag ); + serializeLinks( noteList, multipleDrag, cutting ); + + // The Alternate Flavours: + if (noteList->count() == 1) + noteList->firstStacked()->note->content()->addAlternateDragObjects(multipleDrag); + + // If it is a drag, and not a copy/cut, add the feedback pixmap: + if (source) + setFeedbackPixmap(noteList, multipleDrag); + + return multipleDrag; +} + +void NoteDrag::serializeNotes(NoteSelection *noteList, QDataStream &stream, bool cutting) +{ + for (NoteSelection *node = noteList; node; node = node->next) { + stream << (Q_UINT64)(node->note); + if (node->firstChild) { + stream << (Q_UINT64)(NoteType::Group) << (Q_UINT64)(node->note->groupWidth()) << (Q_UINT64)(node->note->isFolded()); + serializeNotes(node->firstChild, stream, cutting); + } else { + NoteContent *content = node->note->content(); + stream << (Q_UINT64)(content->type()) << (Q_UINT64)(node->note->groupWidth()); + // Serialize file name, and move the file to a temporary place if the note is to be cuttted. + // If note does not have file name, we append empty string to be able to easily decode the notes later: + stream << content->fileName(); + if (content->shouldSerializeFile()) { + if (cutting) { + // Move file in a temporary place: + QString fullPath = Global::tempCutFolder() + Tools::fileNameForNewFile(content->fileName(), Global::tempCutFolder()); + KIO::move(KURL(content->fullPath()), KURL(fullPath), /*showProgressInfo=*/false); + node->fullPath = fullPath; + stream << fullPath; + } else + stream << content->fullPath(); + } else + stream << QString(""); + stream << content->note()->addedDate() << content->note()->lastModificationDate(); + content->serialize(stream); + State::List states = node->note->states(); + for (State::List::Iterator it = states.begin(); it != states.end(); ++it) + stream << (Q_UINT64)(*it); + stream << (Q_UINT64)0; + } + } + stream << (Q_UINT64)0; // Mark the end of the notes in this group/hierarchy. +} + +void NoteDrag::serializeText(NoteSelection *noteList, KMultipleDrag *multipleDrag) +{ + QString textEquivalent; + QString text; + for (NoteSelection *node = noteList->firstStacked(); node; node = node->nextStacked()) { + text = node->note->toText(node->fullPath); // note->toText() and not note->content()->toText() because the first one will also export the tags as text. + if (!text.isEmpty()) + textEquivalent += (!textEquivalent.isEmpty() ? "\n" : "") + text; + } + if (!textEquivalent.isEmpty()) + multipleDrag->addDragObject( new QTextDrag(textEquivalent) ); +} + +void NoteDrag::serializeHtml(NoteSelection *noteList, KMultipleDrag *multipleDrag) +{ + QString htmlEquivalent; + QString html; + for (NoteSelection *node = noteList->firstStacked(); node; node = node->nextStacked()) { + html = node->note->content()->toHtml("", node->fullPath); + if (!html.isEmpty()) + htmlEquivalent += (!htmlEquivalent.isEmpty() ? "<br>\n" : "") + html; + } + if (!htmlEquivalent.isEmpty()) { + // Add HTML flavour: + QTextDrag *htmlDrag = new QTextDrag(htmlEquivalent); + htmlDrag->setSubtype("html"); + multipleDrag->addDragObject(htmlDrag); + // But also QTextEdit flavour, to be able to paste several notes to a text edit: + QByteArray byteArray = ("<!--StartFragment--><p>" + htmlEquivalent).local8Bit(); + QStoredDrag *richTextDrag = new QStoredDrag("application/x-qrichtext"); + richTextDrag->setEncodedData(byteArray); + multipleDrag->addDragObject(richTextDrag); + } +} + +void NoteDrag::serializeImage(NoteSelection *noteList, KMultipleDrag *multipleDrag) +{ + QValueList<QPixmap> pixmaps; + QPixmap pixmap; + for (NoteSelection *node = noteList->firstStacked(); node; node = node->nextStacked()) { + pixmap = node->note->content()->toPixmap(); + if (!pixmap.isNull()) + pixmaps.append(pixmap); + } + if (!pixmaps.isEmpty()) { + QPixmap pixmapEquivalent; + if (pixmaps.count() == 1) + pixmapEquivalent = pixmaps[0]; + else { + // Search the total size: + int height = 0; + int width = 0; + for (QValueList<QPixmap>::iterator it = pixmaps.begin(); it != pixmaps.end(); ++it) { + height += (*it).height(); + if ((*it).width() > width) + width = (*it).width(); + } + // Create the image by painting all image into one big image: + pixmapEquivalent.resize(width, height); + pixmapEquivalent.fill(Qt::white); + QPainter painter(&pixmapEquivalent); + height = 0; + for (QValueList<QPixmap>::iterator it = pixmaps.begin(); it != pixmaps.end(); ++it) { + painter.drawPixmap(0, height, *it); + height += (*it).height(); + } + } + QImageDrag *imageDrag = new QImageDrag(pixmapEquivalent.convertToImage()); + multipleDrag->addDragObject(imageDrag); + } +} + +void NoteDrag::serializeLinks(NoteSelection *noteList, KMultipleDrag *multipleDrag, bool cutting) +{ + KURL::List urls; + QStringList titles; + KURL url; + QString title; + for (NoteSelection *node = noteList->firstStacked(); node; node = node->nextStacked()) { + node->note->content()->toLink(&url, &title, node->fullPath); + if (!url.isEmpty()) { + urls.append(url); + titles.append(title); + } + } + if (!urls.isEmpty()) { + // First, the standard text/uri-list MIME format: +#if KDE_IS_VERSION( 3, 3, 90 ) + KURLDrag *urlsDrag = new KURLDrag(urls); + // ONLY export as text/uri-list, and not as text/plain* as we wil do that better ourself + urlsDrag->setExportAsText(false); + multipleDrag->addDragObject(urlsDrag); +#else + KURLDrag2 *urlsDrag = new KURLDrag2(urls); + QByteArray byteArray = urlsDrag->encodedData2("text/uri-list"); + QStoredDrag *uriListDrag = new QStoredDrag("text/uri-list"); + uriListDrag->setEncodedData(byteArray); + multipleDrag->addDragObject(uriListDrag); + delete urlsDrag; +#endif + // Then, also provide it in the Mozilla proprietary format (that also allow to add titles to URLs): + // A version for Mozilla applications (convert to "theUrl\ntheTitle", into UTF-16): + // FIXME: Does Mozilla support the drag of several URLs at once? + // FIXME: If no, only provide that if theire is only ONE URL. + QString xMozUrl; + for (uint i = 0; i < urls.count(); ++i) + xMozUrl += (xMozUrl.isEmpty() ? "" : "\n") + urls[i].prettyURL() + "\n" + titles[i]; +/* Code for only one: =============== + xMozUrl = note->title() + "\n" + note->url().prettyURL();*/ + QByteArray baMozUrl; + QTextStream stream(baMozUrl, IO_WriteOnly); + stream.setEncoding(QTextStream::RawUnicode); // It's UTF16 (aka UCS2), but with the first two order bytes + stream << xMozUrl; + QStoredDrag *xMozUrlDrag = new QStoredDrag("text/x-moz-url"); + xMozUrlDrag->setEncodedData(baMozUrl); + multipleDrag->addDragObject(xMozUrlDrag); + + if (cutting) { + QByteArray arrayCut(2); + QStoredDrag *storedDragCut = new QStoredDrag("application/x-kde-cutselection"); + arrayCut[0] = '1'; + arrayCut[1] = 0; + storedDragCut->setEncodedData(arrayCut); + multipleDrag->addDragObject(storedDragCut); + } + } +} + +void NoteDrag::setFeedbackPixmap(NoteSelection *noteList, KMultipleDrag *multipleDrag) +{ + QPixmap pixmap = feedbackPixmap(noteList); + if (!pixmap.isNull()) + multipleDrag->setPixmap(pixmap, QPoint(-8, -8)); +} + +QPixmap NoteDrag::feedbackPixmap(NoteSelection *noteList) +{ + if (noteList == 0) + return QPixmap(); + + static const int MARGIN = 2; + static const int SPACING = 1; + + QColor textColor = noteList->firstStacked()->note->basket()->textColor(); + QColor backgroundColor = noteList->firstStacked()->note->basket()->backgroundColor().dark(NoteContent::FEEDBACK_DARKING); + + QValueList<QPixmap> pixmaps; + QValueList<QColor> backgrounds; + QValueList<bool> spaces; + QPixmap pixmap; + int height = 0; + int width = 0; + int i = 0; + bool elipsisImage = false; + QColor bgColor; + bool needSpace; + for (NoteSelection *node = noteList->firstStacked(); node; node = node->nextStacked(), ++i) { + if (elipsisImage) { + pixmap = QPixmap(7, 2); + pixmap.fill(backgroundColor); + QPainter painter(&pixmap); + painter.setPen(textColor); + painter.drawPoint(1, 1); + painter.drawPoint(3, 1); + painter.drawPoint(5, 1); + painter.end(); + bgColor = node->note->basket()->backgroundColor(); + needSpace = false; + } else { + pixmap = node->note->content()->feedbackPixmap(/*maxWidth=*/kapp->desktop()->width() / 2, /*maxHeight=*/96); + bgColor = node->note->backgroundColor(); + needSpace = node->note->content()->needSpaceForFeedbackPixmap(); + } + if (!pixmap.isNull()) { + if (pixmap.width() > width) + width = pixmap.width(); + pixmaps.append(pixmap); + backgrounds.append(bgColor); + spaces.append(needSpace); + height += (i > 0 && needSpace ? 1 : 0) + pixmap.height() + SPACING + (needSpace ? 1 : 0); + if (elipsisImage) + break; + if (height > kapp->desktop()->height() / 2) + elipsisImage = true; + } + } + if (!pixmaps.isEmpty()) { + QPixmap result(MARGIN + width + MARGIN, MARGIN + height - SPACING + MARGIN - (spaces.last() ? 1 : 0)); + QPainter painter(&result); + // Draw all the images: + height = MARGIN; + QValueList<QPixmap>::iterator it; + QValueList<QColor>::iterator it2; + QValueList<bool>::iterator it3; + int i = 0; + for (it = pixmaps.begin(), it2 = backgrounds.begin(), it3 = spaces.begin(); it != pixmaps.end(); ++it, ++it2, ++it3, ++i) { + if (i != 0 && (*it3)) { + painter.fillRect(MARGIN, height, result.width() - 2 * MARGIN, SPACING, (*it2).dark(NoteContent::FEEDBACK_DARKING)); + ++height; + } + painter.drawPixmap(MARGIN, height, *it); + if ((*it).width() < width) + painter.fillRect(MARGIN + (*it).width(), height, width - (*it).width(), (*it).height(), (*it2).dark(NoteContent::FEEDBACK_DARKING)); + if (*it3) { + painter.fillRect(MARGIN, height + (*it).height(), result.width() - 2 * MARGIN, SPACING, (*it2).dark(NoteContent::FEEDBACK_DARKING)); + ++height; + } + painter.fillRect(MARGIN, height + (*it).height(), result.width() - 2 * MARGIN, SPACING, Tools::mixColor(textColor, backgroundColor)); + height += (*it).height() + SPACING; + } + // Draw the border: + painter.setPen(textColor); + painter.drawLine(0, 0, result.width() - 1, 0); + painter.drawLine(0, 0, 0, result.height() - 1); + painter.drawLine(0, result.height() - 1, result.width() - 1, result.height() - 1); + painter.drawLine(result.width() - 1, 0, result.width() - 1, result.height() - 1); + // Draw the "lightly rounded" border: + painter.setPen(Tools::mixColor(textColor, backgroundColor)); + painter.drawPoint(0, 0); + painter.drawPoint(0, result.height() - 1); + painter.drawPoint(result.width() - 1, result.height() - 1); + painter.drawPoint(result.width() - 1, 0); + // Draw the background in the margin (the inside will be painted above, anyway): + painter.setPen(backgroundColor); + painter.drawLine(1, 1, result.width() - 2, 1); + painter.drawLine(1, 1, 1, result.height() - 2); + painter.drawLine(1, result.height() - 2, result.width() - 2, result.height() - 2); + painter.drawLine(result.width() - 2, 1, result.width() - 2, result.height() - 2); + // And assign the feedback pixmap to the drag object: + //multipleDrag->setPixmap(result, QPoint(-8, -8)); + return result; + } + return QPixmap(); +} + +bool NoteDrag::canDecode(QMimeSource *source) +{ + return source->provides(NOTE_MIME_STRING); +} + +Basket* NoteDrag::basketOf(QMimeSource *source) +{ + QBuffer buffer(source->encodedData(NOTE_MIME_STRING)); + if (buffer.open(IO_ReadOnly)) { + QDataStream stream(&buffer); + // Get the parent basket: + Q_UINT64 basketPointer; + stream >> (Q_UINT64&)basketPointer; + return (Basket*)basketPointer; + } else + return 0; +} + +QValueList<Note*> NoteDrag::notesOf(QMimeSource *source) +{ + QBuffer buffer(source->encodedData(NOTE_MIME_STRING)); + if (buffer.open(IO_ReadOnly)) { + QDataStream stream(&buffer); + // Get the parent basket: + Q_UINT64 basketPointer; + stream >> (Q_UINT64&)basketPointer; + // Get the note list: + Q_UINT64 notePointer; + QValueList<Note*> notes; + do { + stream >> notePointer; + if (notePointer != 0) + notes.append((Note*)notePointer); + } while (notePointer); + // Done: + return notes; + } else + return QValueList<Note*>(); +} + +Note* NoteDrag::decode(QMimeSource *source, Basket *parent, bool moveFiles, bool moveNotes) +{ + QBuffer buffer(source->encodedData(NOTE_MIME_STRING)); + if (buffer.open(IO_ReadOnly)) { + QDataStream stream(&buffer); + // Get the parent basket: + Q_UINT64 basketPointer; + stream >> (Q_UINT64&)basketPointer; + Basket *basket = (Basket*)basketPointer; + // Get the note list: + Q_UINT64 notePointer; + QValueList<Note*> notes; + do { + stream >> notePointer; + if (notePointer != 0) + notes.append((Note*)notePointer); + } while (notePointer); + // Decode the note hierarchy: + Note *hierarchy = decodeHierarchy(stream, parent, moveFiles, moveNotes, basket); + // In case we moved notes from one basket to another, save the source basket where notes were removed: + basket->filterAgainDelayed(); // Delayed, because if a note is moved to the same basket, the note is not at its + basket->save(); // new position yet, and the call to ensureNoteVisible would make the interface flicker!! + return hierarchy; + } else + return 0; +} + +Note* NoteDrag::decodeHierarchy(QDataStream &stream, Basket *parent, bool moveFiles, bool moveNotes, Basket *originalBasket) +{ + Q_UINT64 notePointer; + Q_UINT64 type; + QString fileName; + QString fullPath; + QDateTime addedDate; + QDateTime lastModificationDate; + + Note *firstNote = 0; // TODO: class NoteTreeChunk + Note *lastInserted = 0; + + do { + stream >> notePointer; + if (notePointer == 0) + return firstNote; + Note *oldNote = (Note*)notePointer; + + Note *note = 0; + Q_UINT64 groupWidth; + stream >> type >> groupWidth; + if (type == NoteType::Group) { + note = new Note(parent); + note->setGroupWidth(groupWidth); + Q_UINT64 isFolded; + stream >> isFolded; + if (isFolded) + note->toggleFolded(/*animate=*/false); + if (moveNotes) { + note->setX(oldNote->x()); // We don't move groups but re-create them (every childs can to not be selected) + note->setY(oldNote->y()); // We just set the position of the copied group so the animation seems as if the group is the same as (or a copy of) the old. + note->setHeight(oldNote->height()); // Idem: the only use of Note::setHeight() + } + Note* childs = decodeHierarchy(stream, parent, moveFiles, moveNotes, originalBasket); + if (childs) { + for (Note *n = childs; n; n = n->next()) + n->setParentNote(note); + note->setFirstChild(childs); + } + } else { + stream >> fileName >> fullPath >> addedDate >> lastModificationDate; + if (moveNotes) { + originalBasket->unplugNote(oldNote); + note = oldNote; + if (note->basket() != parent) { + QString newFileName = NoteFactory::createFileForNewNote(parent, "", fileName); + note->content()->setFileName(newFileName); + KIO::FileCopyJob *copyJob = KIO::file_move(KURL(fullPath), KURL(parent->fullPath() + newFileName), + /*perms=*/-1, /*override=*/true, /*resume=*/false, /*showProgressInfo=*/false); + parent->connect( copyJob, SIGNAL(result(KIO::Job *)), + parent, SLOT(slotCopyingDone2(KIO::Job *)) ); + } + note->setGroupWidth(groupWidth); + note->setParentNote(0); + note->setPrev(0); + note->setNext(0); + note->setParentBasket(parent); + NoteFactory::consumeContent(stream, (NoteType::Id)type); + } else if ( (note = NoteFactory::decodeContent(stream, (NoteType::Id)type, parent)) ) { + note->setGroupWidth(groupWidth); + note->setAddedDate(addedDate); + note->setLastModificationDate(lastModificationDate); + } else if (!fileName.isEmpty()) { + // Here we are CREATING a new EMPTY file, so the name is RESERVED + // (while dropping several files at once a filename cannot be used by two of them). + // Later on, file_copy/file_move will copy/move the file to the new location. + QString newFileName = NoteFactory::createFileForNewNote(parent, "", fileName); + note = NoteFactory::loadFile(newFileName, (NoteType::Id)type, parent); + KIO::FileCopyJob *copyJob; + if (moveFiles) + copyJob = KIO::file_move(KURL(fullPath), KURL(parent->fullPath() + newFileName), + /*perms=*/-1, /*override=*/true, /*resume=*/false, /*showProgressInfo=*/false); + else + copyJob = KIO::file_copy(KURL(fullPath), KURL(parent->fullPath() + newFileName), + /*perms=*/-1, /*override=*/true, /*resume=*/false, /*showProgressInfo=*/false); + parent->connect( copyJob, SIGNAL(result(KIO::Job *)), + parent, SLOT(slotCopyingDone2(KIO::Job *)) ); + note->setGroupWidth(groupWidth); + note->setAddedDate(addedDate); + note->setLastModificationDate(lastModificationDate); + } + } + // Retreive the states (tags) and assign them to the note: + if (note && note->content()) { + Q_UINT64 statePointer; + do { + stream >> statePointer; + if (statePointer) + note->addState((State*)statePointer); + } while (statePointer); + } + // Now that we have created the note, insert it: + if (note) { + if (!firstNote) + firstNote = note; + else { + lastInserted->setNext(note); + note->setPrev(lastInserted); + } + lastInserted = note; + } + } while (true); + + // We've done: return! + return firstNote; +} + +/** ExtendedTextDrag */ + +bool ExtendedTextDrag::decode(const QMimeSource *e, QString &str) +{ + QCString subtype("plain"); + return decode(e, str, subtype); +} + +bool ExtendedTextDrag::decode(const QMimeSource *e, QString &str, QCString &subtype) +{ + // Get the string: + bool ok = QTextDrag::decode(e, str, subtype); + + // Test if it was a UTF-16 string (from eg. Mozilla): + if (str.length() >= 2) { + if ((str[0] == 0xFF && str[1] == 0xFE) || (str[0] == 0xFE && str[1] == 0xFF)) { + QByteArray utf16 = e->encodedData(QString("text/" + subtype).local8Bit()); + str = QTextCodec::codecForName("utf16")->toUnicode(utf16); + return true; + } + } + + // Test if it was empty (sometimes, from GNOME or Mozilla) + if (str.length() == 0 && subtype == "plain") { + if (e->provides("UTF8_STRING")) { + QByteArray utf8 = e->encodedData("UTF8_STRING"); + str = QTextCodec::codecForName("utf8")->toUnicode(utf8); + return true; + } + if (e->provides("text/unicode")) { // FIXME: It's UTF-16 without order bytes!!! + QByteArray utf16 = e->encodedData("text/unicode"); + str = QTextCodec::codecForName("utf16")->toUnicode(utf16); + return true; + } + if (e->provides("TEXT")) { // local encoding + QByteArray text = e->encodedData("TEXT"); + str = QString(text); + return true; + } + if (e->provides("COMPOUND_TEXT")) { // local encoding + QByteArray text = e->encodedData("COMPOUND_TEXT"); + str = QString(text); + return true; + } + } + return ok; +} + +#include "notedrag.moc" diff --git a/src/notedrag.h b/src/notedrag.h new file mode 100644 index 0000000..6dd9c62 --- /dev/null +++ b/src/notedrag.h @@ -0,0 +1,103 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef NOTEDRAG_H +#define NOTEDRAG_H + +#include <qstring.h> +#include <qdragobject.h> +#include <qdatastream.h> +#include <qpixmap.h> +#include <qvaluelist.h> +#include <kmultipledrag.h> + +class QDataStream; + +class Basket; +class Note; +class NoteSelection; + +/** Dragging/Copying/Cutting Scenario: + * - User select some notes and cut them; + * - NoteDrag::toMultipleDrag() is called with a tree of the selected notes (see Basket::toSelectionTree()): + * - This method create a new KMultipleDrag object, create a stream, + * - And then browse all notes and call the virtual Note::serialize() with the stream as parameter for them to serialize theire content in the "native format". + * - This give the MIME type "application/x-basket-note" that will be used by the application to paste the notes exactly as they were. + * - Then the method try to set alterante formats for the dragged objects: + * - It call successively toText() for each notes and stack up the result so theire is ONE big text flavour to add to the KMultipleDrag object + * - It do the same with toHtml(), toImage() and toLink() to have those flavours as well, if possible... + * - If there is only ONE copied note, addAlternateDragObjects() is called on it, so that Unknown objects can be dragged "as is". + * - It's OK for the flavours. The method finally set the drag feedback pixmap by asking every selected notes to draw the content to a small pixmap. + * - The pixmaps are joined to one big pixmap (but it should not exceed a defined size) and a border is drawn on this image. + * + * Pasting/Dropping Scenario: + * + * @author S�bastien Lao�t + */ +class NoteDrag +{ + protected: + static void serializeNotes( NoteSelection *noteList, QDataStream &stream, bool cutting ); + static void serializeText( NoteSelection *noteList, KMultipleDrag *multipleDrag ); + static void serializeHtml( NoteSelection *noteList, KMultipleDrag *multipleDrag ); + static void serializeImage( NoteSelection *noteList, KMultipleDrag *multipleDrag ); + static void serializeLinks( NoteSelection *noteList, KMultipleDrag *multipleDrag, bool cutting ); + static void setFeedbackPixmap( NoteSelection *noteList, KMultipleDrag *multipleDrag ); + static Note* decodeHierarchy(QDataStream &stream, Basket *parent, bool moveFiles, bool moveNotes, Basket *originalBasket); + public: + static QPixmap feedbackPixmap(NoteSelection *noteList); + static QDragObject* dragObject(NoteSelection *noteList, bool cutting, QWidget *source = 0); + static bool canDecode(QMimeSource *source); + static Note* decode(QMimeSource *source, Basket *parent, bool moveFiles, bool moveNotes); + static Basket* basketOf(QMimeSource *source); + static QValueList<Note*> notesOf(QMimeSource *source); + static void createAndEmptyCuttingTmpFolder(); + + static const char *NOTE_MIME_STRING; +}; + +/** QTextDrag with capabilities to drop GNOME and Mozilla texts + * as well as UTF-16 texts even if it was supposed to be encoded + * with local encoding! + * @author S�bastien Lao�t + */ +class ExtendedTextDrag : public QTextDrag +{ + Q_OBJECT + public: + static bool decode(const QMimeSource *e, QString &str); + static bool decode(const QMimeSource *e, QString &str, QCString &subtype); +}; + +// Support KDE 3.3 and older PROTECTED KURLDrag::encodedData()! + +#include <kurldrag.h> +class KURLDrag2 : public KURLDrag +{ + Q_OBJECT + public: + KURLDrag2(const KURL::List &urls) : KURLDrag(urls) {} + QByteArray encodedData2(const char *mime) const + { + return encodedData(mime); + } +}; + +#endif // NOTEDRAG_H diff --git a/src/noteedit.cpp b/src/noteedit.cpp new file mode 100644 index 0000000..b2d25dd --- /dev/null +++ b/src/noteedit.cpp @@ -0,0 +1,916 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qlabel.h> +#include <qlayout.h> +#include <qlineedit.h> +#include <klineedit.h> +#include <kurlrequester.h> +#include <kicondialog.h> +#include <kcolordialog.h> +#include <kservice.h> +#include <kconfig.h> +#include <kmessagebox.h> +#include <klocale.h> +#include <kmainwindow.h> +#include <ktoolbar.h> +#include <kaction.h> +#include <kurifilter.h> +#include <kdebug.h> +#include <kstdaction.h> +#include <kglobalsettings.h> + +#include "noteedit.h" +#include "notecontent.h" + // Use Tools:: +#include "notefactory.h" +#include "note.h" +#include "basket.h" +#include "settings.h" +#include "tools.h" +#include "variouswidgets.h" +#include "focusedwidgets.h" + +#include <iostream> + +/** class NoteEditor: */ + +NoteEditor::NoteEditor(NoteContent *noteContent) +{ + m_isEmpty = false; + m_canceled = false; + m_widget = 0; + m_textEdit = 0; + m_lineEdit = 0; + m_noteContent = noteContent; +} + +Note* NoteEditor::note() +{ + return m_noteContent->note(); +} + +NoteEditor* NoteEditor::editNoteContent(NoteContent *noteContent, QWidget *parent) +{ + TextContent *textContent = dynamic_cast<TextContent*>(noteContent); + if (textContent) + return new TextEditor(textContent, parent); + + HtmlContent *htmlContent = dynamic_cast<HtmlContent*>(noteContent); + if (htmlContent) + return new HtmlEditor(htmlContent, parent); + + ImageContent *imageContent = dynamic_cast<ImageContent*>(noteContent); + if (imageContent) + return new ImageEditor(imageContent, parent); + + AnimationContent *animationContent = dynamic_cast<AnimationContent*>(noteContent); + if (animationContent) + return new AnimationEditor(animationContent, parent); + + FileContent *fileContent = dynamic_cast<FileContent*>(noteContent); // Same for SoundContent + if (fileContent) + return new FileEditor(fileContent, parent); + + LinkContent *linkContent = dynamic_cast<LinkContent*>(noteContent); + if (linkContent) + return new LinkEditor(linkContent, parent); + + LauncherContent *launcherContent = dynamic_cast<LauncherContent*>(noteContent); + if (launcherContent) + return new LauncherEditor(launcherContent, parent); + + ColorContent *colorContent = dynamic_cast<ColorContent*>(noteContent); + if (colorContent) + return new ColorEditor(colorContent, parent); + + UnknownContent *unknownContent = dynamic_cast<UnknownContent*>(noteContent); + if (unknownContent) + return new UnknownEditor(unknownContent, parent); + + return 0; +} + +void NoteEditor::setInlineEditor(QWidget *inlineEditor) +{ + m_widget = inlineEditor; + m_textEdit = 0; + m_lineEdit = 0; + + KTextEdit *textEdit = dynamic_cast<KTextEdit*>(inlineEditor); + if (textEdit) + m_textEdit = textEdit; + else { + QLineEdit *lineEdit = dynamic_cast<QLineEdit*>(inlineEditor); + if (lineEdit) + m_lineEdit = lineEdit; + } +} + +#include <iostream> + +/** class TextEditor: */ + +TextEditor::TextEditor(TextContent *textContent, QWidget *parent) + : NoteEditor(textContent), m_textContent(textContent) +{ + FocusedTextEdit *textEdit = new FocusedTextEdit(/*disableUpdatesOnKeyPress=*/true, parent); + textEdit->setLineWidth(0); + textEdit->setMidLineWidth(0); + textEdit->setTextFormat(Qt::PlainText); + textEdit->setPaletteBackgroundColor(note()->backgroundColor()); + textEdit->setPaletteForegroundColor(note()->textColor()); + textEdit->setFont(note()->font()); + textEdit->setHScrollBarMode(QScrollView::AlwaysOff); + if (Settings::spellCheckTextNotes()) + textEdit->setCheckSpellingEnabled(true); + textEdit->setText(m_textContent->text()); + textEdit->moveCursor(KTextEdit::MoveEnd, false); // FIXME: Sometimes, the cursor flicker at ends before being positionned where clicked (because kapp->processEvents() I think) + textEdit->verticalScrollBar()->setCursor(Qt::ArrowCursor); + setInlineEditor(textEdit); + connect( textEdit, SIGNAL(escapePressed()), this, SIGNAL(askValidation()) ); + connect( textEdit, SIGNAL(mouseEntered()), this, SIGNAL(mouseEnteredEditorWidget()) ); + + connect( textEdit, SIGNAL(cursorPositionChanged(int, int)), textContent->note()->basket(), SLOT(editorCursorPositionChanged()) ); + // In case it is a very big note, the top is displayed and Enter is pressed: the cursor is on bottom, we should enure it visible: + QTimer::singleShot( 0, textContent->note()->basket(), SLOT(editorCursorPositionChanged()) ); +} + +TextEditor::~TextEditor() +{ + delete widget(); // TODO: delete that in validate(), so we can remove one method +} + +void TextEditor::autoSave(bool toFileToo) +{ + bool autoSpellCheck = true; + if (toFileToo) { + if (Settings::spellCheckTextNotes() != textEdit()->checkSpellingEnabled()) { + Settings::setSpellCheckTextNotes(textEdit()->checkSpellingEnabled()); + Settings::saveConfig(); + } + + autoSpellCheck = textEdit()->checkSpellingEnabled(); + textEdit()->setCheckSpellingEnabled(false); + } + + m_textContent->setText(textEdit()->text()); + + if (toFileToo) { + m_textContent->saveToFile(); + m_textContent->setEdited(); + textEdit()->setCheckSpellingEnabled(autoSpellCheck); + } +} + +void TextEditor::validate() +{ + if (Settings::spellCheckTextNotes() != textEdit()->checkSpellingEnabled()) { + Settings::setSpellCheckTextNotes(textEdit()->checkSpellingEnabled()); + Settings::saveConfig(); + } + + textEdit()->setCheckSpellingEnabled(false); + if (textEdit()->text().isEmpty()) + setEmpty(); + m_textContent->setText(textEdit()->text()); + m_textContent->saveToFile(); + m_textContent->setEdited(); + +// delete widget(); +} + +/** class HtmlEditor: */ + +HtmlEditor::HtmlEditor(HtmlContent *htmlContent, QWidget *parent) + : NoteEditor(htmlContent), m_htmlContent(htmlContent) +{ + FocusedTextEdit *textEdit = new FocusedTextEdit(/*disableUpdatesOnKeyPress=*/true, parent); + textEdit->setLineWidth(0); + textEdit->setMidLineWidth(0); + textEdit->setTextFormat(Qt::RichText); + textEdit->setAutoFormatting(Settings::autoBullet() ? QTextEdit::AutoAll : QTextEdit::AutoNone); + textEdit->setPaletteBackgroundColor(note()->backgroundColor()); + textEdit->setPaletteForegroundColor(note()->textColor()); + textEdit->setFont(note()->font()); + textEdit->setHScrollBarMode(QScrollView::AlwaysOff); + textEdit->setText(m_htmlContent->html()); + textEdit->moveCursor(KTextEdit::MoveEnd, false); + textEdit->verticalScrollBar()->setCursor(Qt::ArrowCursor); + setInlineEditor(textEdit); + + connect( textEdit, SIGNAL(mouseEntered()), this, SIGNAL(mouseEnteredEditorWidget()) ); + connect( textEdit, SIGNAL(escapePressed()), this, SIGNAL(askValidation()) ); + + connect( InlineEditors::instance()->richTextFont, SIGNAL(textChanged(const QString&)), textEdit, SLOT(setFamily(const QString&)) ); + connect( InlineEditors::instance()->richTextFontSize, SIGNAL(sizeChanged(int)), textEdit, SLOT(setPointSize(int)) ); + connect( InlineEditors::instance()->richTextColor, SIGNAL(activated(const QColor&)), textEdit, SLOT(setColor(const QColor&)) ); + + connect( InlineEditors::instance()->richTextFont, SIGNAL(escapePressed()), textEdit, SLOT(setFocus()) ); + connect( InlineEditors::instance()->richTextFont, SIGNAL(returnPressed2()), textEdit, SLOT(setFocus()) ); + connect( InlineEditors::instance()->richTextFont, SIGNAL(activated(int)), textEdit, SLOT(setFocus()) ); + + connect( InlineEditors::instance()->richTextFontSize, SIGNAL(escapePressed()), textEdit, SLOT(setFocus()) ); + connect( InlineEditors::instance()->richTextFontSize, SIGNAL(returnPressed2()), textEdit, SLOT(setFocus()) ); + connect( InlineEditors::instance()->richTextFontSize, SIGNAL(activated(int)), textEdit, SLOT(setFocus()) ); + + connect( InlineEditors::instance()->richTextColor, SIGNAL(escapePressed()), textEdit, SLOT(setFocus()) ); + connect( InlineEditors::instance()->richTextColor, SIGNAL(returnPressed2()), textEdit, SLOT(setFocus()) ); + + connect( textEdit, SIGNAL(cursorPositionChanged(int, int)), this, SLOT(cursorPositionChanged()) ); + connect( textEdit, SIGNAL(clicked(int, int)), this, SLOT(cursorPositionChanged()) ); + connect( textEdit, SIGNAL(currentFontChanged(const QFont&)), this, SLOT(fontChanged(const QFont&)) ); +// connect( textEdit, SIGNAL(currentVerticalAlignmentChanged(VerticalAlignment)), this, SLOT(slotVerticalAlignmentChanged()) ); + + connect( InlineEditors::instance()->richTextBold, SIGNAL(toggled(bool)), textEdit, SLOT(setBold(bool)) ); + connect( InlineEditors::instance()->richTextItalic, SIGNAL(toggled(bool)), textEdit, SLOT(setItalic(bool)) ); + connect( InlineEditors::instance()->richTextUnderline, SIGNAL(toggled(bool)), textEdit, SLOT(setUnderline(bool)) ); + //REMOVE: + //connect( InlineEditors::instance()->richTextBold, SIGNAL(activated()), this, SLOT(setBold()) ); + //connect( InlineEditors::instance()->richTextItalic, SIGNAL(activated()), this, SLOT(setItalic()) ); + //connect( InlineEditors::instance()->richTextUnderline, SIGNAL(activated()), this, SLOT(setUnderline()) ); + connect( InlineEditors::instance()->richTextLeft, SIGNAL(activated()), this, SLOT(setLeft()) ); + connect( InlineEditors::instance()->richTextCenter, SIGNAL(activated()), this, SLOT(setCentered()) ); + connect( InlineEditors::instance()->richTextRight, SIGNAL(activated()), this, SLOT(setRight()) ); + connect( InlineEditors::instance()->richTextJustified, SIGNAL(activated()), this, SLOT(setBlock()) ); + +// InlineEditors::instance()->richTextToolBar()->show(); + cursorPositionChanged(); + fontChanged(textEdit->currentFont()); + //QTimer::singleShot( 0, this, SLOT(cursorPositionChanged()) ); + InlineEditors::instance()->enableRichTextToolBar(); + + connect( InlineEditors::instance()->richTextUndo, SIGNAL(activated()), textEdit, SLOT(undo()) ); + connect( InlineEditors::instance()->richTextRedo, SIGNAL(activated()), textEdit, SLOT(redo()) ); + connect( textEdit, SIGNAL(undoAvailable(bool)), InlineEditors::instance()->richTextUndo, SLOT(setEnabled(bool)) ); + connect( textEdit, SIGNAL(redoAvailable(bool)), InlineEditors::instance()->richTextRedo, SLOT(setEnabled(bool)) ); + connect( textEdit, SIGNAL(textChanged()), this, SLOT(textChanged())); + InlineEditors::instance()->richTextUndo->setEnabled(false); + InlineEditors::instance()->richTextRedo->setEnabled(false); + + connect( textEdit, SIGNAL(cursorPositionChanged(int, int)), htmlContent->note()->basket(), SLOT(editorCursorPositionChanged()) ); + // In case it is a very big note, the top is displayed and Enter is pressed: the cursor is on bottom, we should enure it visible: + QTimer::singleShot( 0, htmlContent->note()->basket(), SLOT(editorCursorPositionChanged()) ); +} + +void HtmlEditor::cursorPositionChanged() +{ + InlineEditors::instance()->richTextFont->setCurrentFont( textEdit()->currentFont().family() ); + if (InlineEditors::instance()->richTextColor->color() != textEdit()->color()) + InlineEditors::instance()->richTextColor->setColor( textEdit()->color() ); + InlineEditors::instance()->richTextBold->setChecked( textEdit()->bold() ); + InlineEditors::instance()->richTextItalic->setChecked( textEdit()->italic() ); + InlineEditors::instance()->richTextUnderline->setChecked( textEdit()->underline() ); + + switch (textEdit()->alignment()) { + default: + case 1/*Qt::AlignLeft*/: InlineEditors::instance()->richTextLeft->setChecked(true); break; + case 4/*Qt::AlignCenter*/: InlineEditors::instance()->richTextCenter->setChecked(true); break; + case 2/*Qt::AlignRight*/: InlineEditors::instance()->richTextRight->setChecked(true); break; + case -8/*Qt::AlignJustify*/: InlineEditors::instance()->richTextJustified->setChecked(true); break; + } +} + +void HtmlEditor::textChanged() +{ + // The following is a workaround for an apparent Qt bug. + // When I start typing in a textEdit, the undo&redo actions are not enabled until I click + // or move the cursor - probably, the signal undoAvailable() is not emitted. + // So, I had to intervene and do that manually. + InlineEditors::instance()->richTextUndo->setEnabled(textEdit()->isUndoAvailable()); + InlineEditors::instance()->richTextRedo->setEnabled(textEdit()->isRedoAvailable()); +} + +void HtmlEditor::fontChanged(const QFont &font) +{ + InlineEditors::instance()->richTextFontSize->setFontSize(font.pointSize()); +} + +/*void HtmlEditor::slotVe<rticalAlignmentChanged(QTextEdit::VerticalAlignment align) +{ + QTextEdit::VerticalAlignment align = textEdit() + switch (align) { + case KTextEdit::AlignSuperScript: + InlineEditors::instance()->richTextSuper->setChecked(true); + InlineEditors::instance()->richTextSub->setChecked(false); + break; + case KTextEdit::AlignSubScript: + InlineEditors::instance()->richTextSuper->setChecked(false); + InlineEditors::instance()->richTextSub->setChecked(true); + break; + default: + InlineEditors::instance()->richTextSuper->setChecked(false); + InlineEditors::instance()->richTextSub->setChecked(false); + } + + NoteHtmlEditor::buttonToggled(int id) : + case 106: + if (isOn && m_toolbar->isButtonOn(107)) + m_toolbar->setButton(107, false); + m_text->setVerticalAlignment(isOn ? KTextEdit::AlignSuperScript : KTextEdit::AlignNormal); + break; + case 107: + if (isOn && m_toolbar->isButtonOn(106)) + m_toolbar->setButton(106, false); + m_text->setVerticalAlignment(isOn ? KTextEdit::AlignSubScript : KTextEdit::AlignNormal); + break; +}*/ + + // REMOVE: These functions are unused - it's now done by direct connection to textEdit + //void HtmlEditor::setBold() { textEdit()->setBold( InlineEditors::instance()->richTextBold->isChecked() ); } + //void HtmlEditor::setItalic() { textEdit()->setItalic( InlineEditors::instance()->richTextItalic->isChecked() ); } + //void HtmlEditor::setUnderline() { textEdit()->setUnderline( InlineEditors::instance()->richTextUnderline->isChecked() ); } +void HtmlEditor::setLeft() { textEdit()->setAlignment(Qt::AlignLeft); } +void HtmlEditor::setCentered() { textEdit()->setAlignment(Qt::AlignCenter); } +void HtmlEditor::setRight() { textEdit()->setAlignment(Qt::AlignRight); } +void HtmlEditor::setBlock() { textEdit()->setAlignment(Qt::AlignJustify); } + +HtmlEditor::~HtmlEditor() +{ + delete widget(); +} + +void HtmlEditor::autoSave(bool toFileToo) +{ + m_htmlContent->setHtml(textEdit()->text()); + if (toFileToo) { + m_htmlContent->saveToFile(); + m_htmlContent->setEdited(); + } +} + +void HtmlEditor::validate() +{ + if (Tools::htmlToText(textEdit()->text()).isEmpty()) + setEmpty(); + m_htmlContent->setHtml(textEdit()->text()); + m_htmlContent->saveToFile(); + m_htmlContent->setEdited(); + + disconnect(); + widget()->disconnect(); + if (InlineEditors::instance()) + { + InlineEditors::instance()->disableRichTextToolBar(); +// if (InlineEditors::instance()->richTextToolBar()) +// InlineEditors::instance()->richTextToolBar()->hide(); + } + delete widget(); + setInlineEditor(0); +} + +/** class ImageEditor: */ + +ImageEditor::ImageEditor(ImageContent *imageContent, QWidget *parent) + : NoteEditor(imageContent) +{ + int choice = KMessageBox::questionYesNo(parent, i18n( + "Images can not be edited here at the moment (the next version of BasKet Note Pads will include an image editor).\n" + "Do you want to open it with an application that understand it?"), + i18n("Edit Image Note"), + KStdGuiItem::open(), + KStdGuiItem::cancel()); + + if (choice == KMessageBox::Yes) + note()->basket()->noteOpen(note()); +} + +/** class AnimationEditor: */ + +AnimationEditor::AnimationEditor(AnimationContent *animationContent, QWidget *parent) + : NoteEditor(animationContent) +{ + int choice = KMessageBox::questionYesNo(parent, i18n( + "This animated image can not be edited here.\n" + "Do you want to open it with an application that understands it?"), + i18n("Edit Animation Note"), + KStdGuiItem::open(), + KStdGuiItem::cancel()); + + if (choice == KMessageBox::Yes) + note()->basket()->noteOpen(note()); +} + +/** class FileEditor: */ + +FileEditor::FileEditor(FileContent *fileContent, QWidget *parent) + : NoteEditor(fileContent), m_fileContent(fileContent) +{ + FocusedLineEdit *lineEdit = new FocusedLineEdit(parent); + lineEdit->setLineWidth(0); + lineEdit->setMidLineWidth(0); + lineEdit->setPaletteBackgroundColor(note()->backgroundColor()); + lineEdit->setPaletteForegroundColor(note()->textColor()); + lineEdit->setFont(note()->font()); + lineEdit->setText(m_fileContent->fileName()); + lineEdit->selectAll(); + setInlineEditor(lineEdit); + connect( lineEdit, SIGNAL(returnPressed()), this, SIGNAL(askValidation()) ); + connect( lineEdit, SIGNAL(escapePressed()), this, SIGNAL(askValidation()) ); + connect( lineEdit, SIGNAL(mouseEntered()), this, SIGNAL(mouseEnteredEditorWidget()) ); +} + +FileEditor::~FileEditor() +{ + delete widget(); +} + +void FileEditor::autoSave(bool toFileToo) +{ + // FIXME: How to detect cancel? + if (toFileToo && !lineEdit()->text().isEmpty() && m_fileContent->trySetFileName(lineEdit()->text())) { + m_fileContent->setFileName(lineEdit()->text()); + m_fileContent->setEdited(); + } +} + +void FileEditor::validate() +{ + autoSave(/*toFileToo=*/true); +} + +/** class LinkEditor: */ + +LinkEditor::LinkEditor(LinkContent *linkContent, QWidget *parent) + : NoteEditor(linkContent) +{ + LinkEditDialog dialog(linkContent, parent); + if (dialog.exec() == QDialog::Rejected) + cancel(); + if (linkContent->url().isEmpty() && linkContent->title().isEmpty()) + setEmpty(); +} + +/** class LauncherEditor: */ + +LauncherEditor::LauncherEditor(LauncherContent *launcherContent, QWidget *parent) + : NoteEditor(launcherContent) +{ + LauncherEditDialog dialog(launcherContent, parent); + if (dialog.exec() == QDialog::Rejected) + cancel(); + if (launcherContent->name().isEmpty() && launcherContent->exec().isEmpty()) + setEmpty(); +} + +/** class ColorEditor: */ + +ColorEditor::ColorEditor(ColorContent *colorContent, QWidget *parent) + : NoteEditor(colorContent) +{ + KColorDialog dialog(parent, /*name=*/"EditColor", /*modal=*/true); + dialog.setColor(colorContent->color()); + dialog.setCaption(i18n("Edit Color Note")); + if (dialog.exec() == QDialog::Accepted) { + if (dialog.color() != colorContent->color()) { + colorContent->setColor(dialog.color()); + colorContent->setEdited(); + } + } else + cancel(); + + /* This code don't allow to set a caption to the dialog: + QColor color = colorContent()->color(); + if (KColorDialog::getColor(color, parent) == QDialog::Accepted && color != m_color) { + colorContent()->setColor(color); + setEdited(); + }*/ +} + +/** class UnknownEditor: */ + +UnknownEditor::UnknownEditor(UnknownContent *unknownContent, QWidget *parent) + : NoteEditor(unknownContent) +{ + KMessageBox::information(parent, i18n( + "The type of this note is unknown and can not be edited here.\n" + "You however can drag or copy the note into an application that understands it."), + i18n("Edit Unknown Note")); +} + +/*********************************************************************/ + + +/** class DebuggedLineEdit: */ + +DebuggedLineEdit::DebuggedLineEdit(const QString &text, QWidget *parent) + : QLineEdit(text, parent) +{ +} + +DebuggedLineEdit::~DebuggedLineEdit() +{ +} + +void DebuggedLineEdit::keyPressEvent(QKeyEvent *event) +{ + QString oldText = text(); + QLineEdit::keyPressEvent(event); + if (oldText != text()) + emit textChanged(text()); +} + + +/** class LinkEditDialog: */ + +LinkEditDialog::LinkEditDialog(LinkContent *contentNote, QWidget *parent/*, QKeyEvent *ke*/) + : KDialogBase(KDialogBase::Plain, i18n("Edit Link Note"), KDialogBase::Ok | KDialogBase::Cancel, + KDialogBase::Ok, parent, /*name=*/"EditLink", /*modal=*/true, /*separator=*/true), + m_noteContent(contentNote) +{ + QWidget *page = plainPage(); + QGridLayout *layout = new QGridLayout(page, /*nRows=*/4, /*nCols=*/2, /*margin=*/0, spacingHint()); + + m_url = new KURLRequester(m_noteContent->url().url(), page); + + QWidget *wid1 = new QWidget(page); + QHBoxLayout *titleLay = new QHBoxLayout(wid1, /*margin=*/0, spacingHint()); + m_title = new DebuggedLineEdit(m_noteContent->title(), wid1); + m_autoTitle = new QPushButton(i18n("Auto"), wid1); + m_autoTitle->setToggleButton(true); + m_autoTitle->setOn(m_noteContent->autoTitle()); + titleLay->addWidget(m_title); + titleLay->addWidget(m_autoTitle); + + QWidget *wid = new QWidget(page); + QHBoxLayout *hLay = new QHBoxLayout(wid, /*margin=*/0, spacingHint()); + m_icon = new KIconButton(wid); + QLabel *label3 = new QLabel(m_icon, i18n("&Icon:"), page); + KURL filteredURL = NoteFactory::filteredURL(KURL(m_url->lineEdit()->text()));//KURIFilter::self()->filteredURI(KURL(m_url->lineEdit()->text())); + m_icon->setIconType(KIcon::NoGroup, KIcon::MimeType); + m_icon->setIconSize(LinkLook::lookForURL(filteredURL)->iconSize()); + m_autoIcon = new QPushButton(i18n("Auto"), wid); // Create before to know size here: + /* Icon button: */ + m_icon->setIcon(m_noteContent->icon()); + int minSize = m_autoIcon->sizeHint().height(); + // Make the icon button at least the same heigh than the other buttons for a better alignment (nicer to the eyes): + if (m_icon->sizeHint().height() < minSize) + m_icon->setFixedSize(minSize, minSize); + else + m_icon->setFixedSize(m_icon->sizeHint().height(), m_icon->sizeHint().height()); // Make it square + /* Auto button: */ + m_autoIcon->setToggleButton(true); + m_autoIcon->setOn(m_noteContent->autoIcon()); + hLay->addWidget(m_icon); + hLay->addWidget(m_autoIcon); + hLay->addStretch(); + + m_url->lineEdit()->setMinimumWidth(m_url->lineEdit()->fontMetrics().maxWidth()*20); + m_title->setMinimumWidth(m_title->fontMetrics().maxWidth()*20); + + //m_url->setShowLocalProtocol(true); + QLabel *label1 = new QLabel(m_url, i18n("Ta&rget:"), page); + QLabel *label2 = new QLabel(m_title, i18n("&Title:"), page); + layout->addWidget(label1, 0, 0, Qt::AlignVCenter); + layout->addWidget(label2, 1, 0, Qt::AlignVCenter); + layout->addWidget(label3, 2, 0, Qt::AlignVCenter); + layout->addWidget(m_url, 0, 1, Qt::AlignVCenter); + layout->addWidget(wid1, 1, 1, Qt::AlignVCenter); + layout->addWidget(wid, 2, 1, Qt::AlignVCenter); + + m_isAutoModified = false; + connect( m_url, SIGNAL(textChanged(const QString&)), this, SLOT(urlChanged(const QString&)) ); + connect( m_title, SIGNAL(textChanged(const QString&)), this, SLOT(doNotAutoTitle(const QString&)) ); + connect( m_icon, SIGNAL(iconChanged(QString)) , this, SLOT(doNotAutoIcon(QString)) ); + connect( m_autoTitle, SIGNAL(clicked()), this, SLOT(guessTitle()) ); + connect( m_autoIcon, SIGNAL(clicked()), this, SLOT(guessIcon()) ); + + QWidget *stretchWidget = new QWidget(page); + stretchWidget->setSizePolicy(QSizePolicy(/*hor=*/QSizePolicy::Fixed, /*ver=*/QSizePolicy::Expanding, /*hStretch=*/1, /*vStretch=*/255)); // Make it fill ALL vertical space + layout->addWidget(stretchWidget, 3, 1, Qt::AlignVCenter); + + + urlChanged(""); + +// if (ke) +// kapp->postEvent(m_url->lineEdit(), ke); +} + +LinkEditDialog::~LinkEditDialog() +{ +} + +void LinkEditDialog::polish() +{ + KDialogBase::polish(); + if (m_url->lineEdit()->text().isEmpty()) { + m_url->setFocus(); + m_url->lineEdit()->end(false); + } else { + m_title->setFocus(); + m_title->end(false); + } +} + + +void LinkEditDialog::urlChanged(const QString&) +{ + m_isAutoModified = true; +// guessTitle(); +// guessIcon(); + // Optimization (filter only once): + KURL filteredURL = NoteFactory::filteredURL(KURL(m_url->url()));//KURIFilter::self()->filteredURI(KURL(m_url->url())); + if (m_autoIcon->isOn()) + m_icon->setIcon(NoteFactory::iconForURL(filteredURL)); + if (m_autoTitle->isOn()) { + m_title->setText(NoteFactory::titleForURL(filteredURL)); + m_autoTitle->setOn(true); // Because the setText() will disable it! + } +} + +void LinkEditDialog::doNotAutoTitle(const QString&) +{ + if (m_isAutoModified) + m_isAutoModified = false; + else + m_autoTitle->setOn(false); +} + +void LinkEditDialog::doNotAutoIcon(QString) +{ + m_autoIcon->setOn(false); +} + +void LinkEditDialog::guessIcon() +{ + if (m_autoIcon->isOn()) { + KURL filteredURL = NoteFactory::filteredURL(KURL(m_url->url()));//KURIFilter::self()->filteredURI(KURL(m_url->url())); + m_icon->setIcon(NoteFactory::iconForURL(filteredURL)); + } +} + +void LinkEditDialog::guessTitle() +{ + if (m_autoTitle->isOn()) { + KURL filteredURL = NoteFactory::filteredURL(KURL(m_url->url()));//KURIFilter::self()->filteredURI(KURL(m_url->url())); + m_title->setText(NoteFactory::titleForURL(filteredURL)); + m_autoTitle->setOn(true); // Because the setText() will disable it! + } +} + +void LinkEditDialog::slotOk() +{ + KURL filteredURL = NoteFactory::filteredURL(KURL(m_url->url()));//KURIFilter::self()->filteredURI(KURL(m_url->url())); + m_noteContent->setLink(filteredURL, m_title->text(), m_icon->icon(), m_autoTitle->isOn(), m_autoIcon->isOn()); + m_noteContent->setEdited(); + + /* Change icon size if link look have changed */ + LinkLook *linkLook = LinkLook::lookForURL(filteredURL); + QString icon = m_icon->icon(); // When we change size, icon isn't changed and keep it's old size + m_icon->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); // Reset size policy + m_icon->setIconSize(linkLook->iconSize()); // So I store it's name and reload it after size change ! + m_icon->setIcon(icon); + int minSize = m_autoIcon->sizeHint().height(); + // Make the icon button at least the same heigh than the other buttons for a better alignment (nicer to the eyes): + if (m_icon->sizeHint().height() < minSize) + m_icon->setFixedSize(minSize, minSize); + else + m_icon->setFixedSize(m_icon->sizeHint().height(), m_icon->sizeHint().height()); // Make it square + + KDialogBase::slotOk(); +} + +/** class LauncherEditDialog: */ + +LauncherEditDialog::LauncherEditDialog(LauncherContent *contentNote, QWidget *parent) + : KDialogBase(KDialogBase::Plain, i18n("Edit Launcher Note"), KDialogBase::Ok | KDialogBase::Cancel, + KDialogBase::Ok, parent, /*name=*/"EditLauncher", /*modal=*/true, /*separator=*/true), + m_noteContent(contentNote) +{ + QWidget *page = plainPage(); + QGridLayout *layout = new QGridLayout(page, /*nRows=*/4, /*nCols=*/2, /*margin=*/0, spacingHint()); + + KService service(contentNote->fullPath()); + + m_command = new RunCommandRequester(service.exec(), i18n("Choose a command to run:"), page); + m_name = new QLineEdit(service.name(), page); + + QWidget *wid = new QWidget(page); + QHBoxLayout *hLay = new QHBoxLayout(wid, /*margin=*/0, spacingHint()); + m_icon = new KIconButton(wid); + QLabel *label = new QLabel(m_icon, i18n("&Icon:"), page); + m_icon->setIconType(KIcon::NoGroup, KIcon::Application); + m_icon->setIconSize(LinkLook::launcherLook->iconSize()); + QPushButton *guessButton = new QPushButton(i18n("&Guess"), wid); + /* Icon button: */ + m_icon->setIcon(service.icon()); + int minSize = guessButton->sizeHint().height(); + // Make the icon button at least the same heigh than the other buttons for a better alignment (nicer to the eyes): + if (m_icon->sizeHint().height() < minSize) + m_icon->setFixedSize(minSize, minSize); + else + m_icon->setFixedSize(m_icon->sizeHint().height(), m_icon->sizeHint().height()); // Make it square + /* Guess button: */ + hLay->addWidget(m_icon); + hLay->addWidget(guessButton); + hLay->addStretch(); + + m_command->lineEdit()->setMinimumWidth(m_command->lineEdit()->fontMetrics().maxWidth()*20); + + QLabel *label1 = new QLabel(m_command->lineEdit(), i18n("Comman&d:"), page); + QLabel *label2 = new QLabel(m_name, i18n("&Name:"), page); + layout->addWidget(label1, 0, 0, Qt::AlignVCenter); + layout->addWidget(label2, 1, 0, Qt::AlignVCenter); + layout->addWidget(label, 2, 0, Qt::AlignVCenter); + layout->addWidget(m_command, 0, 1, Qt::AlignVCenter); + layout->addWidget(m_name, 1, 1, Qt::AlignVCenter); + layout->addWidget(wid, 2, 1, Qt::AlignVCenter); + + QWidget *stretchWidget = new QWidget(page); + stretchWidget->setSizePolicy(QSizePolicy(/*hor=*/QSizePolicy::Fixed, /*ver=*/QSizePolicy::Expanding, /*hStretch=*/1, /*vStretch=*/255)); // Make it fill ALL vertical space + layout->addWidget(stretchWidget, 3, 1, Qt::AlignVCenter); + + connect( guessButton, SIGNAL(clicked()), this, SLOT(guessIcon()) ); +} + +LauncherEditDialog::~LauncherEditDialog() +{ +} + +void LauncherEditDialog::polish() +{ + KDialogBase::polish(); + if (m_command->runCommand().isEmpty()) { + m_command->lineEdit()->setFocus(); + m_command->lineEdit()->end(false); + } else { + m_name->setFocus(); + m_name->end(false); + } +} + +void LauncherEditDialog::slotOk() +{ + // TODO: Remember if a string has been modified AND IS DIFFERENT FROM THE ORIGINAL! + + KConfig conf(m_noteContent->fullPath()); + conf.setGroup("Desktop Entry"); + conf.writeEntry("Exec", m_command->runCommand()); + conf.writeEntry("Name", m_name->text()); + conf.writeEntry("Icon", m_icon->icon()); + + // Just for faster feedback: conf object will save to disk (and then m_note->loadContent() called) + m_noteContent->setLauncher(m_name->text(), m_icon->icon(), m_command->runCommand()); + m_noteContent->setEdited(); + + KDialogBase::slotOk(); +} + +void LauncherEditDialog::guessIcon() +{ + m_icon->setIcon( NoteFactory::iconForCommand(m_command->runCommand()) ); +} + +/** class InlineEditors: */ + +InlineEditors::InlineEditors() +{ +} + +InlineEditors::~InlineEditors() +{ +} + +InlineEditors* InlineEditors::instance() +{ + static InlineEditors *instance = 0; + if (!instance) + instance = new InlineEditors(); + return instance; +} + +void InlineEditors::initToolBars(KActionCollection *actionCollection) +{ + QFont defaultFont; + QColor textColor = (Global::bnpView && Global::bnpView->currentBasket() ? + Global::bnpView->currentBasket()->textColor() : + KGlobalSettings::textColor()); + + // Init the RichTextEditor Toolbar: + richTextFont = new FocusedFontCombo(Global::mainWindow()); + richTextFont->setFixedWidth(richTextFont->sizeHint().width() * 2 / 3); + richTextFont->setCurrentFont(defaultFont.family()); + KWidgetAction *action = new KWidgetAction(richTextFont, i18n("Font"), Qt::Key_F6, + /*receiver=*/0, /*slot=*/"", actionCollection, "richtext_font"); + + richTextFontSize = new FontSizeCombo(/*rw=*/true, Global::mainWindow()); + richTextFontSize->setFontSize(defaultFont.pointSize()); + action = new KWidgetAction(richTextFontSize, i18n("Font Size"), Qt::Key_F7, + /*receiver=*/0, /*slot=*/"", actionCollection, "richtext_font_size"); + + richTextColor = new FocusedColorCombo(Global::mainWindow()); + richTextColor->setFixedWidth(richTextColor->sizeHint().height() * 2); + richTextColor->setColor(textColor); + action = new KWidgetAction(richTextColor, i18n("Color"), KShortcut(), 0, SLOT(), actionCollection, "richtext_color"); + + richTextBold = new KToggleAction( i18n("Bold"), "text_bold", "Ctrl+B", actionCollection, "richtext_bold" ); + richTextItalic = new KToggleAction( i18n("Italic"), "text_italic", "Ctrl+I", actionCollection, "richtext_italic" ); + richTextUnderline = new KToggleAction( i18n("Underline"), "text_under", "Ctrl+U", actionCollection, "richtext_underline" ); + +// richTextSuper = new KToggleAction( i18n("Superscript"), "text_super", "", actionCollection, "richtext_super" ); +// richTextSub = new KToggleAction( i18n("Subscript"), "text_sub", "", actionCollection, "richtext_sub" ); + + richTextLeft = new KToggleAction( i18n("Align Left"), "text_left", "", actionCollection, "richtext_left" ); + richTextCenter = new KToggleAction( i18n("Centered"), "text_center", "", actionCollection, "richtext_center" ); + richTextRight = new KToggleAction( i18n("Align Right"), "text_right", "", actionCollection, "richtext_right" ); + richTextJustified = new KToggleAction( i18n("Justified"), "text_block", "", actionCollection, "richtext_block" ); + + richTextLeft->setExclusiveGroup("rt_justify"); + richTextCenter->setExclusiveGroup("rt_justify"); + richTextRight->setExclusiveGroup("rt_justify"); + richTextJustified->setExclusiveGroup("rt_justify"); + + richTextUndo = new KAction( i18n("Undo"), "undo", "", actionCollection, "richtext_undo"); + richTextRedo = new KAction( i18n("Redo"), "redo", "", actionCollection, "richtext_redo"); + + disableRichTextToolBar(); +} + +KToolBar* InlineEditors::richTextToolBar() +{ + if (Global::mainWindow()) { + Global::mainWindow()->toolBar(); // Make sure we create the main toolbar FIRST, so it will be on top of the edit toolbar! + return Global::mainWindow()->toolBar("richTextEditToolBar"); + } else + return 0; +} + +void InlineEditors::enableRichTextToolBar() +{ + richTextFont->setEnabled(true); + richTextFontSize->setEnabled(true); + richTextColor->setEnabled(true); + richTextBold->setEnabled(true); + richTextItalic->setEnabled(true); + richTextUnderline->setEnabled(true); + richTextLeft->setEnabled(true); + richTextCenter->setEnabled(true); + richTextRight->setEnabled(true); + richTextJustified->setEnabled(true); + richTextUndo->setEnabled(true); + richTextRedo->setEnabled(true); +} + +void InlineEditors::disableRichTextToolBar() +{ + disconnect(richTextFont); + disconnect(richTextFontSize); + disconnect(richTextColor); + disconnect(richTextBold); + disconnect(richTextItalic); + disconnect(richTextUnderline); + disconnect(richTextLeft); + disconnect(richTextCenter); + disconnect(richTextRight); + disconnect(richTextJustified); + disconnect(richTextUndo); + disconnect(richTextRedo); + + richTextFont->setEnabled(false); + richTextFontSize->setEnabled(false); + richTextColor->setEnabled(false); + richTextBold->setEnabled(false); + richTextItalic->setEnabled(false); + richTextUnderline->setEnabled(false); + richTextLeft->setEnabled(false); + richTextCenter->setEnabled(false); + richTextRight->setEnabled(false); + richTextJustified->setEnabled(false); + richTextUndo->setEnabled(false); + richTextRedo->setEnabled(false); + + // Return to a "proper" state: + QFont defaultFont; + QColor textColor = (Global::bnpView && Global::bnpView->currentBasket() ? + Global::bnpView->currentBasket()->textColor() : + KGlobalSettings::textColor()); + richTextFont->setCurrentFont(defaultFont.family()); + richTextFontSize->setFontSize(defaultFont.pointSize()); + richTextColor->setColor(textColor); + richTextBold->setChecked(false); + richTextItalic->setChecked(false); + richTextUnderline->setChecked(false); + richTextLeft->setChecked(false); + richTextCenter->setChecked(false); + richTextRight->setChecked(false); + richTextJustified->setChecked(false); +} + +#include "noteedit.moc" diff --git a/src/noteedit.h b/src/noteedit.h new file mode 100644 index 0000000..1866034 --- /dev/null +++ b/src/noteedit.h @@ -0,0 +1,289 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef NOTEEDIT_H +#define NOTEEDIT_H + +#include <kdialogbase.h> +#include <qtextedit.h> +#include <qlineedit.h> + +class QWidget; +//class QLineEdit; +class QPushButton; +class KIconButton; +class KURLRequester; +class KTextEdit; +class KMainWindow; +class KTooolBar; +class KToggleAction; + +class FontSizeCombo; + +class Note; +class RunCommandRequester; +class FocusedFontCombo; +class FocusedColorCombo; + +#include "notecontent.h" + +/** The base class for every note editors. + * Scenario: + * The Basket class calls NoteEditor::editNoteContent() with the NoteContent to edit. + * This method create the good child NoteEditor depending + * on the note content type and return it to the Basket. + * This custom NoteEditor have two choices regarding what to do in its constructor: + * - Display a dialog and then call cancel() if the user canceled the dialog; + * - Create an inline editor and call setInlineEditor() with that editor as parameter. + * When the user exit the edition, validate() is called by the Basket. + * You should then call setEmpty() is the user cleared the content. + * The custom editor SHOULD call the NoteEditor constructor. + * If the user cleared the content OR if the user canceled the dialog whereas he/she + * JUST ADDED the note, then the note will be deleted by the Basket. + */ +class NoteEditor : public QObject +{ + Q_OBJECT + public: + NoteEditor(NoteContent *noteContent); + bool isEmpty() { return m_isEmpty; } + bool canceled() { return m_canceled; } + bool isInline() { return m_widget != 0; } + QWidget* widget() { return m_widget; } + KTextEdit* textEdit() { return m_textEdit; } + QLineEdit* lineEdit() { return m_lineEdit; } + + private: + bool m_isEmpty; + bool m_canceled; + QWidget *m_widget; + KTextEdit *m_textEdit; + QLineEdit *m_lineEdit; + NoteContent *m_noteContent; + + public: + NoteContent* noteContent() { return m_noteContent; } + Note* note(); + protected: + void setEmpty() { m_isEmpty = true; } + void cancel() { m_canceled = true; } + void setInlineEditor(QWidget *inlineEditor); + public: + virtual void validate() {} + virtual void autoSave(bool /*toFileToo*/) {} // Same as validate(), but does not precede editor close and is triggered either while the editor widget changed size or after 3 seconds of inactivity. + + signals: + void askValidation(); + void mouseEnteredEditorWidget(); + + public: + static NoteEditor* editNoteContent(NoteContent *noteContent, QWidget *parent); +}; + +class TextEditor : public NoteEditor +{ + Q_OBJECT + public: + TextEditor(TextContent *textContent, QWidget *parent); + ~TextEditor(); + void validate(); + void autoSave(bool toFileToo); + protected: + TextContent *m_textContent; +}; + +class HtmlEditor : public NoteEditor +{ + Q_OBJECT + public: + HtmlEditor(HtmlContent *htmlContent, QWidget *parent); + ~HtmlEditor(); + void validate(); + void autoSave(bool toFileToo); + protected: + HtmlContent *m_htmlContent; + public slots: + void cursorPositionChanged(); + void textChanged(); + void fontChanged(const QFont &font); + protected slots: +// void slotVerticalAlignmentChanged(QTextEdit::VerticalAlignment align); + // void setBold(); + // void setItalic(); + // void setUnderline(); + void setLeft(); + void setCentered(); + void setRight(); + void setBlock(); +}; + +class ImageEditor : public NoteEditor +{ + Q_OBJECT + public: + ImageEditor(ImageContent *imageContent, QWidget *parent); +}; + +class AnimationEditor : public NoteEditor +{ + Q_OBJECT + public: + AnimationEditor(AnimationContent *animationContent, QWidget *parent); +}; + +class FileEditor : public NoteEditor +{ + Q_OBJECT + public: + FileEditor(FileContent *fileContent, QWidget *parent); + ~FileEditor(); + void validate(); + void autoSave(bool toFileToo); + protected: + FileContent *m_fileContent; +}; + +class LinkEditor : public NoteEditor +{ + Q_OBJECT + public: + LinkEditor(LinkContent *linkContent, QWidget *parent); +}; + +class LauncherEditor : public NoteEditor +{ + Q_OBJECT + public: + LauncherEditor(LauncherContent *launcherContent, QWidget *parent); +}; + +class ColorEditor : public NoteEditor +{ + Q_OBJECT + public: + ColorEditor(ColorContent *colorContent, QWidget *parent); +}; + +class UnknownEditor : public NoteEditor +{ + Q_OBJECT + public: + UnknownEditor(UnknownContent *unknownContent, QWidget *parent); +}; + +/** QLineEdit behavior: + * Create a new QLineEdit with a text, then the user select a part of it and press ONE letter key. + * The signal textChanged() is not emitted! + * This class correct that! + */ +class DebuggedLineEdit : public QLineEdit +{ + Q_OBJECT + public: + DebuggedLineEdit(const QString &text, QWidget *parent = 0); + ~DebuggedLineEdit(); + protected: + void keyPressEvent(QKeyEvent *event); +}; + +/** The dialog to edit Link Note content. + * @author S�astien Laot + */ +class LinkEditDialog : public KDialogBase +{ + Q_OBJECT + public: + LinkEditDialog(LinkContent *contentNote, QWidget *parent = 0); + ~LinkEditDialog(); + void polish(); + protected slots: + void slotOk(); + void urlChanged(const QString&); + void doNotAutoTitle(const QString&); + void doNotAutoIcon(QString); + void guessTitle(); + void guessIcon(); + private: + LinkContent *m_noteContent; + bool m_isAutoModified; + KURLRequester *m_url; + QLineEdit *m_title; + KIconButton *m_icon; + QPushButton *m_autoTitle; + QPushButton *m_autoIcon; +}; + + +/** The dialog to edit Launcher Note content. + * @author S�astien Laot + */ +class LauncherEditDialog : public KDialogBase +{ + Q_OBJECT + public: + LauncherEditDialog(LauncherContent *contentNote, QWidget *parent = 0); + ~LauncherEditDialog(); + void polish(); + protected slots: + void slotOk(); + void guessIcon(); + private: + LauncherContent *m_noteContent; + RunCommandRequester *m_command; + QLineEdit *m_name; + KIconButton *m_icon; +}; + +/** This class manage toolbars for the inline editors. + * The toolbars should be created once at the application startup, + * then KXMLGUI can manage them and save theire state and position... + * @author S�astien Laot + */ +class InlineEditors : public QObject +{ + Q_OBJECT + public: + InlineEditors(); + ~InlineEditors(); + void initToolBars(KActionCollection *actionCollection); + static InlineEditors* instance(); + + public: + // Rich Text ToolBar: + KToolBar* richTextToolBar(); + void enableRichTextToolBar(); + void disableRichTextToolBar(); + FocusedFontCombo *richTextFont; + FontSizeCombo *richTextFontSize; + FocusedColorCombo *richTextColor; + KToggleAction *richTextBold; + KToggleAction *richTextItalic; + KToggleAction *richTextUnderline; +// KToggleAction *richTextSuper; +// KToggleAction *richTextSub; + KToggleAction *richTextLeft; + KToggleAction *richTextCenter; + KToggleAction *richTextRight; + KToggleAction *richTextJustified; + KAction *richTextUndo; + KAction *richTextRedo; +}; + +#endif // NOTEEDIT_H diff --git a/src/notefactory.cpp b/src/notefactory.cpp new file mode 100644 index 0000000..30efc28 --- /dev/null +++ b/src/notefactory.cpp @@ -0,0 +1,1008 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qstring.h> +#include <kurl.h> +#include <qpixmap.h> +#include <qcolor.h> +#include <qregexp.h> +#include <kcolordrag.h> +#include <kurldrag.h> +#include <qstylesheet.h> +#include <qdir.h> +#include <kmimetype.h> +#include <kmessagebox.h> +#include <klocale.h> +#include <kdesktopfile.h> +#include <kapplication.h> +#include <kaboutdata.h> +#include <qfile.h> +#include <kfilemetainfo.h> +#include <kio/jobclasses.h> +#include <qtextcodec.h> +#include <kopenwith.h> +#include <kfiledialog.h> +#include <kicondialog.h> +#include <kiconloader.h> +#include <qfileinfo.h> +#include <kpopupmenu.h> +#include <kstandarddirs.h> +#include <kurifilter.h> + +#include <iostream> + +#include "basket.h" +#include "note.h" +#include "notefactory.h" +#include "notedrag.h" +#include "linklabel.h" +#include "global.h" +#include "settings.h" +#include "keyboard.h" +#include "variouswidgets.h" +#include "tools.h" + +#include "debugwindow.h" + +/** Create notes from scratch (just a content) */ + +Note* NoteFactory::createNoteText(const QString &text, Basket *parent, bool reallyPlainText/* = false*/) +{ + Note *note = new Note(parent); + if (reallyPlainText) { + TextContent *content = new TextContent(note, createFileForNewNote(parent, "txt")); + content->setText(text); + content->saveToFile(); + } else { + HtmlContent *content = new HtmlContent(note, createFileForNewNote(parent, "html")); + QString html = "<html><head><meta name=\"qrichtext\" content=\"1\" /></head><body>" + Tools::textToHTMLWithoutP(text) + "</body></html>"; + content->setHtml(html); + content->saveToFile(); + } + return note; +} + +Note* NoteFactory::createNoteHtml(const QString &html, Basket *parent) +{ + Note *note = new Note(parent); + HtmlContent *content = new HtmlContent(note, createFileForNewNote(parent, "html")); + content->setHtml(html); + content->saveToFile(); + return note; +} + +Note* NoteFactory::createNoteLink(const KURL &url, Basket *parent) +{ + Note *note = new Note(parent); + new LinkContent(note, url, titleForURL(url), iconForURL(url), /*autoTitle=*/true, /*autoIcon=*/true); + return note; +} + +Note* NoteFactory::createNoteLink(const KURL &url, const QString &title, Basket *parent) +{ + Note *note = new Note(parent); + new LinkContent(note, url, title, iconForURL(url), /*autoTitle=*/false, /*autoIcon=*/true); + return note; +} + +Note* NoteFactory::createNoteImage(const QPixmap &image, Basket *parent) +{ + Note *note = new Note(parent); + ImageContent *content = new ImageContent(note, createFileForNewNote(parent, "png")); + content->setPixmap(image); + content->saveToFile(); + return note; +} + +Note* NoteFactory::createNoteColor(const QColor &color, Basket *parent) +{ + Note *note = new Note(parent); + new ColorContent(note, color); + return note; +} + +/** Return a string list containing {url1, title1, url2, title2, url3, title3...} + */ +QStringList NoteFactory::textToURLList(const QString &text) +{ + // List to return: + QStringList list; + + // Split lines: + QStringList texts = QStringList::split('\n', text); + + // For each lines: + QStringList::iterator it; + for (it = texts.begin(); it != texts.end(); ++it) { + // Strip white spaces: + (*it) = (*it).stripWhiteSpace(); + + // Don't care of empty entries: + if ((*it).isEmpty()) + continue; + + // Compute lower case equivalent: + QString ltext = (*it).lower(); + + /* Search for mail address ("*@*.*" ; "*" can contain '_', '-', or '.') and add protocol to it */ + QString mailExpString = "[\\w-\\.]+@[\\w-\\.]+\\.[\\w]+"; + QRegExp mailExp("^"+mailExpString+"$"); + if (mailExp.search(ltext) != -1) { + ltext.insert(0, "mailto:"); + (*it).insert(0, "mailto:"); + } + + // TODO: Recognize "<link>" (link between '<' and '>') + // TODO: Replace " at " by "@" and " dot " by "." to look for e-mail addresses + + /* Search for mail address like "Name <[email protected]>" */ + QRegExp namedMailExp("^([\\w\\s]+)\\s<("+mailExpString+")>$"); + //namedMailExp.setCaseSensitive(true); // For the name to be keeped with uppercases // DOESN'T WORK ! + if (namedMailExp.search(ltext) != -1) { + QString name = namedMailExp.cap(1); + QString address = "mailto:" + namedMailExp.cap(2); + // Threat it NOW, as it's an exception (it have a title): + list.append(address); + list.append(name); + continue; + } + + /* Search for an url and create an URL note */ + if ( ltext.startsWith("/") && ltext[1] != '/' && ltext[1] != '*' || // Take files but not C/C++/... comments ! + ltext.startsWith("file:") || + ltext.startsWith("http://") || + ltext.startsWith("https://") || + ltext.startsWith("www.") || + ltext.startsWith("ftp.") || + ltext.startsWith("ftp://") || + ltext.startsWith("mailto:") ) { + + // First, correct the text to use the good format for the url + if (ltext.startsWith( "/")) + (*it).insert(0, "file:"); + if (ltext.startsWith("www.")) + (*it).insert(0, "http://"); + if (ltext.startsWith("ftp.")) + (*it).insert(0, "ftp://"); + + // And create the Url note (or launcher if URL point a .desktop file) + list.append(*it); + list.append(""); // We don't have any title + } else + return QStringList(); // FAILED: treat the text as a text, and not as a URL list! + } + return list; +} + +Note* NoteFactory::createNoteFromText(const QString &text, Basket *parent) +{ + /* Search for a color (#RGB , #RRGGBB , #RRRGGGBBB , #RRRRGGGGBBBB) and create a color note */ + QRegExp exp("^#(?:[a-fA-F\\d]{3}){1,4}$"); + if ( exp.search(text) != -1 ) + return createNoteColor(QColor(text), parent); + + /* Try to convert the text as a URL or a list of URLs */ + QStringList uriList = textToURLList(text); + if ( ! uriList.isEmpty() ) { + // TODO: This code is almost duplicated from fropURLs()! + Note *note; + Note *firstNote = 0; + Note *lastInserted = 0; + QStringList::iterator it; + for (it = uriList.begin(); it != uriList.end(); ++it) { + QString url = (*it); + ++it; + QString title = (*it); + if (title.isEmpty()) + note = createNoteLinkOrLauncher(KURL(url), parent); + else + note = createNoteLink(KURL(url), title, parent); + + // If we got a new note, insert it in a linked list (we will return the first note of that list): + if (note) { +// std::cout << "Drop URL: " << (*it).prettyURL() << std::endl; + if (!firstNote) + firstNote = note; + else { + lastInserted->setNext(note); + note->setPrev(lastInserted); + } + lastInserted = note; + } + + } + return firstNote; // It don't return ALL inserted notes ! + } + + //QString newText = text.stripWhiteSpace(); // The text for a new note, without useless spaces + /* Else, it's a text or an HTML note, so, create it */ + if (QStyleSheet::mightBeRichText(/*newT*/text)) + return createNoteHtml(/*newT*/text, parent); + else + return createNoteText(/*newT*/text, parent); +} + +Note* NoteFactory::createNoteLauncher(const KURL &url, Basket *parent) +{ + if (url.isEmpty()) + return createNoteLauncher("", "", "", parent); + else + return copyFileAndLoad(url, parent); +} + +Note* NoteFactory::createNoteLauncher(const QString &command, const QString &name, const QString &icon, Basket *parent) +{ + QString fileName = createNoteLauncherFile(command, name, icon, parent); + if (fileName.isEmpty()) + return 0L; + else + return loadFile(fileName, parent); +} + +QString NoteFactory::createNoteLauncherFile(const QString &command, const QString &name, const QString &icon, Basket *parent) +{ + QString content = QString( + "[Desktop Entry]\n" + "Exec=%1\n" + "Name=%2\n" + "Icon=%3\n" + "Encoding=UTF-8\n" + "Type=Application\n").arg(command, name, icon.isEmpty() ? QString("exec") : icon); + QString fileName = fileNameForNewNote(parent, "launcher.desktop"); + QString fullPath = parent->fullPathForFileName(fileName); +// parent->dontCareOfCreation(fullPath); + QFile file(fullPath); + if ( file.open(IO_WriteOnly) ) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); + stream << content; + file.close(); + return fileName; + } else + return QString(); +} + +Note* NoteFactory::createNoteLinkOrLauncher(const KURL &url, Basket *parent) +{ + // IMPORTANT: we create the service ONLY if the extension is ".desktop". + // Otherwise, KService take a long time to analyse all the file + // and output such things to stdout: + // "Invalid entry (missing '=') at /my/file.ogg:11984" + // "Invalid entry (missing ']') at /my/file.ogg:11984"... + KService::Ptr service; + if (url.fileName().endsWith(".desktop")) + service = new KService(url.path()); + + // If link point to a .desktop file then add a launcher, otherwise it's a link + if (service && service->isValid()) + return createNoteLauncher(url, parent); + else + return createNoteLink(url, parent); +} + +#include <qstrlist.h> +#include <qimage.h> + +bool NoteFactory::movingNotesInTheSameBasket(QMimeSource *source, Basket *parent, QDropEvent::Action action) +{ + if (NoteDrag::canDecode(source)) + return action == QDropEvent::Move && NoteDrag::basketOf(source) == parent; + else + return false; +} + +Note* NoteFactory::dropNote(QMimeSource *source, Basket *parent, bool fromDrop, QDropEvent::Action action, Note */*noteSource*/) +{ + Note *note = 0L; + + /* No data */ + if (source->format(0) == 0L) { + // TODO: add a parameter to say if it's from a clipboard paste, a selection paste, or a drop + // To be able to say "The clipboard/selection/drop is empty". +// KMessageBox::error(parent, i18n("There is no data to insert."), i18n("No Data")); + return 0; + } + + /* Debug */ + if (Global::debugWindow) { + *Global::debugWindow << "<b>Drop :</b>"; + for (int i = 0; source->format(i); ++i) + if ( *(source->format(i)) ) + *Global::debugWindow << "\t[" + QString::number(i) + "] " + QString(source->format(i)); + switch (action) { // The source want that we: + case QDropEvent::Copy: *Global::debugWindow << ">> Drop action: Copy"; break; + case QDropEvent::Move: *Global::debugWindow << ">> Drop action: Move"; break; + case QDropEvent::Link: *Global::debugWindow << ">> Drop action: Link"; break; + case QDropEvent::Private: *Global::debugWindow << ">> Drop action: Private"; break; // What is it? (Copy?) + case QDropEvent::UserAction: *Global::debugWindow << ">> Drop action: UserAction"; break; // Not currently + default: *Global::debugWindow << ">> Drop action: Unknown"; // supported by Qt! + } + } + + /* Copy or move a Note */ + if (NoteDrag::canDecode(source)) { + bool moveFiles = fromDrop && action == QDropEvent::Move; + bool moveNotes = moveFiles; + return NoteDrag::decode(source, parent, moveFiles, moveNotes); // Filename will be kept + } + + /* Else : Drop object to note */ + + QPixmap pixmap; + if ( QImageDrag::decode(source, pixmap) ) + return createNoteImage(pixmap, parent); + + // KColorDrag::decode() is buggy and can trheat strings like "#include <foo.h>" as a black color + // The correct "ideal" code: + /*QColor color; + if ( KColorDrag::decode(source, color) ) { + createNoteColor(color, parent); + return; +}*/ + // And then the hack (if provide color MIME type or a text that contains color), using createNote Color RegExp: + QString hack; + QRegExp exp("^#(?:[a-fA-F\\d]{3}){1,4}$"); + if (source->provides("application/x-color") || (QTextDrag::decode(source, hack) && (exp.search(hack) != -1)) ) { + QColor color; + if (KColorDrag::decode(source, color)) + return createNoteColor(color, parent); +// if ( (note = createNoteColor(color, parent)) ) +// return note; +// // Theorically it should be returned. If not, continue by dropping other things + } + + KURL::List urls; + if ( KURLDrag::decode(source, urls) ) { + // If it's a Paste, we should know if files should be copied (copy&paste) or moved (cut&paste): + if (!fromDrop && Tools::isAFileCut(source)) + action = QDropEvent::Move; + return dropURLs(urls, parent, action, fromDrop); + } + + // FIXME: use dropURLs() also from Mozilla? + + /* + * Mozilla's stuff sometimes uses utf-16-le - little-endian UTF-16. + * + * This has the property that for the ASCII subset case (And indeed, the + * ISO-8859-1 subset, I think), if you treat it as a C-style string, + * it'll come out to one character long in most cases, since it looks + * like: + * + * "<\0H\0T\0M\0L\0>\0" + * + * A strlen() call on that will give you 1, which simply isn't correct. + * That might, I suppose, be the answer, or something close. + * + * Also, Mozilla's drag/drop code predates the use of MIME types in XDnD + * - hence it'll throw about STRING and UTF8_STRING quite happily, hence + * the odd named types. + * + * Thanks to Dave Cridland for having said me that. + */ + if (source->provides("text/x-moz-url")) { // FOR MOZILLA + // Get the array and create a QChar array of 1/2 of the size + QByteArray mozilla = source->encodedData("text/x-moz-url"); + QMemArray<QChar> chars( mozilla.count() / 2 ); + // A small debug work to know the value of each bytes + if (Global::debugWindow) + for (uint i = 0; i < mozilla.count(); i++) + *Global::debugWindow << QString("'") + QChar(mozilla[i]) + "' " + QString::number(int(mozilla[i])); + // text/x-moz-url give the URL followed by the link title and separed by OxOA (10 decimal: new line?) + uint size = 0; + QChar *name = 0L; + // For each little endian mozilla chars, copy it to the array of QChars + for (uint i = 0; i < mozilla.count(); i += 2) { + chars[i/2] = QChar(mozilla[i], mozilla[i+1]); + if (mozilla[i] == 0x0A) { + size = i/2; + name = &(chars[i/2+1]); + } + } + // Create a QString that take the address of the first QChar and a length + if (name == 0L) { // We haven't found name (FIXME: Is it possible ?) + QString normalHtml(&(chars[0]), chars.size()); + return createNoteLink(normalHtml, parent); + } else { + QString normalHtml( &(chars[0]), size ); + QString normalTitle( name, chars.size()-size-1); + return createNoteLink(normalHtml, normalTitle, parent); + } + } + + if (source->provides("text/html")) { + QString html; + QCString subtype("html"); + // If the text/html comes from Mozilla or GNOME it can be UTF-16 encoded: we need ExtendedTextDrag to check that + ExtendedTextDrag::decode(source, html, subtype); + return createNoteHtml(html, parent); + } + + QString text; + // If the text/plain comes from GEdit or GNOME it can be empty: we need ExtendedTextDrag to check other MIME types + if ( ExtendedTextDrag::decode(source, text) ) + return createNoteFromText(text, parent); + + /* Unsucceful drop */ + note = createNoteUnknown(source, parent); + QString message = i18n("<p>%1 doesn't support the data you've dropped.<br>" + "It however created a generic note, allowing you to drag or copy it to an application that understand it.</p>" + "<p>If you want the support of these data, please contact developer or visit the " + "<a href=\"http://basket.kde.org/dropdb.php\">BasKet Drop Database</a>.</p>").arg(kapp->aboutData()->programName()); + KMessageBox::information(parent, message, i18n("Unsupported MIME Type(s)"), + "unsupportedDropInfo", KMessageBox::AllowLink); + return note; +} + +Note* NoteFactory::createNoteUnknown(QMimeSource *source, Basket *parent/*, const QString &annotations*/) +{ + // Save the MimeSource in a file: create and open the file: + QString fileName = createFileForNewNote(parent, "unknown"); + QFile file(parent->fullPath() + fileName); + if ( ! file.open(IO_WriteOnly) ) + return 0L; + QDataStream stream(&file); + + // Echo MIME types: + for (int i = 0; source->format(i); ++i) + if ( *(source->format(i)) ) + stream << QString(source->format(i)); // Output the '\0'-terminated format name string + + // Echo end of MIME types list delimiter: + stream << ""; + + // Echo the length (in bytes) and then the data, and then same for next MIME type: + for (int i = 0; source->format(i); ++i) + if ( *(source->format(i)) ) { + QByteArray data = source->encodedData(source->format(i)); + stream << (Q_UINT32)data.count(); + stream.writeRawBytes(data.data(), data.count()); + } + file.close(); + + Note *note = new Note(parent); + new UnknownContent(note, fileName); + return note; +} + +Note* NoteFactory::dropURLs(KURL::List urls, Basket *parent, QDropEvent::Action action, bool fromDrop) +{ + int shouldAsk = 0; // shouldAsk==0: don't ask ; shouldAsk==1: ask for "file" ; shouldAsk>=2: ask for "files" + bool shiftPressed = Keyboard::shiftPressed(); + bool ctrlPressed = Keyboard::controlPressed(); + bool modified = fromDrop && (shiftPressed || ctrlPressed); + + if (modified) // Then no menu + modified action + ; // action is already set: no work to do + else if (fromDrop) { // Compute if user should be asked or not + for ( KURL::List::iterator it = urls.begin(); it != urls.end(); ++it ) + if ((*it).protocol() != "mailto") { // Do not ask when dropping mail address :-) + shouldAsk++; + if (shouldAsk == 1/*2*/) // Sufficient + break; + } + if (shouldAsk) { + KPopupMenu menu(parent); + menu.insertItem( SmallIconSet("goto"), i18n("&Move Here\tShift"), 0 ); + menu.insertItem( SmallIconSet("editcopy"), i18n("&Copy Here\tCtrl"), 1 ); + menu.insertItem( SmallIconSet("www"), i18n("&Link Here\tCtrl+Shift"), 2 ); + menu.insertSeparator(); + menu.insertItem( SmallIconSet("cancel"), i18n("C&ancel\tEscape"), 3 ); + int id = menu.exec(QCursor::pos()); + switch (id) { + case 0: action = QDropEvent::Move; break; + case 1: action = QDropEvent::Copy; break; + case 2: action = QDropEvent::Link; break; + default: return 0; + } + modified = true; + } + } else { // fromPaste + ; + } + + /* Policy of drops of URL: + * Email: [Modifier keys: Useless] + + - Link mail address + * Remote URL: [Modifier keys: {Copy,Link}] + + - Download as Image, Animation and Launcher + + - Link other URLs + * Local URL: [Modifier keys: {Copy,Move,Link}] + * - Copy as Image, Animation and Launcher [Modifier keys: {Copy,Move,Link}] + * - Link folder [Modifier keys: Useless] + * - Make Launcher of executable [Modifier keys: {Copy_exec,Move_exec,Link_Launcher}] + * - Ask for file (if use want to copy and it is a sound: make Sound) + * Policy of pastes of URL: [NO modifier keys] + * - Same as drops + * - But copy when ask should be done + * - Unless cut-selection is true: move files instead + * Policy of file created in the basket dir: [NO modifier keys] + * - View as Image, Animation, Sound, Launcher + * - View as File + */ + Note *note; + Note *firstNote = 0; + Note *lastInserted = 0; + for (KURL::List::iterator it = urls.begin(); it != urls.end(); ++it) { + if ( ((*it).protocol() == "mailto") || + (action == QDropEvent::Link) ) + note = createNoteLinkOrLauncher(*it, parent); + else if (!(*it).isLocalFile()) { + if ( action != QDropEvent::Link && (maybeImageOrAnimation(*it)/* || maybeSound(*it)*/) ) + note = copyFileAndLoad(*it, parent); + else + note = createNoteLinkOrLauncher(*it, parent); + } else { + if (action == QDropEvent::Copy) + note = copyFileAndLoad(*it, parent); + else if (action == QDropEvent::Move) + note = moveFileAndLoad(*it, parent); + else + note = createNoteLinkOrLauncher(*it, parent); + } + + // If we got a new note, insert it in a linked list (we will return the first note of that list): + if (note) { + DEBUG_WIN << "Drop URL: " + (*it).prettyURL(); + if (!firstNote) + firstNote = note; + else { + lastInserted->setNext(note); + note->setPrev(lastInserted); + } + lastInserted = note; + } + } + return firstNote; +} + +void NoteFactory::consumeContent(QDataStream &stream, NoteType::Id type) +{ + if (type == NoteType::Link) { + KURL url; + QString title, icon; + Q_UINT64 autoTitle64, autoIcon64; + stream >> url >> title >> icon >> autoTitle64 >> autoIcon64; + } else if (type == NoteType::Color) { + QColor color; + stream >> color; + } +} + +Note* NoteFactory::decodeContent(QDataStream &stream, NoteType::Id type, Basket *parent) +{ +/* if (type == NoteType::Text) { + QString text; + stream >> text; + return NoteFactory::createNoteText(text, parent); +} else if (type == NoteType::Html) { + QString html; + stream >> html; + return NoteFactory::createNoteHtml(html, parent); +} else if (type == NoteType::Image) { + QPixmap pixmap; + stream >> pixmap; + return NoteFactory::createNoteImage(pixmap, parent); +} else */ + if (type == NoteType::Link) { + KURL url; + QString title, icon; + Q_UINT64 autoTitle64, autoIcon64; + bool autoTitle, autoIcon; + stream >> url >> title >> icon >> autoTitle64 >> autoIcon64; + autoTitle = (bool)autoTitle64; + autoIcon = (bool)autoIcon64; + Note *note = NoteFactory::createNoteLink(url, parent); + ((LinkContent*)(note->content()))->setLink(url, title, icon, autoTitle, autoIcon); + return note; + } else if (type == NoteType::Color) { + QColor color; + stream >> color; + return NoteFactory::createNoteColor(color, parent); + } else + return 0; // NoteFactory::loadFile() is sufficient +} + +// mayBeLauncher: url.url().endsWith(".desktop"); + +bool NoteFactory::maybeText(const KURL &url) +{ + QString path = url.url().lower(); + return path.endsWith(".txt"); +} + +bool NoteFactory::maybeHtml(const KURL &url) +{ + QString path = url.url().lower(); + return path.endsWith(".html") || path.endsWith(".htm"); +} + +bool NoteFactory::maybeImageOrAnimation(const KURL &url) +{ + /* Examples on my machine: + QImageDrag can understands + {"image/png", "image/bmp", "image/jpeg", "image/pgm", "image/ppm", "image/xbm", "image/xpm"} + QImageIO::inputFormats() returns + {"BMP", "GIF", "JPEG", "MNG", "PBM", "PGM", "PNG", "PPM", "XBM", "XPM"} + QImageDecoder::inputFormats(): + {"GIF", "MNG", "PNG"} */ + QStrList list = QImageIO::inputFormats(); + list.prepend("jpg"); // Since QImageDrag return only "JPEG" and extensions can be "JPG"; preprend for heuristic optim. + char *s; + QString path = url.url().lower(); + for (s = list.first(); s; s = list.next()) + if (path.endsWith(QString(".") + QString(s).lower())) + return true; + // TODO: Search real MIME type for local files? + return false; +} + +bool NoteFactory::maybeAnimation(const KURL &url) +{ + QString path = url.url().lower(); + return path.endsWith(".mng") || path.endsWith(".gif"); +} + +bool NoteFactory::maybeSound(const KURL &url) +{ + QString path = url.url().lower(); + return path.endsWith(".mp3") || path.endsWith(".ogg"); +} + +bool NoteFactory::maybeLauncher(const KURL &url) +{ + QString path = url.url().lower(); + return path.endsWith(".desktop"); +} + +////////////// NEW: + +Note* NoteFactory::copyFileAndLoad(const KURL &url, Basket *parent) +{ + QString fileName = fileNameForNewNote(parent, url.fileName()); + QString fullPath = parent->fullPathForFileName(fileName); + + if (Global::debugWindow) + *Global::debugWindow << "copyFileAndLoad: " + url.prettyURL() + " to " + fullPath; + +// QString annotations = i18n("Original file: %1").arg(url.prettyURL()); +// parent->dontCareOfCreation(fullPath); + + +// KIO::CopyJob *copyJob = KIO::copy(url, KURL(fullPath)); +// parent->connect( copyJob, SIGNAL(copyingDone(KIO::Job *, const KURL &, const KURL &, bool, bool)), +// parent, SLOT(slotCopyingDone(KIO::Job *, const KURL &, const KURL &, bool, bool)) ); + + KIO::FileCopyJob *copyJob = new KIO::FileCopyJob( + url, KURL(fullPath), 0666, /*move=*/false, + /*overwrite=*/true, /*resume=*/true, /*showProgress=*/true ); + parent->connect( copyJob, SIGNAL(result(KIO::Job *)), + parent, SLOT(slotCopyingDone2(KIO::Job *)) ); + + + NoteType::Id type = typeForURL(url, parent); // Use the type of the original file because the target doesn't exist yet + return loadFile(fileName, type, parent); +} + +Note* NoteFactory::moveFileAndLoad(const KURL &url, Basket *parent) +{ + // Globally the same as copyFileAndLoad() but move instead of copy (KIO::move()) + QString fileName = fileNameForNewNote(parent, url.fileName()); + QString fullPath = parent->fullPathForFileName(fileName); + + if (Global::debugWindow) + *Global::debugWindow << "moveFileAndLoad: " + url.prettyURL() + " to " + fullPath; + +// QString annotations = i18n("Original file: %1").arg(url.prettyURL()); +// parent->dontCareOfCreation(fullPath); + + +// KIO::CopyJob *copyJob = KIO::move(url, KURL(fullPath)); +// parent->connect( copyJob, SIGNAL(copyingDone(KIO::Job *, const KURL &, const KURL &, bool, bool)), +// parent, SLOT(slotCopyingDone(KIO::Job *, const KURL &, const KURL &, bool, bool)) ); + + KIO::FileCopyJob *copyJob = new KIO::FileCopyJob( + url, KURL(fullPath), 0666, /*move=*/true, + /*overwrite=*/true, /*resume=*/true, /*showProgress=*/true ); + parent->connect( copyJob, SIGNAL(result(KIO::Job *)), + parent, SLOT(slotCopyingDone2(KIO::Job *)) ); + + + NoteType::Id type = typeForURL(url, parent); // Use the type of the original file because the target doesn't exist yet + return loadFile(fileName, type, parent); +} + +Note* NoteFactory::loadFile(const QString &fileName, Basket *parent) +{ + // The file MUST exists + QFileInfo file( KURL(parent->fullPathForFileName(fileName)).path() ); + if ( ! file.exists() ) + return 0L; + + NoteType::Id type = typeForURL(parent->fullPathForFileName(fileName), parent); + Note *note = loadFile(fileName, type, parent); + return note; +} + +Note* NoteFactory::loadFile(const QString &fileName, NoteType::Id type, Basket *parent) +{ + Note *note = new Note(parent); + switch (type) { + case NoteType::Text: new TextContent( note, fileName ); break; + case NoteType::Html: new HtmlContent( note, fileName ); break; + case NoteType::Image: new ImageContent( note, fileName ); break; + case NoteType::Animation: new AnimationContent( note, fileName ); break; + case NoteType::Sound: new SoundContent( note, fileName ); break; + case NoteType::File: new FileContent( note, fileName ); break; + case NoteType::Launcher: new LauncherContent( note, fileName ); break; + case NoteType::Unknown: new UnknownContent( note, fileName ); break; + + default: + case NoteType::Link: + case NoteType::Color: + return 0; + } + + return note; +} + +NoteType::Id NoteFactory::typeForURL(const KURL &url, Basket */*parent*/) +{ +/* KMimeType::Ptr kMimeType = KMimeType::findByURL(url); + if (Global::debugWindow) + *Global::debugWindow << "typeForURL: " + kMimeType->parentMimeType();//property("MimeType").toString();*/ + bool viewText = Settings::viewTextFileContent(); + bool viewHTML = Settings::viewHtmlFileContent(); + bool viewImage = Settings::viewImageFileContent(); + bool viewSound = Settings::viewSoundFileContent(); + + KFileMetaInfo metaInfo(url); + if (Global::debugWindow && metaInfo.isEmpty()) + *Global::debugWindow << "typeForURL: metaInfo is empty for " + url.prettyURL(); + if (metaInfo.isEmpty()) { // metaInfo is empty for GIF files on my machine ! + if (viewText && maybeText(url)) return NoteType::Text; + else if (viewHTML && (maybeHtml(url))) return NoteType::Html; + else if (viewImage && maybeAnimation(url)) return NoteType::Animation; // See Note::movieStatus(int) + else if (viewImage && maybeImageOrAnimation(url)) return NoteType::Image; // for more explanations + else if (viewSound && maybeSound(url)) return NoteType::Sound; + else if (maybeLauncher(url)) return NoteType::Launcher; + else return NoteType::File; + } + QString mimeType = metaInfo.mimeType(); + + if (Global::debugWindow) + *Global::debugWindow << "typeForURL: " + url.prettyURL() + " ; MIME type = " + mimeType; + + if (mimeType == "application/x-desktop") return NoteType::Launcher; + else if (viewText && mimeType.startsWith("text/plain")) return NoteType::Text; + else if (viewHTML && mimeType.startsWith("text/html")) return NoteType::Html; + else if (viewImage && mimeType == "movie/x-mng") return NoteType::Animation; + else if (viewImage && mimeType == "image/gif") return NoteType::Animation; + else if (viewImage && mimeType.startsWith("image/")) return NoteType::Image; + else if (viewSound && mimeType.startsWith("audio/")) return NoteType::Sound; + else return NoteType::File; +} + +QString NoteFactory::fileNameForNewNote(Basket *parent, const QString &wantedName) +{ + return Tools::fileNameForNewFile(wantedName, parent->fullPath()); +} + +// Create a file to store a new note in Basket parent and with extension extension. +// If wantedName is provided, the function will first try to use this file name, or derive it if it's impossible +// (extension willn't be used for that case) +QString NoteFactory::createFileForNewNote(Basket *parent, const QString &extension, const QString &wantedName) +{ + static int nb = 1; + + QString fileName; + QString fullName; + + if (wantedName.isEmpty()) { // TODO: fileNameForNewNote(parent, "note1."+extension); + QDir dir; + for (/*int nb = 1*/; ; ++nb) { // TODO: FIXME: If overflow ??? + fileName = "note" + QString::number(nb)/*.rightJustify(5, '0')*/ + "." + extension; + fullName = parent->fullPath() + fileName; + dir = QDir(fullName); + if ( ! dir.exists(fullName) ) + break; + } + } else { + fileName = fileNameForNewNote(parent, wantedName); + fullName = parent->fullPath() + fileName; + } + + // Create the file +// parent->dontCareOfCreation(fullName); + QFile file(fullName); + file.open(IO_WriteOnly); + file.close(); + + return fileName; +} + +KURL NoteFactory::filteredURL(const KURL &url) +{ + // KURIFilter::filteredURI() is slow if the URL contains only letters, digits and '-' or '+'. + // So, we don't use that function is that case: + bool isSlow = true; + for (uint i = 0; i < url.url().length(); ++i) { + QChar c = url.url()[i]; + if (!c.isLetterOrNumber() && c != '-' && c != '+') { + isSlow = false; + break; + } + } + if (isSlow) + return url; + else + return KURIFilter::self()->filteredURI(url); +} + +QString NoteFactory::titleForURL(const KURL &url) +{ + QString title = url.prettyURL(); + QString home = "file:" + QDir::homeDirPath() + "/"; + + if (title.startsWith("mailto:")) + return title.remove(0, 7); + + if (title.startsWith(home)) + title = "~/" + title.remove(0, home.length()); + + if (title.startsWith("file://")) + title = title.remove(0, 7); // 7 == QString("file://").length() - 1 + else if (title.startsWith("file:")) + title = title.remove(0, 5); // 5 == QString("file:").length() - 1 + else if (title.startsWith("http://www.")) + title = title.remove(0, 11); // 11 == QString("http://www.").length() - 1 + else if (title.startsWith("http://")) + title = title.remove(0, 7); // 7 == QString("http://").length() - 1 + + if ( ! url.isLocalFile() ) { + if (title.endsWith("/index.html") && title.length() > 11) + title.truncate(title.length() - 11); // 11 == QString("/index.html").length() + else if (title.endsWith("/index.htm") && title.length() > 10) + title.truncate(title.length() - 10); // 10 == QString("/index.htm").length() + else if (title.endsWith("/index.xhtml") && title.length() > 12) + title.truncate(title.length() - 12); // 12 == QString("/index.xhtml").length() + else if (title.endsWith("/index.php") && title.length() > 10) + title.truncate(title.length() - 10); // 10 == QString("/index.php").length() + else if (title.endsWith("/index.asp") && title.length() > 10) + title.truncate(title.length() - 10); // 10 == QString("/index.asp").length() + else if (title.endsWith("/index.php3") && title.length() > 11) + title.truncate(title.length() - 11); // 11 == QString("/index.php3").length() + else if (title.endsWith("/index.php4") && title.length() > 11) + title.truncate(title.length() - 11); // 11 == QString("/index.php4").length() + else if (title.endsWith("/index.php5") && title.length() > 11) + title.truncate(title.length() - 11); // 11 == QString("/index.php5").length() + } + + if (title.length() > 2 && title.endsWith("/")) // length > 2 because "/" and "~/" shouldn't be transformed to "" and "~" + title.truncate(title.length() - 1); // eg. transform "www.kde.org/" to "www.kde.org" + + return title; +} + +QString NoteFactory::iconForURL(const KURL &url) +{ + QString icon = KMimeType::iconForURL(url.url()); + if ( url.protocol() == "mailto" ) + icon = "message"; + return icon; +} + +// TODO: Can I add "autoTitle" and "autoIcon" entries to .desktop files? or just store them in basket, as now... + +/* Try our better to find an icon suited to the command line + * eg. "/usr/bin/kwrite-3.2 ~/myfile.txt /home/other/file.xml" + * will give the "kwrite" icon! + */ +QString NoteFactory::iconForCommand(const QString &command) +{ + QString icon; + + // 1. Use first word as icon (typically the program without argument) + icon = QStringList::split(' ', command).first(); + // 2. If the command is a full path, take only the program file name + icon = icon.mid(icon.findRev('/') + 1); // strip path if given [But it doesn't care of such + // "myprogram /my/path/argument" -> return "argument". Would + // must first strip first word and then strip path... Useful ?? + // 3. Use characters before any '-' (e.g. use "gimp" icon if run command is "gimp-1.3") + if ( ! isIconExist(icon) ) + icon = QStringList::split('-', icon).first(); + // 4. If the icon still not findable, use a generic icon + if ( ! isIconExist(icon) ) + icon = "exec"; + + return icon; +} + +bool NoteFactory::isIconExist(const QString &icon) +{ + return ! kapp->iconLoader()->loadIcon(icon, KIcon::NoGroup, 16, KIcon::DefaultState, 0L, true).isNull(); +} + +Note* NoteFactory::createEmptyNote(NoteType::Id type, Basket *parent) +{ + QPixmap *pixmap; + switch (type) { + case NoteType::Text: + return NoteFactory::createNoteText("", parent, /*reallyPlainText=*/true); + case NoteType::Html: + return NoteFactory::createNoteHtml("", parent); + case NoteType::Image: + pixmap = new QPixmap( QSize(Settings::defImageX(), Settings::defImageY()) ); + pixmap->fill(); + pixmap->setMask(pixmap->createHeuristicMask()); + return NoteFactory::createNoteImage(*pixmap, parent); + case NoteType::Link: + return NoteFactory::createNoteLink(KURL(), parent); + case NoteType::Launcher: + return NoteFactory::createNoteLauncher(KURL(), parent); + case NoteType::Color: + return NoteFactory::createNoteColor(Qt::black, parent); + default: + case NoteType::Animation: + case NoteType::Sound: + case NoteType::File: + case NoteType::Unknown: + return 0; // Not possible! + } +} + +Note* NoteFactory::importKMenuLauncher(Basket *parent) +{ + KOpenWithDlg dialog(parent); + dialog.setSaveNewApplications(true); // To create temp file, needed by createNoteLauncher() + dialog.exec(); + if (dialog.service()) { + // * locateLocal() return a local file even if it is a system wide one (local one doesn't exists) + // * desktopEntryPath() returns the full path for system wide ressources, but relative path if in home + QString serviceUrl = dialog.service()->desktopEntryPath(); + if ( ! serviceUrl.startsWith("/") ) + serviceUrl = dialog.service()->locateLocal(); //locateLocal("xdgdata-apps", serviceUrl); + return createNoteLauncher(serviceUrl, parent); + } + return 0; +} + +Note* NoteFactory::importIcon(Basket *parent) +{ + QString iconName = KIconDialog::getIcon( KIcon::Desktop, KIcon::Application, false, Settings::defIconSize() ); + if ( ! iconName.isEmpty() ) { + IconSizeDialog dialog(i18n("Import Icon as Image"), i18n("Choose the size of the icon to import as an image:"), iconName, Settings::defIconSize(), 0); + dialog.exec(); + if (dialog.iconSize() > 0) { + Settings::setDefIconSize(dialog.iconSize()); + Settings::saveConfig(); + return createNoteImage( DesktopIcon(iconName, dialog.iconSize()), parent ); // TODO: wantedName = iconName ! + } + } + return 0; +} + +Note* NoteFactory::importFileContent(Basket *parent) +{ + KURL url = KFileDialog::getOpenURL( QString::null, QString::null, parent, i18n("Load File Content into a Note") ); + if ( ! url.isEmpty() ) + return copyFileAndLoad(url, parent); + return 0; +} diff --git a/src/notefactory.h b/src/notefactory.h new file mode 100644 index 0000000..3fee459 --- /dev/null +++ b/src/notefactory.h @@ -0,0 +1,96 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef NOTEFACTORY_H +#define NOTEFACTORY_H + +#include <qevent.h> +#include <kurl.h> +#include <qstringlist.h> + +class QString; +class QPixmap; +class QColor; + +class Basket; +class Note; + +enum NoteType::Id; + +/** Factory class to create (new, drop, past) or load BasketIem, and eventuelly save them (?) + * @author S�bastien Lao�t + */ +namespace NoteFactory +{ + /** Functions to create a new note from a content. + * Content, if any, is saved to file but the note is not insterted in the basket, and the basket is not saved. + * Return 0 if the note has not been successfully created. + * In some cases, the returned note can be a group containing several notes or the first note of a chained list. + * The method Basket::TODO() can insert several grouped or chained notes without problem. + */ + Note* createNoteText( const QString &text, Basket *parent, bool reallyPlainText = false); + Note* createNoteHtml( const QString &html, Basket *parent); + Note* createNoteLink( const KURL &url, Basket *parent); + Note* createNoteLink( const KURL &url, const QString &title, Basket *parent); + Note* createNoteImage( const QPixmap &image, Basket *parent); + Note* createNoteColor( const QColor &color, Basket *parent); + Note* createNoteFromText( const QString &content, Basket *parent); // Find automatically the type from the text meaning // TODO: Return Note::List? + Note* createNoteLauncher( const KURL &url, Basket *parent); + Note* createNoteLauncher( const QString &command, const QString &name, const QString &icon, Basket *parent); + Note* createNoteUnknown( QMimeSource *source, Basket *parent); + /** Functions to create derived notes from a content */ + Note* createNoteLinkOrLauncher( const KURL &url, Basket *parent); + Note* copyFileAndLoad( const KURL &url, Basket *parent); + Note* moveFileAndLoad( const KURL &url, Basket *parent); + Note* loadFile( const QString &fileName, Basket *parent); /// << Determine the content of the file (the file SHOULD exists) and return a note of the good type. + Note* loadFile( const QString &fileName, NoteType::Id type, Basket *parent ); /// << Create a note of type @p type. The file is not obliged to exist. + /** Functions to create a new note from a drop or past event */ + Note* dropNote(QMimeSource *source, Basket *parent, + bool fromDrop = false, QDropEvent::Action action = QDropEvent::Copy, Note *noteSource = 0); + bool movingNotesInTheSameBasket(QMimeSource *source, Basket *parent, QDropEvent::Action action); + Note* dropURLs(KURL::List urls, Basket *parent, QDropEvent::Action action, bool fromDrop); + Note* decodeContent(QDataStream &stream, NoteType::Id type, Basket *parent); /// << Decode the @p stream to a note or return 0 if a general loadFile() is sufficient. + void consumeContent(QDataStream &stream, NoteType::Id type); /// << Decode the @p stream to a note or return 0 if a general loadFile() is sufficient. + /** Functions to create a note file but not load it in a note object */ + QString createNoteLauncherFile(const QString &command, const QString &name, const QString &icon, Basket *parent); + /** Other useful functions */ + NoteType::Id typeForURL(const KURL &url, Basket *parent); + bool maybeText(const KURL &url); + bool maybeHtml(const KURL &url); + bool maybeImageOrAnimation(const KURL &url); + bool maybeAnimation(const KURL &url); + bool maybeSound(const KURL &url); + bool maybeLauncher(const KURL &url); + QString fileNameForNewNote(Basket *parent, const QString &wantedName); + QString createFileForNewNote(Basket *parent, const QString &extension, const QString &wantedName = ""); + KURL filteredURL(const KURL &url); + QString titleForURL(const KURL &url); + QString iconForURL(const KURL &url); + QString iconForCommand(const QString &command); + bool isIconExist(const QString &icon); + QStringList textToURLList(const QString &text); // @Return { url1, title1, url2, title2, url3, title3... } + /** Insert GUI menu */ + Note* createEmptyNote( NoteType::Id type, Basket *parent ); // Insert empty if of type Note::Type + Note* importKMenuLauncher(Basket *parent); + Note* importIcon(Basket *parent); + Note* importFileContent(Basket *parent); +} + +#endif // NOTEFACTORY_H diff --git a/src/password.cpp b/src/password.cpp new file mode 100644 index 0000000..cbac314 --- /dev/null +++ b/src/password.cpp @@ -0,0 +1,113 @@ +/*************************************************************************** + * Copyright (C) 2006 by Petri Damsten * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include "password.h" + +#ifdef HAVE_LIBGPGME + +#include <qlayout.h> +#include <qtoolbutton.h> +#include <qbuttongroup.h> +#include <qradiobutton.h> +#include <klocale.h> +#include <kiconloader.h> +#include <kmessagebox.h> +#include <kgpgme.h> +#include <basket.h> + +PasswordDlg::PasswordDlg(QWidget *parent, const char *name) + :KDialogBase(Plain, i18n("Password Protection"), Ok|Cancel, Ok, + parent, name, /*modal=*/true, /*separator=*/true), w(0) +{ + QHBoxLayout* toplayout = new QHBoxLayout(plainPage(), 0, 0); + w = new Password(plainPage()); + toplayout->addWidget(w, 1); +} + +PasswordDlg::~PasswordDlg() +{ + delete w; +} + +void PasswordDlg::slotOk() +{ + int n = type(); + if(n == Basket::PrivateKeyEncryption && key().isEmpty()) + KMessageBox::error(w, i18n("No private key selected.")); + else + KDialogBase::slotOk(); +} + +QString PasswordDlg::key() const +{ + QString s = w->keyCombo->currentText(); + if(s.length() < 16) + return ""; + int n = s.findRev(' '); + if(n < 0) + return ""; + return s.mid(n+1); +} + +int PasswordDlg::type() const +{ + return w->buttonGroup->selectedId(); +} + +void PasswordDlg::setKey(const QString& key) +{ + for(int i = 0; i < w->keyCombo->count(); ++i) + { + if(w->keyCombo->text(i).find(key) >= 0) + { + w->keyCombo->setCurrentItem(i); + return; + } + } +} + +void PasswordDlg::setType(int type) +{ + w->buttonGroup->setButton(type); +} + +Password::Password(QWidget *parent, const char *name) + : PasswordLayout(parent, name) +{ + KGpgMe gpg; + + KGpgKeyList list = gpg.keys(true); + for(KGpgKeyList::iterator it = list.begin(); it != list.end(); ++it) { + QString name = gpg.checkForUtf8((*it).name); + + keyCombo->insertItem(QString("%1 <%2> %3").arg(name).arg((*it).email).arg((*it).id)); + } + publicPrivateRadioButton->setEnabled(keyCombo->count() > 0); + keyCombo->setEnabled(keyCombo->count() > 0); +} + + +Password::~Password() +{ +} + +#include "password.moc" + +#endif diff --git a/src/password.h b/src/password.h new file mode 100644 index 0000000..8df3e0e --- /dev/null +++ b/src/password.h @@ -0,0 +1,65 @@ +/*************************************************************************** + * Copyright (C) 2006 by Petri Damsten * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ +#ifndef PASSWORD_H +#define PASSWORD_H + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#ifdef HAVE_LIBGPGME + +#include <passwordlayout.h> +#include <kdialogbase.h> + +/** + @author Petri Damsten <[email protected]> +*/ +class Password : public PasswordLayout +{ + Q_OBJECT + public: + Password(QWidget *parent, const char *name = 0); + ~Password(); +}; + +class PasswordDlg : public KDialogBase +{ + Q_OBJECT + public: + PasswordDlg(QWidget *parent, const char *name = 0); + ~PasswordDlg(); + + QString key() const; + int type() const; + void setKey(const QString& key); + void setType(int type); + + protected slots: + virtual void slotOk(); + + private: + Password* w; +}; + +#endif // HAVE_LIBGPGME + +#endif // PASSWORD_H + diff --git a/src/passwordlayout.ui b/src/passwordlayout.ui new file mode 100644 index 0000000..9cc630d --- /dev/null +++ b/src/passwordlayout.ui @@ -0,0 +1,122 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>PasswordLayout</class> +<widget class="QWidget"> + <property name="name"> + <cstring>PasswordLayout</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>314</width> + <height>69</height> + </rect> + </property> + <property name="caption"> + <string>Password Protection</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QButtonGroup"> + <property name="name"> + <cstring>buttonGroup</cstring> + </property> + <property name="lineWidth"> + <number>0</number> + </property> + <property name="title"> + <string></string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QRadioButton"> + <property name="name"> + <cstring>noPasswordRadioButton</cstring> + </property> + <property name="text"> + <string>&No protection</string> + </property> + <property name="accel"> + <string>Alt+N</string> + </property> + </widget> + <widget class="QRadioButton"> + <property name="name"> + <cstring>passwordRadioButton</cstring> + </property> + <property name="text"> + <string>Protect basket with a &password</string> + </property> + <property name="accel"> + <string>Alt+P</string> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout2</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QRadioButton"> + <property name="name"> + <cstring>publicPrivateRadioButton</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>0</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Protect basket with private &key:</string> + </property> + <property name="accel"> + <string>Alt+K</string> + </property> + </widget> + <widget class="QComboBox"> + <property name="name"> + <cstring>keyCombo</cstring> + </property> + </widget> + </hbox> + </widget> + </vbox> + </widget> + <spacer> + <property name="name"> + <cstring>spacer</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>6</height> + </size> + </property> + </spacer> + </vbox> +</widget> +<slots> + <slot access="private">changeKey()</slot> + <slot access="private">clearKey()</slot> +</slots> +<layoutdefaults spacing="0" margin="0"/> +<layoutfunctions spacing="KDialog::spacingHint"/> +</UI> diff --git a/src/popupmenu.cpp b/src/popupmenu.cpp new file mode 100644 index 0000000..a73aa65 --- /dev/null +++ b/src/popupmenu.cpp @@ -0,0 +1,151 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kapplication.h> +#include <qdesktopwidget.h> +#include <qpopupmenu.h> +#include <qrect.h> + +#include "popupmenu.h" + +#if KDE_IS_VERSION( 3, 2, 90 ) // KDE 3.3.x + #define MENU_Y_OFFSET 1 +#else + #define MENU_Y_OFFSET 2 +#endif + +/** NOTE 1 : This implementation forget BIDI support (eg RightToLeft languages + * expect to try to popup the menu first at bottom-right + * and not at bottom-left. + * NOTE 2 : This implementation do not support virtual desktop with more than + * one screen. Pehrapse QPopupMenu solve it by itself but I can't + * try : I have only one screen. + * => Have those methods directly in Qt (or KDE) would be a great benefits ! + */ + +void PopupMenu::execAtRectCenter(QPopupMenu &menu, const QRect &rect) +{ + // Compute point where to popup the menu that should be centered : + QSize menuSize = menu.sizeHint(); + QSize menuHalfSize = menuSize / 2; + QPoint point = rect.center() - QPoint(menuHalfSize.width(), menuHalfSize.height()); + + // Very strange : menu.exec(point) do that clipping (but not when exec() by mouse !!! ) : + // If menu is partially out of the screen, move it : +/* int desktopWidth = kapp->desktop()->width(); + int desktopHeight = kapp->desktop()->height(); + if (point.x() + menuSize.width() > desktopWidth) point.setX(desktopWidth - menuSize.width()); + if (point.y() + menuSize.height() - 2 > desktopHeight) point.setY(desktopHeight - menuSize.height() + 2); + if (point.x() < 0) point.setX(0); + if (point.y() < 0) point.setY(0);*/ + + // And show the menu : + menu.exec( point + QPoint(0, MENU_Y_OFFSET) ); // Stupid offset (Qt bug ?) : we should show the menus 2 pixels more bottom ! +} + +// Needed on debug to draw the passed global rectangle : +//#include <qpainter.h> +//#include <qpen.h> + +void PopupMenu::execAtRectBottom(QPopupMenu &menu, const QRect &rect, bool centered) +{ + QSize menuSize = menu.sizeHint() - QSize(1, 1); // A size is [1..n] => We want two lengths that are [0..(n-1)] + int desktopWidth = kapp->desktop()->width(); // to be compared/added/substracted with QRects/QPoints... + int desktopHeight = kapp->desktop()->height(); + + /** Paint the rect on the screen (desktop). + * For test purpose only (to be sure the passed rectangle is right). + * Comment this (and the qpainter and qpen includes) for a non-debug version. + */ + /*QPainter paint(kapp->desktop(), kapp->desktop(), true); + paint.setPen( QPen(Qt::black, 1) ); + paint.drawRect(rect); + paint.end();*/ + + // rect.bottomLeft() and rect.bottomRight() must be VISIBLE : + // show the menu 1 pixel more BOTTOM (add 1 in Y) : + QPoint point = rect.bottomLeft() + QPoint(0, 1); + if (point.y() + menuSize.height() < desktopHeight) { // First try at bottom + if (centered) + point = QPoint( rect.center().x() - menuSize.width() / 2, point.y() ); + else if (point.x() + menuSize.width() < desktopWidth) // Then, try at bottom-left + /*point is already set*/; + else // Overwise, at bottom-right + point = rect.bottomRight() - QPoint(menuSize.width(), - 1); + // Idem : rect.topLeft() and rect.topRight() must be VISIBLE : + // show the menu 1 pixel more TOP (substract 1 in Y) : + } else { // Overwize, try at top + if (centered) + point = QPoint( rect.center().x() - menuSize.width() / 2, rect.top() - menuSize.height() - 1 ); + else if (point.x() + menuSize.width() < desktopWidth) // Then, try at top-left + point = rect.topLeft() - QPoint(0, menuSize.height() + 1); + else // Overwise, at top-right + point = rect.topRight() - QPoint(menuSize.width(), menuSize.height() + 1); + } + + // No need to clip : it will be done by menu.exec(...) + + // And show the menu : + menu.exec( point + QPoint(0, MENU_Y_OFFSET) ); // Stupid offset (Qt bug ?) : we should show the menus 2 pixels more bottom ! +} + +void PopupMenu::execAtRectRight(QPopupMenu &menu, const QRect &rect, bool centered) +{ + QSize menuSize = menu.sizeHint() - QSize(1, 1); // A size is [1..n] => We want two lengths that are [0..(n-1)] + int desktopWidth = kapp->desktop()->width(); // to be compared/added/substracted with QRects/QPoints... + int desktopHeight = kapp->desktop()->height(); + + /** Paint the rect on the screen (desktop). + * For test purpose only (to be sure the passed rectangle is right). + * Comment this (and the qpainter and qpen includes) for a non-debug version. + */ + /*QPainter paint(kapp->desktop(), kapp->desktop(), true); + paint.setPen( QPen(Qt::black, 1) ); + paint.drawRect(rect); + paint.end();*/ + + // rect.topRight() and rect.topLeft() must be VISIBLE : + // show the menu 1 pixel more RIGHT (add 1 in X) : + QPoint point = rect.topRight() + QPoint(1, 0); + if (point.x() + menuSize.width() < desktopWidth) { // First try at right + if (centered) + point = QPoint( point.x(), rect.center().y() - menuSize.height() / 2 ); + else if (point.y() + menuSize.height() < desktopHeight) // Then, try at top-right + /*point is already set*/; + else // Overwise, at top-left + point = rect.bottomRight() - QPoint(-1, menuSize.height()); + // Idem : rect.topLeft() and rect.bottomLeft() must be VISIBLE : + // show the menu 1 pixel more LEFT (substract 1 in X) : + } else { // Overwize, try at top + if (centered) + point = QPoint( rect.left() - menuSize.width() - 1, rect.center().y() - menuSize.height() / 2 ); + else if (point.y() + menuSize.height() < desktopHeight) // Then, try at top-left + point = rect.topLeft() - QPoint(menuSize.width() + 1, 0); + else // Overwise, at bottom-left + point = rect.bottomLeft() - QPoint(menuSize.width() + 1, menuSize.height()); + } + + // No need to clip : it will be done by menu.exec(...) + + // And show the menu : + menu.exec( point + QPoint(0, MENU_Y_OFFSET) ); // Stupid offset (Qt bug ?) : we should show the menus 2 pixels more bottom ! +} + +// # i n c l u d e " p o p u p m e n u . m o c " // Comment this if you don't compile PopupMenuTest class diff --git a/src/popupmenu.h b/src/popupmenu.h new file mode 100644 index 0000000..fa92f39 --- /dev/null +++ b/src/popupmenu.h @@ -0,0 +1,123 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef POPUPMENU_H +#define POPUPMENU_H + +class QPopupMenu; +class QRect; + +/** QPopupMenu/KPopupMenu doesn't provide metod to exec a menu + * at a given rectangle ! + * eg, popup at bottom of a rectangle, and at top if not possible... + * @author S�bastien Lao�t + */ +namespace PopupMenu +{ + /** Show the popup menu centered into rect. + */ + void execAtRectCenter(QPopupMenu &menu, const QRect &rect); + + /** Show the popup menu at left-bottom of rect, or at right-bottom + * if not possible (not enought place). + * If it isn't possible to show it at bottom, it will be shown on + * top of rect (top-left if possible, if not it will be top-right). + * If center is true, it will try to horizontaly center the popup with + * rect, so it will try two positions : bottom center and then top center. + */ + void execAtRectBottom(QPopupMenu &menu, const QRect &rect, bool centered = false); + + /** Idem execAtRectBottom but on the right or left sides, + * prior aligned with the top of the rect, and at the bottom + * if not possible. + * If center is true, it will try to vertically center the popup with + * rect, so it will try two positions : right center and then left center. + */ + void execAtRectRight(QPopupMenu &menu, const QRect &rect, bool centered = false); +} + +/** Test window of PopupMenu methods. + * Just include popupmenu.h in a main Qt application and call + * new PopupMenuTest(); + * Click the window for more explications. + * Resize it to test particular cases. + * (Comment the class, if it isn't done yet to do not compile it :-) ). + * @author S�bastien Lao�t + */ + +/***** + +#include <qwidget.h> +#include <qpopupmenu.h> +#include <qpainter.h> +#include <qpen.h> + +c l a s s P o p u p M e n u T e s t : p u b l i c Q W i d g e t +{ + Q _ O B J E C T + p u b l i c: + PopupMenuTest() + : QWidget(0) + { + setCaption("Click to test!"); + show(); + } + + void mousePressEvent(QMouseEvent *event) + { + QPopupMenu menu; + QRect rect( mapToGlobal(QPoint(0,0)), size() ); + + menu.insertItem("A test of popup menu!"); + menu.insertItem("This menu contain some items"); + menu.insertItem("Resize the window as you want and:"); + menu.insertItem("- click : execAtRectCenter"); + menu.insertItem("- right click : execAtRectBottom"); + menu.insertItem("- middle click : execAtRectRight"); + menu.insertItem("- Shift + right click : execAtRectBottom centered"); + menu.insertItem("- Shift + middle click : execAtRectRight centered"); + + if (event->button() & Qt::LeftButton) + PopupMenu::execAtRectCenter(menu, rect); + else if ((event->button() & Qt::RightButton) && (event->state() & Qt::ShiftButton)) + PopupMenu::execAtRectBottom(menu, rect, true); + else if (event->button() & Qt::RightButton) + PopupMenu::execAtRectBottom(menu, rect); + else if ((event->button() & Qt::MidButton) && (event->state() & Qt::ShiftButton)) + PopupMenu::execAtRectRight(menu, rect, true); + else if (event->button() & Qt::MidButton) + PopupMenu::execAtRectRight(menu, rect); + } + + void paintEvent(QPaintEvent*) + { + QPainter paint(this); + paint.setPen(paletteBackgroundColor()); + paint.drawRect(rect()); + paint.drawWinFocusRect(rect()); + paint.setPen( QPen(Qt::black, 1) ); + paint.drawLine( rect().topLeft(), rect().bottomRight() ); + paint.drawLine( rect().topRight(), rect().bottomLeft() ); + } +}; + +*****/ + +#endif // POPUPMENU_H diff --git a/src/qeffects.cpp b/src/qeffects.cpp new file mode 100644 index 0000000..4658373 --- /dev/null +++ b/src/qeffects.cpp @@ -0,0 +1,602 @@ +#if 0 + +// Note: this file has been copied from the Qt source. +// Those classes are normally used internally in Qt +// but we need them for immitate the roll-over effect of QComboBox. +// +// A portion of code has been added. It's underlined by "THIS CODE WAS ADDED:". +// +// And some class definitions have been moved from this file to qeffects.h +// Theire old position is indicated by "REMOVED CLASS DEFINITION HERE (MOVED TO qeffects.h)" + +/**************************************************************************** +** $Id: qt/qeffects.cpp 3.3.4 edited Dec 10 10:13 $ +** +** Implementation of QEffects functions +** +** Created : 000621 +** +** Copyright (C) 2000 Trolltech AS. All rights reserved. +** +** This file is part of the widgets module of the Qt GUI Toolkit. +** +** This file may be distributed under the terms of the Q Public License +** as defined by Trolltech AS of Norway and appearing in the file +** LICENSE.QPL included in the packaging of this file. +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** Licensees holding valid Qt Enterprise Edition or Qt Professional Edition +** licenses may use this file in accordance with the Qt Commercial License +** Agreement provided with the Software. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See http://www.trolltech.com/pricing.html or email [email protected] for +** information about Qt Commercial License Agreements. +** See http://www.trolltech.com/qpl/ for QPL licensing information. +** See http://www.trolltech.com/gpl/ for GPL licensing information. +** +** Contact [email protected] if any conditions of this licensing are +** not clear to you. +** +**********************************************************************/ + +#include "qapplication.h" +#ifndef QT_NO_EFFECTS +#include "qwidget.h" +#include "qeffects.h" +#include "qpixmap.h" +#include "qimage.h" +#include "qtimer.h" +#include "qdatetime.h" +#include "qguardedptr.h" +#include "qscrollview.h" + +/* REMOVED CLASS DEFINITION HERE (MOVED TO qeffects.h) */ + +static QAlphaWidget* q_blend = 0; + +/* + Constructs a QAlphaWidget. +*/ +QAlphaWidget::QAlphaWidget( QWidget* w, WFlags f ) + : QWidget( QApplication::desktop()->screen(QApplication::desktop()->screenNumber(w)), + "qt internal alpha effect widget", f ) +{ +#if 1 //ndef Q_WS_WIN + setEnabled( FALSE ); +#endif + + pm.setOptimization( QPixmap::BestOptim ); + setBackgroundMode( NoBackground ); + widget = (QAccessWidget*)w; + alpha = 0; +} + +/* + \reimp +*/ +void QAlphaWidget::paintEvent( QPaintEvent* ) +{ + bitBlt( this, QPoint(0,0), &pm ); +} + +/* + Starts the alphablending animation. + The animation will take about \a time ms +*/ +void QAlphaWidget::run( int time ) +{ + duration = time; + + if ( duration < 0 ) + duration = 150; + + if ( !widget ) + return; + + elapsed = 0; + checkTime.start(); + + showWidget = TRUE; + qApp->installEventFilter( this ); + + widget->setWState( WState_Visible ); + + move( widget->geometry().x(),widget->geometry().y() ); + resize( widget->size().width(), widget->size().height() ); + + front = QImage( widget->size(), 32 ); + front = QPixmap::grabWidget( widget ); + + back = QImage( widget->size(), 32 ); + back = QPixmap::grabWindow( QApplication::desktop()->winId(), + widget->geometry().x(), widget->geometry().y(), + widget->geometry().width(), widget->geometry().height() ); + + if ( !back.isNull() && checkTime.elapsed() < duration / 2 ) { + mixed = back.copy(); + pm = mixed; + show(); + setEnabled(FALSE); + + connect( &anim, SIGNAL(timeout()), this, SLOT(render())); + anim.start( 1 ); + } else { + duration = 0; + render(); + } +} + +/* + \reimp +*/ +bool QAlphaWidget::eventFilter( QObject* o, QEvent* e ) +{ + switch ( e->type() ) { + case QEvent::Move: + if ( o != widget ) + break; + move( widget->geometry().x(),widget->geometry().y() ); + update(); + break; + case QEvent::Hide: + case QEvent::Close: + if ( o != widget ) + break; + case QEvent::MouseButtonPress: +#ifndef QT_NO_SCROLLVIEW + if ( ::qt_cast<QScrollView*>(o) ) + break; +#endif + case QEvent::MouseButtonDblClick: + setEnabled(TRUE); + showWidget = FALSE; + render(); + break; + case QEvent::KeyPress: + { + QKeyEvent *ke = (QKeyEvent*)e; + if ( ke->key() == Key_Escape ) + showWidget = FALSE; + else + duration = 0; + render(); + break; + } + default: + break; + } + return QWidget::eventFilter( o, e ); +} + +/* + \reimp +*/ +void QAlphaWidget::closeEvent( QCloseEvent *e ) +{ + e->accept(); + if ( !q_blend ) + return; + + showWidget = FALSE; + render(); + + QWidget::closeEvent( e ); +} + +/* + Render alphablending for the time elapsed. + + Show the blended widget and free all allocated source + if the blending is finished. +*/ +void QAlphaWidget::render() +{ + int tempel = checkTime.elapsed(); + if ( elapsed >= tempel ) + elapsed++; + else + elapsed = tempel; + + if ( duration != 0 ) + alpha = tempel / double(duration); + else + alpha = 1; + if ( alpha >= 1 || !showWidget) { + anim.stop(); + qApp->removeEventFilter( this ); + + if ( widget ) { + if ( !showWidget ) { +#ifdef Q_WS_WIN + setEnabled(TRUE); + setFocus(); +#endif + widget->hide(); + widget->setWState( WState_ForceHide ); + widget->clearWState( WState_Visible ); + } else if ( duration ) { + BackgroundMode bgm = widget->backgroundMode(); + QColor erc = widget->eraseColor(); + const QPixmap *erp = widget->erasePixmap(); + + widget->clearWState( WState_Visible ); + widget->setBackgroundMode( NoBackground ); + widget->show(); + if ( bgm != FixedColor && bgm != FixedPixmap ) { + widget->clearWState( WState_Visible ); // prevent update in setBackgroundMode + widget->setBackgroundMode( bgm ); + widget->setWState( WState_Visible ); + } + if ( erc.isValid() ) { + widget->setEraseColor( erc ); + } else if ( erp ) { + widget->setErasePixmap( *erp ); + } + } else { + widget->clearWState( WState_Visible ); + widget->show(); + } + } + q_blend = 0; + deleteLater(); + } else { + if (widget) + widget->clearWState( WState_ForceHide ); + alphaBlend(); + pm = mixed; + repaint( FALSE ); + } +} + +/* + Calculate an alphablended image. +*/ +void QAlphaWidget::alphaBlend() +{ + const double ia = 1-alpha; + const int sw = front.width(); + const int sh = front.height(); + switch( front.depth() ) { + case 32: + { + Q_UINT32** md = (Q_UINT32**)mixed.jumpTable(); + Q_UINT32** bd = (Q_UINT32**)back.jumpTable(); + Q_UINT32** fd = (Q_UINT32**)front.jumpTable(); + + for (int sy = 0; sy < sh; sy++ ) { + Q_UINT32* bl = ((Q_UINT32*)bd[sy]); + Q_UINT32* fl = ((Q_UINT32*)fd[sy]); + for (int sx = 0; sx < sw; sx++ ) { + Q_UINT32 bp = bl[sx]; + Q_UINT32 fp = fl[sx]; + + ((Q_UINT32*)(md[sy]))[sx] = qRgb(int (qRed(bp)*ia + qRed(fp)*alpha), + int (qGreen(bp)*ia + qGreen(fp)*alpha), + int (qBlue(bp)*ia + qBlue(fp)*alpha) ); + } + } + } + default: + break; + } +} + +/* REMOVED CLASS DEFINITION HERE (MOVED TO qeffects.h) */ + +static QRollEffect* q_roll = 0; + +/* + Construct a QRollEffect widget. +*/ +QRollEffect::QRollEffect( QWidget* w, WFlags f, DirFlags orient ) + : QWidget( QApplication::desktop()->screen(QApplication::desktop()->screenNumber(w)), + "qt internal roll effect widget", f ), orientation(orient) +{ +#if 1 //ndef Q_WS_WIN + setEnabled( FALSE ); +#endif + widget = (QAccessWidget*) w; + Q_ASSERT( widget ); + + setBackgroundMode( NoBackground ); + + if ( widget->testWState( WState_Resized ) ) { + totalWidth = widget->width(); + totalHeight = widget->height(); + } else { + totalWidth = widget->sizeHint().width(); + totalHeight = widget->sizeHint().height(); + } + + currentHeight = totalHeight; + currentWidth = totalWidth; + + if ( orientation & (RightScroll|LeftScroll) ) + currentWidth = 0; + if ( orientation & (DownScroll|UpScroll) ) + currentHeight = 0; + + pm.setOptimization( QPixmap::BestOptim ); + pm = QPixmap::grabWidget( widget ); +} + +/* + \reimp +*/ +void QRollEffect::paintEvent( QPaintEvent* ) +{ + int x = orientation & RightScroll ? QMIN(0, currentWidth - totalWidth) : 0; + int y = orientation & DownScroll ? QMIN(0, currentHeight - totalHeight) : 0; + + bitBlt( this, x, y, &pm, + 0, 0, pm.width(), pm.height(), CopyROP, TRUE ); +} + +/* + \reimp +*/ +bool QRollEffect::eventFilter( QObject* o, QEvent* e ) +{ + switch ( e->type() ) { + case QEvent::Move: + if ( o != widget ) + break; + move( widget->geometry().x(),widget->geometry().y() ); + update(); + break; + case QEvent::Hide: + case QEvent::Close: + if ( o != widget || done ) + break; + setEnabled(TRUE); + showWidget = FALSE; + done = TRUE; + scroll(); + break; + case QEvent::MouseButtonPress: +#ifndef QT_NO_SCROLLVIEW + if ( ::qt_cast<QScrollView*>(o) ) + break; +#endif + case QEvent::MouseButtonDblClick: + if ( done ) + break; + setEnabled(TRUE); + showWidget = FALSE; + done = TRUE; + scroll(); + break; + case QEvent::KeyPress: + { + QKeyEvent *ke = (QKeyEvent*)e; + + + /* THIS CODE WAS ADDED: ************** */ + if (ke->key() == Qt::Key_Enter) /*** Because we are simulating an Enter key press. */ + break; /*** So we should not car about it and continue the animation. */ + /************************************* */ + + + if ( ke->key() == Key_Escape ) + showWidget = FALSE; + done = TRUE; + scroll(); + break; + } + default: + break; + } + return QWidget::eventFilter( o, e ); +} + +/* + \reimp +*/ +void QRollEffect::closeEvent( QCloseEvent *e ) +{ + e->accept(); + if ( done ) + return; + showWidget = FALSE; + done = TRUE; + scroll(); + + QWidget::closeEvent( e ); +} + +/* + Start the animation. + + The animation will take about \a time ms, or is + calculated if \a time is negative +*/ +void QRollEffect::run( int time ) +{ + if ( !widget ) + return; + + duration = time; + elapsed = 0; + + if ( duration < 0 ) { + int dist = 0; + if ( orientation & (RightScroll|LeftScroll) ) + dist += totalWidth - currentWidth; + if ( orientation & (DownScroll|UpScroll) ) + dist += totalHeight - currentHeight; + duration = QMIN( QMAX( dist/3, 50 ), 120 ); + } + + connect( &anim, SIGNAL(timeout()), this, SLOT(scroll())); + + widget->setWState( WState_Visible ); + + move( widget->geometry().x(),widget->geometry().y() ); + resize( QMIN( currentWidth, totalWidth ), QMIN( currentHeight, totalHeight ) ); + + show(); + setEnabled(FALSE); + + qApp->installEventFilter( this ); + + showWidget = TRUE; + done = FALSE; + anim.start( 1 ); + checkTime.start(); +} + +/* + Roll according to the time elapsed. +*/ +void QRollEffect::scroll() +{ + if ( !done && widget) { + widget->clearWState( WState_ForceHide ); + int tempel = checkTime.elapsed(); + if ( elapsed >= tempel ) + elapsed++; + else + elapsed = tempel; + + if ( currentWidth != totalWidth ) { + currentWidth = totalWidth * (elapsed/duration) + + ( 2 * totalWidth * (elapsed%duration) + duration ) + / ( 2 * duration ); + // equiv. to int( (totalWidth*elapsed) / duration + 0.5 ) + done = (currentWidth >= totalWidth); + } + if ( currentHeight != totalHeight ) { + currentHeight = totalHeight * (elapsed/duration) + + ( 2 * totalHeight * (elapsed%duration) + duration ) + / ( 2 * duration ); + // equiv. to int( (totalHeight*elapsed) / duration + 0.5 ) + done = (currentHeight >= totalHeight); + } + done = ( currentHeight >= totalHeight ) && + ( currentWidth >= totalWidth ); + + int w = totalWidth; + int h = totalHeight; + int x = widget->geometry().x(); + int y = widget->geometry().y(); + + if ( orientation & RightScroll || orientation & LeftScroll ) + w = QMIN( currentWidth, totalWidth ); + if ( orientation & DownScroll || orientation & UpScroll ) + h = QMIN( currentHeight, totalHeight ); + + setUpdatesEnabled( FALSE ); + if ( orientation & UpScroll ) + y = widget->geometry().y() + QMAX( 0, totalHeight - currentHeight ); + if ( orientation & LeftScroll ) + x = widget->geometry().x() + QMAX( 0, totalWidth - currentWidth ); + if ( orientation & UpScroll || orientation & LeftScroll ) + move( x, y ); + + resize( w, h ); + setUpdatesEnabled( TRUE ); + repaint( FALSE ); + } + if ( done ) { + anim.stop(); + qApp->removeEventFilter( this ); + if ( widget ) { + if ( !showWidget ) { +#ifdef Q_WS_WIN + setEnabled(TRUE); + setFocus(); +#endif + widget->hide(); + widget->setWState( WState_ForceHide ); + widget->clearWState( WState_Visible ); + } else { + BackgroundMode bgm = widget->backgroundMode(); + QColor erc = widget->eraseColor(); + const QPixmap *erp = widget->erasePixmap(); + + widget->clearWState( WState_Visible ); + widget->setBackgroundMode( NoBackground ); + widget->show(); + if ( bgm != FixedColor && bgm != FixedPixmap ) { + widget->clearWState( WState_Visible ); // prevent update in setBackgroundMode + widget->setBackgroundMode( bgm ); + widget->setWState( WState_Visible ); + } + if ( erc.isValid() ) { + widget->setEraseColor( erc ); + } else if ( erp ) { + widget->setErasePixmap( *erp ); + } + } + } + q_roll = 0; + deleteLater(); + } +} + +/* + Delete this after timeout +*/ + +#include "qeffects.moc" + +/*! + Scroll widget \a w in \a time ms. \a orient may be 1 (vertical), 2 + (horizontal) or 3 (diagonal). + */ +void qScrollEffect( QWidget* w, QEffects::DirFlags orient, int time ) +{ + if ( q_roll ) { + delete q_roll; + q_roll = 0; + } + + qApp->sendPostedEvents( w, QEvent::Move ); + qApp->sendPostedEvents( w, QEvent::Resize ); +#ifdef Q_WS_X11 + uint flags = Qt::WStyle_Customize | Qt::WNoAutoErase | Qt::WStyle_StaysOnTop + | (w->isPopup() ? Qt::WType_Popup : (Qt::WX11BypassWM | Qt::WStyle_Tool)); +#else + uint flags = Qt::WStyle_Customize | Qt::WType_Popup | Qt::WX11BypassWM | Qt::WNoAutoErase | Qt::WStyle_StaysOnTop; +#endif + + // those can popups - they would steal the focus, but are disabled + q_roll = new QRollEffect( w, flags, orient ); + q_roll->run( time ); +} + +/*! + Fade in widget \a w in \a time ms. + */ +void qFadeEffect( QWidget* w, int time ) +{ + if ( q_blend ) { + delete q_blend; + q_blend = 0; + } + + qApp->sendPostedEvents( w, QEvent::Move ); + qApp->sendPostedEvents( w, QEvent::Resize ); + +#ifdef Q_WS_X11 + uint flags = Qt::WStyle_Customize | Qt::WNoAutoErase | Qt::WStyle_StaysOnTop + | (w->isPopup() ? Qt::WType_Popup : (Qt::WX11BypassWM | Qt::WStyle_Tool)); +#else + uint flags = Qt::WStyle_Customize | Qt::WType_Popup | Qt::WX11BypassWM | Qt::WNoAutoErase | Qt::WStyle_StaysOnTop; +#endif + + // those can popups - they would steal the focus, but are disabled + q_blend = new QAlphaWidget( w, flags ); + + q_blend->run( time ); +} +#endif //QT_NO_EFFECTS + +#endif // #if 0 diff --git a/src/qeffects.h b/src/qeffects.h new file mode 100644 index 0000000..b898ad4 --- /dev/null +++ b/src/qeffects.h @@ -0,0 +1,195 @@ +#if 0 + +// Note: this file has been copied from the Qt source. +// Those classes are normally used internally in Qt +// but we need them for immitate the roll-over effect of QComboBox. +// +// Some class definitions have been moved from qeffects.cpp to this file. +// They are framed with the comment "MOVED FROM qeffect.cpp" + +/**************************************************************************** +** $Id: qt/qeffects_p.h 3.3.4 edited May 27 2003 $ +** +** Definition of QEffects functions +** +** Created : 000621 +** +** Copyright (C) 2000 Trolltech AS. All rights reserved. +** +** This file is part of the widgets module of the Qt GUI Toolkit. +** +** This file may be distributed under the terms of the Q Public License +** as defined by Trolltech AS of Norway and appearing in the file +** LICENSE.QPL included in the packaging of this file. +** +** This file may be distributed and/or modified under the terms of the +** GNU General Public License version 2 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. +** +** Licensees holding valid Qt Enterprise Edition or Qt Professional Edition +** licenses may use this file in accordance with the Qt Commercial License +** Agreement provided with the Software. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +** See http://www.trolltech.com/pricing.html or email [email protected] for +** information about Qt Commercial License Agreements. +** See http://www.trolltech.com/qpl/ for QPL licensing information. +** See http://www.trolltech.com/gpl/ for GPL licensing information. +** +** Contact [email protected] if any conditions of this licensing are +** not clear to you. +** +**********************************************************************/ + +#ifndef QEFFECTS_P_H +#define QEFFECTS_P_H + + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of qeffects.cpp, qcombobox.cpp, qpopupmenu.cpp and qtooltip.cpp. +// This header file may change from version to version without notice, +// or even be removed. +// +// We mean it. +// +// + +#ifndef QT_H +#include "qnamespace.h" +#endif // QT_H + +#ifndef QT_NO_EFFECTS +class QWidget; + +struct QEffects +{ + enum Direction { + LeftScroll = 0x0001, + RightScroll = 0x0002, + UpScroll = 0x0004, + DownScroll = 0x0008 + }; + + typedef uint DirFlags; +}; + +extern void Q_EXPORT qScrollEffect( QWidget*, QEffects::DirFlags dir = QEffects::DownScroll, int time = -1 ); +extern void Q_EXPORT qFadeEffect( QWidget*, int time = -1 ); + + +/******************* MOVED FROM qeffect.cpp: */ + +#include "qguardedptr.h" +#include "qdatetime.h" +#include "qtimer.h" +#include "qpixmap.h" +#include "qimage.h" + +/* + Internal class to get access to protected QWidget-members +*/ + +class QAccessWidget : public QWidget +{ + friend class QAlphaWidget; + friend class QRollEffect; + public: + QAccessWidget( QWidget* parent=0, const char* name=0, WFlags f = 0 ) + : QWidget( parent, name, f ) {} +}; + +/* + Internal class QAlphaWidget. + + The QAlphaWidget is shown while the animation lasts + and displays the pixmap resulting from the alpha blending. +*/ + +class QAlphaWidget: public QWidget, private QEffects +{ + Q_OBJECT + public: + QAlphaWidget( QWidget* w, WFlags f = 0 ); + + void run( int time ); + + protected: + void paintEvent( QPaintEvent* e ); + void closeEvent( QCloseEvent* ); + bool eventFilter( QObject* o, QEvent* e ); + void alphaBlend(); + + protected slots: + void render(); + + private: + QPixmap pm; + double alpha; + QImage back; + QImage front; + QImage mixed; + QGuardedPtr<QAccessWidget> widget; + int duration; + int elapsed; + bool showWidget; + QTimer anim; + QTime checkTime; +}; + +/* + Internal class QRollEffect + + The QRollEffect widget is shown while the animation lasts + and displays a scrolling pixmap. +*/ + +class QRollEffect : public QWidget, private QEffects +{ + Q_OBJECT + public: + QRollEffect( QWidget* w, WFlags f, DirFlags orient ); + + void run( int time ); + + protected: + void paintEvent( QPaintEvent* ); + bool eventFilter( QObject*, QEvent* ); + void closeEvent( QCloseEvent* ); + + private slots: + void scroll(); + + private: + QGuardedPtr<QAccessWidget> widget; + + int currentHeight; + int currentWidth; + int totalHeight; + int totalWidth; + + int duration; + int elapsed; + bool done; + bool showWidget; + int orientation; + + QTimer anim; + QTime checkTime; + + QPixmap pm; +}; + +/******************************/ + +#endif // QT_NO_EFFECTS + +#endif // QEFFECTS_P_H + +#endif // #if 0 diff --git a/src/regiongrabber.cpp b/src/regiongrabber.cpp new file mode 100644 index 0000000..f7924e6 --- /dev/null +++ b/src/regiongrabber.cpp @@ -0,0 +1,180 @@ +// Code from KSnapshot! + +/* + Copyright (C) 2003 Nadeem Hasan <[email protected]> + + 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. + + 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 + Library General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this library; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "regiongrabber.h" + +#include <qapplication.h> +#include <qpainter.h> +#include <qpalette.h> +#include <qstyle.h> +#include <qtimer.h> +#include <qtooltip.h> + +#include <kglobalsettings.h> + +SizeTip::SizeTip( QWidget *parent, const char *name ) + : QLabel( parent, name, WStyle_Customize | WX11BypassWM | + WStyle_StaysOnTop | WStyle_NoBorder | WStyle_Tool ) +{ + setMargin( 2 ); + setIndent( 0 ); + setFrameStyle( QFrame::Plain | QFrame::Box ); + + setPalette( QToolTip::palette() ); +} + +void SizeTip::setTip( const QRect &rect ) +{ + QString tip = QString( "%1x%2" ).arg( rect.width() ) + .arg( rect.height() ); + + setText( tip ); + adjustSize(); + + positionTip( rect ); +} + +void SizeTip::positionTip( const QRect &rect ) +{ + QRect tipRect = geometry(); + tipRect.moveTopLeft( QPoint( 0, 0 ) ); + + if ( rect.intersects( tipRect ) ) + { + QRect deskR = KGlobalSettings::desktopGeometry( QPoint( 0, 0 ) ); + + tipRect.moveCenter( QPoint( deskR.width()/2, deskR.height()/2 ) ); + if ( !rect.contains( tipRect, true ) && rect.intersects( tipRect ) ) + tipRect.moveBottomRight( geometry().bottomRight() ); + } + + move( tipRect.topLeft() ); +} + +RegionGrabber::RegionGrabber(int delay) + : QWidget( 0, 0 ), + mouseDown( false ), sizeTip( 0L ) +{ + sizeTip = new SizeTip( ( QWidget * )0L ); + + tipTimer = new QTimer( this ); + connect( tipTimer, SIGNAL( timeout() ), SLOT( updateSizeTip() ) ); + + QTimer::singleShot( delay, this, SLOT( initGrabber() ) ); +} + +RegionGrabber::~RegionGrabber() +{ + delete sizeTip; +} + +void RegionGrabber::initGrabber() +{ + pixmap = QPixmap::grabWindow( qt_xrootwin() ); + setPaletteBackgroundPixmap( pixmap ); + + QDesktopWidget desktopWidget; + QRect desktopSize; + if ( desktopWidget.isVirtualDesktop() ) + desktopSize = desktopWidget.geometry(); + else + desktopSize = desktopWidget.screenGeometry( qt_xrootwin() ); + + setGeometry( desktopSize ); + showFullScreen(); + + QApplication::setOverrideCursor( crossCursor ); +} + +void RegionGrabber::mousePressEvent( QMouseEvent *e ) +{ + if ( e->button() == LeftButton ) + { + mouseDown = true; + grabRect = QRect( e->pos(), e->pos() ); + drawRubber(); + } +} + +void RegionGrabber::mouseMoveEvent( QMouseEvent *e ) +{ + if ( mouseDown ) + { + sizeTip->hide(); + tipTimer->start( 250, true ); + + drawRubber(); + grabRect.setBottomRight( e->pos() ); + drawRubber(); + } +} + +void RegionGrabber::mouseReleaseEvent( QMouseEvent *e ) +{ + mouseDown = false; + drawRubber(); + sizeTip->hide(); + + grabRect.setBottomRight( e->pos() ); + grabRect = grabRect.normalize(); + + QPixmap region = QPixmap::grabWindow( winId(), grabRect.x(), grabRect.y(), + grabRect.width(), grabRect.height() ); + + QApplication::restoreOverrideCursor(); + + emit regionGrabbed( region ); +} + +void RegionGrabber::keyPressEvent( QKeyEvent *e ) +{ + if ( e->key() == Key_Escape ) + { + QApplication::restoreOverrideCursor(); + emit regionGrabbed( QPixmap() ); + } + else + e->ignore(); +} + +void RegionGrabber::updateSizeTip() +{ + QRect rect = grabRect.normalize(); + + sizeTip->setTip( rect ); + sizeTip->show(); +} + +void RegionGrabber::drawRubber() +{ + QPainter p; + p.begin( this ); + p.setRasterOp( NotROP ); + p.setPen( QPen( color0, 1 ) ); + p.setBrush( NoBrush ); + + style().drawPrimitive( QStyle::PE_FocusRect, &p, grabRect, colorGroup(), + QStyle::Style_Default, QStyleOption( colorGroup().base() ) ); + + p.end(); +} + +#include "regiongrabber.moc" diff --git a/src/regiongrabber.h b/src/regiongrabber.h new file mode 100644 index 0000000..528c4ff --- /dev/null +++ b/src/regiongrabber.h @@ -0,0 +1,72 @@ +// Code from KSnapshot! + +/* + Copyright (C) 2003 Nadeem Hasan <[email protected]> + + 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. + + 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 + Library General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this library; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef REGIONGRABBER_H +#define REGIONGRABBER_H + +#include <qlabel.h> +#include <qpixmap.h> + +class QTimer; + +class SizeTip : public QLabel +{ + public: + SizeTip( QWidget *parent, const char *name=0 ); + ~SizeTip() {} + + void setTip( const QRect &rect ); + void positionTip( const QRect &rect ); +}; + +class RegionGrabber : public QWidget +{ + Q_OBJECT + + public: + RegionGrabber(int delay); + ~RegionGrabber(); + + protected slots: + void initGrabber(); + void updateSizeTip(); + + signals: + void regionGrabbed( const QPixmap & ); + + protected: + void mousePressEvent( QMouseEvent *e ); + void mouseReleaseEvent( QMouseEvent *e ); + void mouseMoveEvent( QMouseEvent *e ); + void keyPressEvent( QKeyEvent *e ); + + void drawRubber(); + + bool mouseDown; + QRect grabRect; + QPixmap pixmap; + + SizeTip *sizeTip; + QTimer *tipTimer; +}; + +#endif // REGIONGRABBER_H + diff --git a/src/settings.cpp b/src/settings.cpp new file mode 100644 index 0000000..218dd0a --- /dev/null +++ b/src/settings.cpp @@ -0,0 +1,991 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <config.h> +#include <qlayout.h> +#include <qlineedit.h> +#include <qhbox.h> +#include <qvbox.h> +#include <qtabwidget.h> +#include <qgroupbox.h> +#include <qlabel.h> +#include <qpushbutton.h> +#include <knuminput.h> +#include <kcolorcombo.h> +#include <kiconloader.h> +#include <kconfig.h> +#include <kglobal.h> +#include <klocale.h> +#include <qwhatsthis.h> +#include <qbuttongroup.h> +#include <qradiobutton.h> +#include <qvbuttongroup.h> +#include <kapplication.h> +#include <kaboutdata.h> +#include <kmimetype.h> +#include <kstandarddirs.h> +#include <kgpgme.h> +#include <kdebug.h> +#include <qdatetime.h> + +#include "basket.h" +#include "linklabel.h" +#include "settings.h" +#include "variouswidgets.h" +#include "note.h" + +/** Settings */ + +// General: // TODO: Use this grouping everywhere! +bool Settings::s_useSystray = true; +bool Settings::s_usePassivePopup = true; +bool Settings::s_playAnimations = true; +bool Settings::s_showNotesToolTip = true; // TODO: RENAME: useBasketTooltips +bool Settings::s_confirmNoteDeletion = true; +bool Settings::s_bigNotes = false; +bool Settings::s_autoBullet = true; +bool Settings::s_exportTextTags = true; +bool Settings::s_useGnuPGAgent = false; +bool Settings::s_treeOnLeft = true; +bool Settings::s_filterOnTop = true; +int Settings::s_defImageX = 300; +int Settings::s_defImageY = 200; +bool Settings::s_enableReLockTimeout = true; +int Settings::s_reLockTimeoutMinutes = 0; +int Settings::s_newNotesPlace = 1; +int Settings::s_viewTextFileContent = false; +int Settings::s_viewHtmlFileContent = false; +int Settings::s_viewImageFileContent = false; +int Settings::s_viewSoundFileContent = false; +// Applications: +bool Settings::s_htmlUseProg = false; // TODO: RENAME: s_*App (with KService!) +bool Settings::s_imageUseProg = true; +bool Settings::s_animationUseProg = true; +bool Settings::s_soundUseProg = false; +QString Settings::s_htmlProg = "quanta"; +QString Settings::s_imageProg = "kolourpaint"; +QString Settings::s_animationProg = "gimp"; +QString Settings::s_soundProg = ""; +// Addictive Features: +bool Settings::s_groupOnInsertionLine = false; +int Settings::s_middleAction = 0; +bool Settings::s_showIconInSystray = false; // TODO: RENAME: basketIconInSystray +bool Settings::s_hideOnMouseOut = false; +int Settings::s_timeToHideOnMouseOut = 0; +bool Settings::s_showOnMouseIn = false; +int Settings::s_timeToShowOnMouseIn = 1; +// Rememberings: +int Settings::s_defIconSize = 32; // TODO: RENAME: importIconSize +bool Settings::s_blinkedFilter = false; +bool Settings::s_startDocked = false; +int Settings::s_basketTreeWidth = -1; +bool Settings::s_welcomeBasketsAdded = false; +QString Settings::s_dataFolder = ""; +QDate Settings::s_lastBackup = QDate(); +QPoint Settings::s_mainWindowPosition = QPoint(); +QSize Settings::s_mainWindowSize = QSize(); +bool Settings::s_showEmptyBasketInfo = true; +bool Settings::s_spellCheckTextNotes = true; + +void Settings::loadConfig() +{ + LinkLook defaultSoundLook; + LinkLook defaultFileLook; + LinkLook defaultLocalLinkLook; + LinkLook defaultNetworkLinkLook; + LinkLook defaultLauncherLook; /* italic bold underlining color hoverColor iconSize preview */ + defaultSoundLook.setLook( false, false, LinkLook::Never, QColor(), QColor(), 32, LinkLook::None ); + defaultFileLook.setLook( false, false, LinkLook::Never, QColor(), QColor(), 32, LinkLook::TwiceIconSize ); + defaultLocalLinkLook.setLook( true, false, LinkLook::OnMouseHover, QColor(), QColor(), 22, LinkLook::TwiceIconSize ); + defaultNetworkLinkLook.setLook( false, false, LinkLook::OnMouseOutside, QColor(), QColor(), 16, LinkLook::None ); + defaultLauncherLook.setLook( false, true, LinkLook::Never, QColor(), QColor(), 48, LinkLook::None ); + + loadLinkLook(LinkLook::soundLook, "Sound Look", defaultSoundLook ); + loadLinkLook(LinkLook::fileLook, "File Look", defaultFileLook ); + loadLinkLook(LinkLook::localLinkLook, "Local Link Look", defaultLocalLinkLook ); + loadLinkLook(LinkLook::networkLinkLook, "Network Link Look", defaultNetworkLinkLook); + loadLinkLook(LinkLook::launcherLook, "Launcher Look", defaultLauncherLook ); + + KConfig* config = Global::config(); + config->setGroup("Main window"); // TODO: Split with a "System tray icon" group ! + setTreeOnLeft( config->readBoolEntry("treeOnLeft", true) ); + setFilterOnTop( config->readBoolEntry("filterOnTop", true) ); + setPlayAnimations( config->readBoolEntry("playAnimations", true) ); + setShowNotesToolTip( config->readBoolEntry("showNotesToolTip", true) ); + setBigNotes( config->readBoolEntry("bigNotes", false) ); + setConfirmNoteDeletion( config->readBoolEntry("confirmNoteDeletion", true) ); + setAutoBullet( config->readBoolEntry("autoBullet", true) ); + setExportTextTags( config->readBoolEntry("exportTextTags", true) ); + setUseGnuPGAgent( config->readBoolEntry("useGnuPGAgent", false) ); + setBlinkedFilter( config->readBoolEntry("blinkedFilter", false) ); + setEnableReLockTimeout( config->readNumEntry( "enableReLockTimeout", true) ); + setReLockTimeoutMinutes( config->readNumEntry( "reLockTimeoutMinutes", 0) ); + setUseSystray( config->readBoolEntry("useSystray", true) ); + setShowIconInSystray( config->readBoolEntry("showIconInSystray", false) ); + setStartDocked( config->readBoolEntry("startDocked", false) ); + setMiddleAction( config->readNumEntry( "middleAction", 0) ); + setGroupOnInsertionLine( config->readBoolEntry("groupOnInsertionLine", false) ); + setSpellCheckTextNotes( config->readBoolEntry("spellCheckTextNotes", true) ); + setHideOnMouseOut( config->readBoolEntry("hideOnMouseOut", false) ); + setTimeToHideOnMouseOut( config->readNumEntry( "timeToHideOnMouseOut", 0) ); + setShowOnMouseIn( config->readBoolEntry("showOnMouseIn", false) ); + setTimeToShowOnMouseIn( config->readNumEntry( "timeToShowOnMouseIn", 1) ); + setBasketTreeWidth( config->readNumEntry( "basketTreeWidth", -1) ); + setUsePassivePopup( config->readBoolEntry("usePassivePopup", true) ); + setWelcomeBasketsAdded( config->readBoolEntry("welcomeBasketsAdded", false) ); + setDataFolder( config->readPathEntry("dataFolder", "") ); + setLastBackup( config->readDateTimeEntry("lastBackup", new QDateTime()).date()); + setMainWindowPosition( config->readPointEntry("position" ) ); + setMainWindowSize( config->readSizeEntry( "size" ) ); + + config->setGroup("Notification Messages"); + setShowEmptyBasketInfo( config->readBoolEntry("emptyBasketInfo", true) ); + + config->setGroup("Programs"); + setIsHtmlUseProg( config->readBoolEntry("htmlUseProg", false) ); + setIsImageUseProg( config->readBoolEntry("imageUseProg", true) ); + setIsAnimationUseProg( config->readBoolEntry("animationUseProg", true) ); + setIsSoundUseProg( config->readBoolEntry("soundUseProg", false) ); + setHtmlProg( config->readEntry( "htmlProg", "quanta") ); + setImageProg( config->readEntry( "imageProg", "kolourpaint") ); + setAnimationProg( config->readEntry( "animationProg", "gimp") ); + setSoundProg( config->readEntry( "soundProg", "") ); + + config->setGroup("Note Addition"); + setNewNotesPlace( config->readNumEntry( "newNotesPlace", 1) ); + setViewTextFileContent( config->readBoolEntry("viewTextFileContent", false) ); + setViewHtmlFileContent( config->readBoolEntry("viewHtmlFileContent", false) ); + setViewImageFileContent( config->readBoolEntry("viewImageFileContent", true) ); + setViewSoundFileContent( config->readBoolEntry("viewSoundFileContent", true) ); + + config->setGroup("Insert Note Default Values"); + setDefImageX( config->readNumEntry( "defImageX", 300) ); + setDefImageY( config->readNumEntry( "defImageY", 200) ); + setDefIconSize( config->readNumEntry( "defIconSize", 32) ); + + config->setGroup("MainWindow Toolbar mainToolBar"); + // The first time we start, we define "Text Alongside Icons" for the main toolbar. + // After that, the user is free to hide the text from the icons or customize as he/she want. + // But it is a good default (Fitt's Laws, better looking, less "empty"-feeling), especially for this application. +// if (!config->readBoolEntry("alreadySetIconTextRight", false)) { +// config->writeEntry( "IconText", "IconTextRight" ); +// config->writeEntry( "alreadySetIconTextRight", true ); +// } + if (!config->readBoolEntry("alreadySetToolbarSettings", false)) { + config->writeEntry("IconText", "IconOnly"); // In 0.6.0 Alpha versions, it was made "IconTextRight". We're back to IconOnly + config->writeEntry("Index", "0"); // Make sure the main toolbar is the first... + config->setGroup("MainWindow Toolbar richTextEditToolBar"); + config->writeEntry("Position", "Top"); // In 0.6.0 Alpha versions, it was made "Bottom" + config->writeEntry("Index", "1"); // ... and the rich text toolbar is on the right of the main toolbar + config->setGroup("MainWindow Toolbar mainToolBar"); + config->writeEntry("alreadySetToolbarSettings", true); + } +} + +void Settings::saveConfig() +{ + saveLinkLook(LinkLook::soundLook, "Sound Look" ); + saveLinkLook(LinkLook::fileLook, "File Look" ); + saveLinkLook(LinkLook::localLinkLook, "Local Link Look" ); + saveLinkLook(LinkLook::networkLinkLook, "Network Link Look"); + saveLinkLook(LinkLook::launcherLook, "Launcher Look" ); + + KConfig* config = Global::config(); + config->setGroup("Main window"); + config->writeEntry( "treeOnLeft", treeOnLeft() ); + config->writeEntry( "filterOnTop", filterOnTop() ); + config->writeEntry( "playAnimations", playAnimations() ); + config->writeEntry( "showNotesToolTip", showNotesToolTip() ); + config->writeEntry( "confirmNoteDeletion", confirmNoteDeletion() ); + config->writeEntry( "bigNotes", bigNotes() ); + config->writeEntry( "autoBullet", autoBullet() ); + config->writeEntry( "exportTextTags", exportTextTags() ); +#ifdef HAVE_LIBGPGME + if (KGpgMe::isGnuPGAgentAvailable()) + config->writeEntry( "useGnuPGAgent", useGnuPGAgent() ); +#endif + config->writeEntry( "blinkedFilter", blinkedFilter() ); + config->writeEntry( "enableReLockTimeout", enableReLockTimeout() ); + config->writeEntry( "reLockTimeoutMinutes", reLockTimeoutMinutes() ); + config->writeEntry( "useSystray", useSystray() ); + config->writeEntry( "showIconInSystray", showIconInSystray() ); + config->writeEntry( "startDocked", startDocked() ); + config->writeEntry( "middleAction", middleAction() ); + config->writeEntry( "groupOnInsertionLine", groupOnInsertionLine() ); + config->writeEntry( "spellCheckTextNotes", spellCheckTextNotes() ); + config->writeEntry( "hideOnMouseOut", hideOnMouseOut() ); + config->writeEntry( "timeToHideOnMouseOut", timeToHideOnMouseOut() ); + config->writeEntry( "showOnMouseIn", showOnMouseIn() ); + config->writeEntry( "timeToShowOnMouseIn", timeToShowOnMouseIn() ); + config->writeEntry( "basketTreeWidth", basketTreeWidth() ); + config->writeEntry( "usePassivePopup", usePassivePopup() ); + config->writeEntry( "welcomeBasketsAdded", welcomeBasketsAdded() ); + config->writePathEntry("dataFolder", dataFolder() ); + config->writeEntry( "lastBackup", QDateTime(lastBackup())); + config->writeEntry( "position", mainWindowPosition() ); + config->writeEntry( "size", mainWindowSize() ); + + config->setGroup("Notification Messages"); + config->writeEntry( "emptyBasketInfo", showEmptyBasketInfo() ); + + config->setGroup("Programs"); + config->writeEntry( "htmlUseProg", isHtmlUseProg() ); + config->writeEntry( "imageUseProg", isImageUseProg() ); + config->writeEntry( "animationUseProg", isAnimationUseProg() ); + config->writeEntry( "soundUseProg", isSoundUseProg() ); + config->writeEntry( "htmlProg", htmlProg() ); + config->writeEntry( "imageProg", imageProg() ); + config->writeEntry( "animationProg", animationProg() ); + config->writeEntry( "soundProg", soundProg() ); + + config->setGroup("Note Addition"); + config->writeEntry( "newNotesPlace", newNotesPlace() ); + config->writeEntry( "viewTextFileContent", viewTextFileContent() ); + config->writeEntry( "viewHtmlFileContent", viewHtmlFileContent() ); + config->writeEntry( "viewImageFileContent", viewImageFileContent() ); + config->writeEntry( "viewSoundFileContent", viewSoundFileContent() ); + + config->setGroup("Insert Note Default Values"); + config->writeEntry( "defImageX", defImageX() ); + config->writeEntry( "defImageY", defImageY() ); + config->writeEntry( "defIconSize", defIconSize() ); + + config->sync(); +} + +void Settings::loadLinkLook(LinkLook *look, const QString &name, const LinkLook &defaultLook) +{ + KConfig* config = Global::config(); + config->setGroup(name); + + QString underliningStrings[] = { "Always", "Never", "OnMouseHover", "OnMouseOutside" }; + QString defaultUnderliningString = underliningStrings[defaultLook.underlining()]; + + QString previewStrings[] = { "None", "IconSize", "TwiceIconSize", "ThreeIconSize" }; + QString defaultPreviewString = previewStrings[defaultLook.preview()]; + + bool italic = config->readBoolEntry( "italic", defaultLook.italic() ); + bool bold = config->readBoolEntry( "bold", defaultLook.bold() ); + QString underliningString = config->readEntry( "underlining", defaultUnderliningString ); + QColor color = config->readPropertyEntry( "color", defaultLook.color() ).asColor(); + QColor hoverColor = config->readPropertyEntry( "hoverColor", defaultLook.hoverColor() ).asColor(); + int iconSize = config->readNumEntry( "iconSize", defaultLook.iconSize() ); + QString previewString = config->readEntry( "preview", defaultPreviewString ); + + int underlining = 0; + if (underliningString == underliningStrings[1]) underlining = 1; + else if (underliningString == underliningStrings[2]) underlining = 2; + else if (underliningString == underliningStrings[3]) underlining = 3; + + int preview = 0; + if (previewString == previewStrings[1]) preview = 1; + else if (previewString == previewStrings[2]) preview = 2; + else if (previewString == previewStrings[3]) preview = 3; + + look->setLook(italic, bold, underlining, color, hoverColor, iconSize, preview); +} + +void Settings::saveLinkLook(LinkLook *look, const QString &name) +{ + KConfig* config = Global::config(); + config->setGroup(name); + + QString underliningStrings[] = { "Always", "Never", "OnMouseHover", "OnMouseOutside" }; + QString underliningString = underliningStrings[look->underlining()]; + + QString previewStrings[] = { "None", "IconSize", "TwiceIconSize", "ThreeIconSize" }; + QString previewString = previewStrings[look->preview()]; + + config->writeEntry( "italic", look->italic() ); + config->writeEntry( "bold", look->bold() ); + config->writeEntry( "underlining", underliningString ); + config->writeEntry( "color", look->color() ); + config->writeEntry( "hoverColor", look->hoverColor() ); + config->writeEntry( "iconSize", look->iconSize() ); + config->writeEntry( "preview", previewString ); +} + +void Settings::setBigNotes(bool big) +{ + if (big == s_bigNotes) + return; + + s_bigNotes = big; + // Big notes for accessibility reasons OR Standard small notes: + Note::NOTE_MARGIN = (big ? 4 : 2); + Note::INSERTION_HEIGHT = (big ? 5 : 3); + Note::EXPANDER_WIDTH = 9; + Note::EXPANDER_HEIGHT = 9; + Note::GROUP_WIDTH = 2*Note::NOTE_MARGIN + Note::EXPANDER_WIDTH; + Note::HANDLE_WIDTH = Note::GROUP_WIDTH; + Note::RESIZER_WIDTH = Note::GROUP_WIDTH; + Note::TAG_ARROW_WIDTH = 5 + (big ? 4 : 0); + Note::EMBLEM_SIZE = 16; + Note::MIN_HEIGHT = 2*Note::NOTE_MARGIN + Note::EMBLEM_SIZE; + + if (Global::bnpView) + Global::bnpView->relayoutAllBaskets(); +} + +void Settings::setAutoBullet(bool yes) +{ + s_autoBullet = yes; + if (Global::bnpView && Global::bnpView->currentBasket()) { + Global::bnpView->currentBasket()->editorPropertiesChanged(); + } +} + +/** GeneralPage */ + +GeneralPage::GeneralPage(QWidget * parent, const char * name) + : KCModule(parent, name) +{ + QVBoxLayout *layout = new QVBoxLayout(this, /*margin=*/0, KDialogBase::spacingHint()); + QHBoxLayout *hLay; + QLabel *label; + HelpLabel *hLabel; + + QGridLayout *gl = new QGridLayout(layout, /*nRows=*/3, /*nCols=*/3); + gl->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding), 0, 2); + + // Basket Tree Position: + m_treeOnLeft = new QComboBox(this); + m_treeOnLeft->insertItem(i18n("On left")); + m_treeOnLeft->insertItem(i18n("On right")); + label = new QLabel(m_treeOnLeft, i18n("&Basket tree position:"), this); + gl->addWidget(label, 0, 0); + gl->addWidget(m_treeOnLeft, 0, 1); + connect( m_treeOnLeft, SIGNAL(activated(int)), this, SLOT(changed()) ); + + // Filter Bar Position: + m_filterOnTop = new QComboBox(this); + m_filterOnTop->insertItem(i18n("On top")); + m_filterOnTop->insertItem(i18n("On bottom")); + label = new QLabel(m_filterOnTop, i18n("&Filter bar position:"), this); + gl->addWidget(label, 1, 0); + gl->addWidget(m_filterOnTop, 1, 1); + connect( m_filterOnTop, SIGNAL(activated(int)), this, SLOT(changed()) ); + + // Use Baloons to Report Results of Global Actions: + hLay = new QHBoxLayout(0L, /*margin=*/0, KDialogBase::spacingHint()); + m_usePassivePopup = new QCheckBox(i18n("&Use balloons to report results of global actions"), this); + connect( m_usePassivePopup, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + hLabel = new HelpLabel( + i18n("What are global actions?"), + ("<p>" + i18n("You can configure global shortcuts to do some actions without having to show the main window. For instance, you can paste the clipboard content, take a color from " + "a point of the screen, etc. You can also use the mouse scroll wheel over the system tray icon to change the current basket. Or use the middle mouse button " + "on that icon to paste the current selection.") + "</p>" + + "<p>" + i18n("When doing so, %1 pops up a little balloon message to inform you the action has been successfully done. You can disable that balloon.") + "</p>" + + "<p>" + i18n("Note that those messages are smart enough to not appear if the main window is visible. This is because you already see the result of your actions in the main window.") + "</p>") + .arg(kapp->aboutData()->programName()), + this); + hLay->addWidget(m_usePassivePopup); + hLay->addWidget(hLabel); + hLay->addStretch(); + layout->addLayout(hLay); + + // System Tray Icon: + QGroupBox *gbSys = new QGroupBox(3, Qt::Vertical, i18n("System Tray Icon"), this); + layout->addWidget(gbSys); + QVBoxLayout *sysLay = new QVBoxLayout(gbSys, /*margin=*/0, KDialogBase::spacingHint()); + + // Dock in System Tray: + m_useSystray = new QCheckBox(i18n("&Dock in system tray"), gbSys); + sysLay->addWidget(m_useSystray); + connect( m_useSystray, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + + m_systray = new QWidget(gbSys); + QVBoxLayout *subSysLay = new QVBoxLayout(m_systray, /*margin=*/0, KDialogBase::spacingHint()); + sysLay->addWidget(m_systray); + + // Show Current Basket Icon in System Tray Icon: + m_showIconInSystray = new QCheckBox(i18n("&Show current basket icon in system tray icon"), m_systray); + subSysLay->addWidget(m_showIconInSystray); + connect(m_showIconInSystray, SIGNAL(stateChanged(int)), this, SLOT(changed())); + + QGridLayout *gs = new QGridLayout(0, /*nRows=*/2, /*nCols=*/3); + subSysLay->addLayout(gs); + gs->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding), 0, 2); + + // Hide Main Window when Mouse Goes out of it for Some Time: + m_timeToHideOnMouseOut = new KIntNumInput(0, m_systray); + m_hideOnMouseOut = new QCheckBox(i18n("&Hide main window when mouse leaves it for"), m_systray); + m_timeToHideOnMouseOut->setRange(0, 600, 1, false); + m_timeToHideOnMouseOut->setSuffix(i18n(" tenths of seconds")); + gs->addWidget(m_hideOnMouseOut, 0, 0); + gs->addWidget(m_timeToHideOnMouseOut, 0, 1); + connect(m_hideOnMouseOut, SIGNAL(stateChanged(int)), this, SLOT(changed())); + connect(m_timeToHideOnMouseOut, SIGNAL(valueChanged (int)), this, SLOT(changed())); +// subSysLay->addWidget( + + // Show Main Window when Mouse Hovers over the System Tray Icon for Some Time: + m_timeToShowOnMouseIn = new KIntNumInput(0, m_systray); + m_showOnMouseIn = new QCheckBox(i18n("Show &main window when mouse hovers over the system tray icon for"), m_systray); + m_timeToShowOnMouseIn->setRange(0, 600, 1, false); + m_timeToShowOnMouseIn->setSuffix(i18n(" tenths of seconds")); + gs->addWidget(m_showOnMouseIn, 1, 0); + gs->addWidget(m_timeToShowOnMouseIn, 1, 1); + connect(m_showOnMouseIn, SIGNAL(stateChanged(int)), this, SLOT(changed())); + connect(m_timeToShowOnMouseIn, SIGNAL(valueChanged (int)), this, SLOT(changed())); + + connect( m_hideOnMouseOut, SIGNAL(toggled(bool)), m_timeToHideOnMouseOut, SLOT(setEnabled(bool)) ); + connect( m_showOnMouseIn, SIGNAL(toggled(bool)), m_timeToShowOnMouseIn, SLOT(setEnabled(bool)) ); + + connect( m_useSystray, SIGNAL(toggled(bool)), m_systray, SLOT(setEnabled(bool)) ); + + layout->insertStretch(-1); + load(); +} + +void GeneralPage::load() +{ + m_treeOnLeft->setCurrentItem( (int)!Settings::treeOnLeft() ); + m_filterOnTop->setCurrentItem( (int)!Settings::filterOnTop() ); + + m_usePassivePopup->setChecked(Settings::usePassivePopup()); + + m_useSystray->setChecked( Settings::useSystray() ); + m_systray->setEnabled( Settings::useSystray() ); + + m_showIconInSystray->setChecked( Settings::showIconInSystray() ); + + m_hideOnMouseOut->setChecked( Settings::hideOnMouseOut() ); + m_timeToHideOnMouseOut->setValue( Settings::timeToHideOnMouseOut() ); + m_timeToHideOnMouseOut->setEnabled( Settings::hideOnMouseOut() ); + + m_showOnMouseIn->setChecked( Settings::showOnMouseIn() ); + m_timeToShowOnMouseIn->setValue( Settings::timeToShowOnMouseIn() ); + m_timeToShowOnMouseIn->setEnabled( Settings::showOnMouseIn() ); + + +} + +void GeneralPage::save() +{ + Settings::setTreeOnLeft( ! m_treeOnLeft->currentItem() ); + Settings::setFilterOnTop( ! m_filterOnTop->currentItem() ); + + Settings::setUsePassivePopup( m_usePassivePopup->isChecked() ); + + Settings::setUseSystray( m_useSystray->isChecked() ); + Settings::setShowIconInSystray( m_showIconInSystray->isChecked() ); + Settings::setShowOnMouseIn( m_showOnMouseIn->isChecked() ); + Settings::setTimeToShowOnMouseIn( m_timeToShowOnMouseIn->value() ); + Settings::setHideOnMouseOut( m_hideOnMouseOut->isChecked() ); + Settings::setTimeToHideOnMouseOut( m_timeToHideOnMouseOut->value() ); +} + +void GeneralPage::defaults() +{ + // TODO +} + +/** BasketsPage */ + +BasketsPage::BasketsPage(QWidget * parent, const char * name) + : KCModule(parent, name) +{ + QVBoxLayout *layout = new QVBoxLayout(this, /*margin=*/0, KDialogBase::spacingHint()); + QHBoxLayout *hLay; + HelpLabel *hLabel; + + // Appearance: + + QGroupBox *appearanceBox = new QGroupBox(3, Qt::Vertical, i18n("Appearance"), this); + layout->addWidget(appearanceBox); + + m_playAnimations = new QCheckBox(i18n("Ani&mate changes in baskets"), appearanceBox); + connect( m_playAnimations, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + + m_showNotesToolTip = new QCheckBox(i18n("&Show tooltips in baskets"), appearanceBox); + connect( m_showNotesToolTip, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + + m_bigNotes = new QCheckBox(i18n("&Big notes"), appearanceBox); + connect( m_bigNotes, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + + // Behavior: + + QGroupBox *behaviorBox = new QGroupBox(5, Qt::Vertical, i18n("Behavior"), this); + layout->addWidget(behaviorBox); + + m_autoBullet = new QCheckBox(i18n("&Transform lines starting with * or - to lists in text editors"), behaviorBox); + connect( m_autoBullet, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + + m_confirmNoteDeletion = new QCheckBox(i18n("Ask confirmation before &deleting notes"), behaviorBox); + connect( m_confirmNoteDeletion, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + + QWidget *widget = new QWidget(behaviorBox); + hLay = new QHBoxLayout(widget, /*margin=*/0, KDialogBase::spacingHint()); + m_exportTextTags = new QCheckBox(i18n("&Export tags in texts"), widget); + connect( m_exportTextTags, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + + QPixmap pixmapHelp(KGlobal::dirs()->findResource("data", "basket/images/tag_export_help.png")); + QMimeSourceFactory::defaultFactory()->setPixmap("__resource_help_tag_export.png", pixmapHelp); + hLabel = new HelpLabel( + i18n("When does this apply?"), + "<p>" + i18n("It does apply when you copy and paste, or drag and drop notes to a text editor.") + "</p>" + + "<p>" + i18n("If enabled, this property lets you paste the tags as textual equivalents.") + "<br>" + + i18n("For instance, a list of notes with the <b>To Do</b> and <b>Done</b> tags are exported as lines preceded by <b>[ ]</b> or <b>[x]</b>, " + "representing an empty checkbox and a checked box.") + "</p>" + + "<p align='center'><img src=\"__resource_help_tag_export.png\"></p>", + widget); + hLay->addWidget(m_exportTextTags); + hLay->addWidget(hLabel); + hLay->addStretch(); + + m_groupOnInsertionLineWidget = new QWidget(behaviorBox); + QHBoxLayout *hLayV = new QHBoxLayout(m_groupOnInsertionLineWidget, /*margin=*/0, KDialogBase::spacingHint()); + m_groupOnInsertionLine = new QCheckBox(i18n("&Group a new note when clicking on the right of the insertion line"), m_groupOnInsertionLineWidget); + QPixmap pixmap(KGlobal::dirs()->findResource("data", "basket/images/insertion_help.png")); + QMimeSourceFactory::defaultFactory()->setPixmap("__resource_help_insertion_line.png", pixmap); + HelpLabel *helpV = new HelpLabel( + i18n("How to group a new note?"), + i18n("<p>When this option is enabled, the insertion-line not only allows you to insert notes at the cursor position, but also allows you to group a new note with the one under the cursor:</p>") + + "<p align='center'><img src=\"__resource_help_insertion_line.png\"></p>" + + i18n("<p>Place your mouse between notes, where you want to add a new one.<br>" + "Click on the <b>left</b> of the insertion-line middle-mark to <b>insert</b> a note.<br>" + "Click on the <b>right</b> to <b>group</b> a note, with the one <b>below or above</b>, depending on where your mouse is.</p>"), + m_groupOnInsertionLineWidget); + hLayV->addWidget(m_groupOnInsertionLine); + hLayV->addWidget(helpV); + hLayV->insertStretch(-1); + layout->addWidget(m_groupOnInsertionLineWidget); + connect(m_groupOnInsertionLine, SIGNAL(stateChanged(int)), this, SLOT(changed())); + + widget = new QWidget(behaviorBox); + QGridLayout *ga = new QGridLayout(widget, /*nRows=*/3, /*nCols=*/4, /*margin=*/0, KDialogBase::spacingHint()); + ga->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding), 0, 3); + + m_middleAction = new QComboBox(widget); + m_middleAction->insertItem( i18n("Do nothing") ); + m_middleAction->insertItem( i18n("Paste clipboard") ); + m_middleAction->insertItem( i18n("Insert image note") ); + m_middleAction->insertItem( i18n("Insert link note") ); + m_middleAction->insertItem( i18n("Insert launcher note") ); + m_middleAction->insertItem( i18n("Insert color note") ); + m_middleAction->insertItem( i18n("Grab screen zone") ); + m_middleAction->insertItem( i18n("Insert color from screen") ); + m_middleAction->insertItem( i18n("Load note from file") ); + m_middleAction->insertItem( i18n("Import Launcher from KDE Menu") ); + m_middleAction->insertItem( i18n("Import icon") ); + QLabel *labelM = new QLabel(m_middleAction, i18n("&Shift+middle-click anywhere:"), widget); + ga->addWidget(labelM, 0, 0); + ga->addWidget(m_middleAction, 0, 1); + ga->addWidget(new QLabel(i18n("at cursor position"), widget), 0, 2); + connect( m_middleAction, SIGNAL(activated(int)), this, SLOT(changed()) ); + + // Protection: + + QGroupBox *protectionBox = new QGroupBox(3, Qt::Vertical, i18n("Password Protection"), this); + layout->addWidget(protectionBox); + widget = new QWidget(protectionBox); + + // Re-Lock timeout configuration + hLay = new QHBoxLayout(widget, /*margin=*/0, KDialogBase::spacingHint()); + m_enableReLockTimeoutMinutes = new QCheckBox(i18n("A&utomatically lock protected baskets when closed for"), widget); + hLay->addWidget(m_enableReLockTimeoutMinutes); + m_reLockTimeoutMinutes = new KIntNumInput(widget); + m_reLockTimeoutMinutes->setMinValue(0); + m_reLockTimeoutMinutes->setSuffix(i18n(" minutes")); + hLay->addWidget(m_reLockTimeoutMinutes); + //label = new QLabel(i18n("minutes"), this); + //hLay->addWidget(label); + hLay->addStretch(); +// layout->addLayout(hLay); + connect( m_enableReLockTimeoutMinutes, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + connect( m_reLockTimeoutMinutes, SIGNAL(valueChanged(int)), this, SLOT(changed()) ); + connect( m_enableReLockTimeoutMinutes, SIGNAL(toggled(bool)), m_reLockTimeoutMinutes, SLOT(setEnabled(bool)) ); + +#ifdef HAVE_LIBGPGME + m_useGnuPGAgent = new QCheckBox(i18n("Use GnuPG agent for &private/public key protected baskets"), protectionBox); +// hLay->addWidget(m_useGnuPGAgent); + connect( m_useGnuPGAgent, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); +#endif + + layout->insertStretch(-1); + load(); +} + +void BasketsPage::load() +{ + m_playAnimations->setChecked(Settings::playAnimations()); + m_showNotesToolTip->setChecked(Settings::showNotesToolTip()); + m_bigNotes->setChecked(Settings::bigNotes()); + + m_autoBullet->setChecked(Settings::autoBullet()); + m_confirmNoteDeletion->setChecked(Settings::confirmNoteDeletion()); + m_exportTextTags->setChecked(Settings::exportTextTags()); + + m_groupOnInsertionLine->setChecked(Settings::groupOnInsertionLine()); + m_middleAction->setCurrentItem(Settings::middleAction()); + + // The correctness of this code depends on the default of enableReLockTimeout + // being true - otherwise, the reLockTimeoutMinutes widget is not disabled properly. + m_enableReLockTimeoutMinutes->setChecked(Settings::enableReLockTimeout()); + m_reLockTimeoutMinutes->setValue(Settings::reLockTimeoutMinutes()); +#ifdef HAVE_LIBGPGME + m_useGnuPGAgent->setChecked(Settings::useGnuPGAgent()); + + if (KGpgMe::isGnuPGAgentAvailable()) { + m_useGnuPGAgent->setChecked(Settings::useGnuPGAgent()); + } else { + m_useGnuPGAgent->setChecked(false); + m_useGnuPGAgent->setEnabled(false); + } +#endif +} + +void BasketsPage::save() +{ + Settings::setPlayAnimations( m_playAnimations->isChecked() ); + Settings::setShowNotesToolTip( m_showNotesToolTip->isChecked() ); + Settings::setBigNotes( m_bigNotes->isChecked() ); + + Settings::setAutoBullet( m_autoBullet->isChecked() ); + Settings::setConfirmNoteDeletion( m_confirmNoteDeletion->isChecked() ); + Settings::setExportTextTags( m_exportTextTags->isChecked() ); + + Settings::setGroupOnInsertionLine( m_groupOnInsertionLine->isChecked() ); + Settings::setMiddleAction( m_middleAction->currentItem() ); + + Settings::setEnableReLockTimeout( m_enableReLockTimeoutMinutes->isChecked()); + Settings::setReLockTimeoutMinutes(m_reLockTimeoutMinutes->value()); +#ifdef HAVE_LIBGPGME + Settings::setUseGnuPGAgent( m_useGnuPGAgent->isChecked() ); +#endif +} + +void BasketsPage::defaults() +{ + // TODO +} + +/** class NewNotesPage: */ + +NewNotesPage::NewNotesPage(QWidget * parent, const char * name) + : KCModule(parent, name) +{ + QVBoxLayout *layout = new QVBoxLayout(this, /*margin=*/0, KDialogBase::spacingHint()); + QHBoxLayout *hLay; + QLabel *label; + + // Place of New Notes: + + hLay = new QHBoxLayout(0L, /*margin=*/0, KDialogBase::spacingHint()); + m_newNotesPlace = new QComboBox(this); + label = new QLabel(m_newNotesPlace, i18n("&Place of new notes:"), this); + m_newNotesPlace->insertItem(i18n("On top")); + m_newNotesPlace->insertItem(i18n("On bottom")); + m_newNotesPlace->insertItem(i18n("At current note")); + hLay->addWidget(label); + hLay->addWidget(m_newNotesPlace); + hLay->addStretch(); + //layout->addLayout(hLay); + label->hide(); + m_newNotesPlace->hide(); + connect( m_newNotesPlace, SIGNAL(textChanged(const QString &)), this, SLOT(changed()) ); + + // New Images Size: + + hLay = new QHBoxLayout(0L, /*margin=*/0, KDialogBase::spacingHint()); + m_imgSizeX = new KIntNumInput(this); + m_imgSizeX->setMinValue(1); + m_imgSizeX->setMaxValue(4096); + m_imgSizeX->setReferencePoint(100); + connect( m_imgSizeX, SIGNAL(valueChanged(int)), this, SLOT(changed()) ); + label = new QLabel(m_imgSizeX, i18n("&New images size:"), this); + hLay->addWidget(label); + hLay->addWidget(m_imgSizeX); + m_imgSizeY = new KIntNumInput(this); + m_imgSizeY->setMinValue(1); + m_imgSizeY->setMaxValue(4096); + m_imgSizeY->setReferencePoint(100); + connect( m_imgSizeY, SIGNAL(valueChanged(int)), this, SLOT(changed()) ); + label = new QLabel(m_imgSizeY, i18n("&by"), this); + hLay->addWidget(label); + hLay->addWidget(m_imgSizeY); + label = new QLabel(i18n("pixels"), this); + hLay->addWidget(label); + m_pushVisualize = new QPushButton(i18n("&Visualize..."), this); + hLay->addWidget(m_pushVisualize); + hLay->addStretch(); + layout->addLayout(hLay); + connect( m_pushVisualize, SIGNAL(clicked()), this, SLOT(visualize()) ); + + // View File Content: + + QVButtonGroup *buttonGroup = new QVButtonGroup(i18n("View Content of Added Files for the Following Types"), this); + m_viewTextFileContent = new QCheckBox( i18n("&Plain text"), buttonGroup ); + m_viewHtmlFileContent = new QCheckBox( i18n("&HTML page"), buttonGroup ); + m_viewImageFileContent = new QCheckBox( i18n("&Image or animation"), buttonGroup ); + m_viewSoundFileContent = new QCheckBox( i18n("&Sound"), buttonGroup ); + layout->addWidget(buttonGroup); + connect( m_viewTextFileContent, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + connect( m_viewHtmlFileContent, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + connect( m_viewImageFileContent, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + connect( m_viewSoundFileContent, SIGNAL(stateChanged(int)), this, SLOT(changed()) ); + + layout->insertStretch(-1); + load(); +} + +void NewNotesPage::load() +{ + m_newNotesPlace->setCurrentItem(Settings::newNotesPlace()); + + m_imgSizeX->setValue(Settings::defImageX()); + m_imgSizeY->setValue(Settings::defImageY()); + + m_viewTextFileContent->setChecked( Settings::viewTextFileContent() ); + m_viewHtmlFileContent->setChecked( Settings::viewHtmlFileContent() ); + m_viewImageFileContent->setChecked( Settings::viewImageFileContent() ); + m_viewSoundFileContent->setChecked( Settings::viewSoundFileContent() ); +} + +void NewNotesPage::save() +{ + Settings::setNewNotesPlace(m_newNotesPlace->currentItem()); + + Settings::setDefImageX(m_imgSizeX->value()); + Settings::setDefImageY(m_imgSizeY->value()); + + Settings::setViewTextFileContent( m_viewTextFileContent->isChecked() ); + Settings::setViewHtmlFileContent( m_viewHtmlFileContent->isChecked() ); + Settings::setViewImageFileContent( m_viewImageFileContent->isChecked() ); + Settings::setViewSoundFileContent( m_viewSoundFileContent->isChecked() ); +} + +void NewNotesPage::defaults() +{ + // TODO +} + +void NewNotesPage::visualize() +{ + ViewSizeDialog size(this, m_imgSizeX->value(), m_imgSizeY->value()); + size.exec(); + m_imgSizeX->setValue(size.width()); + m_imgSizeY->setValue(size.height()); +} + +/** class NotesAppearancePage: */ + +NotesAppearancePage::NotesAppearancePage(QWidget * parent, const char * name) + : KCModule(parent, name) +{ + QVBoxLayout *layout = new QVBoxLayout(this, /*margin=*/0, KDialogBase::spacingHint()); + QTabWidget *tabs = new QTabWidget(this); + layout->addWidget(tabs); + + m_soundLook = new LinkLookEditWidget(this, i18n("Conference audio record"), "sound", tabs); + m_fileLook = new LinkLookEditWidget(this, i18n("Annual report"), "document", tabs); + m_localLinkLook = new LinkLookEditWidget(this, i18n("Home folder"), "folder_home", tabs); + m_networkLinkLook = new LinkLookEditWidget(this, "www.kde.org", KMimeType::iconForURL("http://www.kde.org"), tabs); + m_launcherLook = new LinkLookEditWidget(this, i18n("Launch %1").arg(kapp->aboutData()->programName()), "basket", tabs); + tabs->addTab(m_soundLook, i18n("&Sounds") ); + tabs->addTab(m_fileLook, i18n("&Files") ); + tabs->addTab(m_localLinkLook, i18n("&Local Links") ); + tabs->addTab(m_networkLinkLook, i18n("&Network Links")); + tabs->addTab(m_launcherLook, i18n("Launc&hers") ); + + load(); +} + +void NotesAppearancePage::load() +{ + m_soundLook->set(LinkLook::soundLook); + m_fileLook->set(LinkLook::fileLook); + m_localLinkLook->set(LinkLook::localLinkLook); + m_networkLinkLook->set(LinkLook::networkLinkLook); + m_launcherLook->set(LinkLook::launcherLook); +} + +void NotesAppearancePage::save() +{ + m_soundLook->saveChanges(); + m_fileLook->saveChanges(); + m_localLinkLook->saveChanges(); + m_networkLinkLook->saveChanges(); + m_launcherLook->saveChanges(); + Global::bnpView->linkLookChanged(); +} + +void NotesAppearancePage::defaults() +{ + // TODO +} + +/** class ApplicationsPage: */ + +ApplicationsPage::ApplicationsPage(QWidget * parent, const char * name) + : KCModule(parent, name) +{ + /* Applications page */ + QVBoxLayout *layout = new QVBoxLayout(this, /*margin=*/0, KDialogBase::spacingHint()); + + m_htmlUseProg = new QCheckBox(i18n("Open &text notes with a custom application:"), this); + m_htmlProg = new RunCommandRequester("", i18n("Open text notes with:"), this); + QHBoxLayout *hLayH = new QHBoxLayout(0L, /*margin=*/0, KDialogBase::spacingHint()); + hLayH->insertSpacing(-1, 20); + hLayH->addWidget(m_htmlProg); + connect(m_htmlUseProg, SIGNAL(stateChanged(int)), this, SLOT(changed())); + connect(m_htmlProg->lineEdit(), SIGNAL(textChanged(const QString &)), this, SLOT(changed())); + + m_imageUseProg = new QCheckBox(i18n("Open &image notes with a custom application:"), this); + m_imageProg = new RunCommandRequester("", i18n("Open image notes with:"), this); + QHBoxLayout *hLayI = new QHBoxLayout(0L, /*margin=*/0, KDialogBase::spacingHint()); + hLayI->insertSpacing(-1, 20); + hLayI->addWidget(m_imageProg); + connect(m_imageUseProg, SIGNAL(stateChanged(int)), this, SLOT(changed())); + connect(m_imageProg->lineEdit(), SIGNAL(textChanged(const QString &)), this, SLOT(changed())); + + m_animationUseProg = new QCheckBox(i18n("Open a&nimation notes with a custom application:"), this); + m_animationProg = new RunCommandRequester("", i18n("Open animation notes with:"), this); + QHBoxLayout *hLayA = new QHBoxLayout(0L, /*margin=*/0, KDialogBase::spacingHint()); + hLayA->insertSpacing(-1, 20); + hLayA->addWidget(m_animationProg); + connect(m_animationUseProg, SIGNAL(stateChanged(int)), this, SLOT(changed())); + connect(m_animationProg->lineEdit(), SIGNAL(textChanged(const QString &)), this, SLOT(changed())); + + m_soundUseProg = new QCheckBox(i18n("Open so&und notes with a custom application:"), this); + m_soundProg = new RunCommandRequester("", i18n("Open sound notes with:"), this); + QHBoxLayout *hLayS = new QHBoxLayout(0L, /*margin=*/0, KDialogBase::spacingHint()); + hLayS->insertSpacing(-1, 20); + hLayS->addWidget(m_soundProg); + connect(m_soundUseProg, SIGNAL(stateChanged(int)), this, SLOT(changed())); + connect(m_soundProg->lineEdit(), SIGNAL(textChanged(const QString &)), this, SLOT(changed())); + + QString whatsthis = i18n( + "<p>If checked, the application defined below will be used when opening that type of note.</p>" + "<p>Otherwise, the application you've configured in Konqueror will be used.</p>"); + + QWhatsThis::add(m_htmlUseProg, whatsthis); + QWhatsThis::add(m_imageUseProg, whatsthis); + QWhatsThis::add(m_animationUseProg, whatsthis); + QWhatsThis::add(m_soundUseProg, whatsthis); + + whatsthis = i18n( + "<p>Define the application to use for opening that type of note instead of the " + "application configured in Konqueror.</p>"); + + QWhatsThis::add(m_htmlProg, whatsthis); + QWhatsThis::add(m_imageProg, whatsthis); + QWhatsThis::add(m_animationProg, whatsthis); + QWhatsThis::add(m_soundProg, whatsthis); + + layout->addWidget(m_htmlUseProg); + layout->addItem(hLayH); + layout->addWidget(m_imageUseProg); + layout->addItem(hLayI); + layout->addWidget(m_animationUseProg); + layout->addItem(hLayA); + layout->addWidget(m_soundUseProg); + layout->addItem(hLayS); + + layout->addSpacing(KDialogBase::spacingHint()); + + QHBoxLayout *hLay = new QHBoxLayout(0L, /*margin=*/0, /*spacing=*/0); + HelpLabel *hl1 = new HelpLabel( + i18n("How to change the application used to open Web links?"), + i18n("<p>When opening Web links, they are opened in different applications, depending on the content of the link " + "(a Web page, an image, a PDF document...), such as if they were files on your computer.</p>" + "<p>Here is how to do if you want every Web addresses to be opened in your Web browser. " + "It is useful if you are not using KDE (if you are using eg. GNOME, XFCE...).</p>" + "<ul>" + "<li>Open the KDE Control Center (if it is not available, try to type \"kcontrol\" in a command line terminal);</li>" + "<li>Go to the \"KDE Components\" and then \"Components Selector\" section;</li>" + "<li>Choose \"Web Browser\", check \"In the following browser:\" and enter the name of your Web browser (like \"firefox\" or \"epiphany\").</li>" + "</ul>" + "<p>Now, when you click <i>any</i> link that start with \"http://...\", it will be opened in your Web browser (eg. Mozilla Firefox or Epiphany or...).</p>" + "<p>For more fine-grained configuration (like opening only Web pages in your Web browser), read the second help link.</p>"), + this); + hLay->addWidget(hl1); + hLay->addStretch(); + layout->addLayout(hLay); + + hLay = new QHBoxLayout(0L, /*margin=*/0, /*spacing=*/0); + HelpLabel *hl2 = new HelpLabel( + i18n("How to change the applications used to open files and links?"), + i18n("<p>Here is how to set the application to be used for each type of file. " + "This also applies to Web links if you choose not to open them systematically in a Web browser (see the first help link). " + "The default settings should be good enough for you, but this tip is useful if you are using GNOME, XFCE, or another environment than KDE.</p>" + "<p>This is an example of how to open HTML pages in your Web browser (and keep using the other applications for other addresses or files). " + "Repeat these steps for each type of file you want to open in a specific application.</p>" + "<ul>" + "<li>Open the KDE Control Center (if it is not available, try to type \"kcontrol\" in a command line terminal);</li>" + "<li>Go to the \"KDE Components\" and then \"File Associations\" section;</li>" + "<li>In the tree, expand \"text\" and click \"html\";</li>" + "<li>In the applications list, add your Web browser as the first entry;</li>" + "<li>Do the same for the type \"application -> xhtml+xml\".</li>" + "</ul>"), + this); + hLay->addWidget(hl2); + hLay->addStretch(); + layout->addLayout(hLay); + + connect( m_htmlUseProg, SIGNAL(toggled(bool)), m_htmlProg, SLOT(setEnabled(bool)) ); + connect( m_imageUseProg, SIGNAL(toggled(bool)), m_imageProg, SLOT(setEnabled(bool)) ); + connect( m_animationUseProg, SIGNAL(toggled(bool)), m_animationProg, SLOT(setEnabled(bool)) ); + connect( m_soundUseProg, SIGNAL(toggled(bool)), m_soundProg, SLOT(setEnabled(bool)) ); + + layout->insertStretch(-1); + load(); +} + +void ApplicationsPage::load() +{ + m_htmlProg->setRunCommand(Settings::htmlProg()); + m_htmlUseProg->setChecked(Settings::isHtmlUseProg()); + m_htmlProg->setEnabled(Settings::isHtmlUseProg()); + + m_imageProg->setRunCommand(Settings::imageProg()); + m_imageUseProg->setChecked(Settings::isImageUseProg()); + m_imageProg->setEnabled(Settings::isImageUseProg()); + + m_animationProg->setRunCommand(Settings::animationProg()); + m_animationUseProg->setChecked(Settings::isAnimationUseProg()); + m_animationProg->setEnabled(Settings::isAnimationUseProg()); + + m_soundProg->setRunCommand(Settings::soundProg()); + m_soundUseProg->setChecked(Settings::isSoundUseProg()); + m_soundProg->setEnabled(Settings::isSoundUseProg()); +} + +void ApplicationsPage::save() +{ + Settings::setIsHtmlUseProg( m_htmlUseProg->isChecked() ); + Settings::setHtmlProg( m_htmlProg->runCommand() ); + + Settings::setIsImageUseProg( m_imageUseProg->isChecked() ); + Settings::setImageProg( m_imageProg->runCommand() ); + + Settings::setIsAnimationUseProg( m_animationUseProg->isChecked() ); + Settings::setAnimationProg( m_animationProg->runCommand() ); + + Settings::setIsSoundUseProg( m_soundUseProg->isChecked() ); + Settings::setSoundProg( m_soundProg->runCommand() ); +} + +void ApplicationsPage::defaults() +{ + // TODO +} + +#include "settings.moc" diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..e731262 --- /dev/null +++ b/src/settings.h @@ -0,0 +1,392 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef SETTINGS_H +#define SETTINGS_H + +#include <qstring.h> +#include <kdialogbase.h> +#include <qcheckbox.h> +#include <qcombobox.h> +#include <qtooltip.h> +#include <qcolor.h> +#include <qpoint.h> +#include <qsize.h> +#include <kcmodule.h> +#include <kaction.h> +#include <kmainwindow.h> +#include <qdatetime.h> + +#include "global.h" +#include "bnpview.h" +#include "systemtray.h" + +class QGroupBox; +class QButtonGroup; +class KColorCombo; +class KIntNumInput; +class KGlobalAccel; +class QLabel; + +class Container; +class SystemTray; +class DebugWindow; +class LinkLook; +class LinkLookEditWidget; +class RunCommandRequester; +class IconSizeCombo; + +class GeneralPage : public KCModule +{ + Q_OBJECT + public: + GeneralPage(QWidget * parent=0, const char * name=0); + GeneralPage() {}; + virtual ~GeneralPage() {} + + virtual void load(); + virtual void save(); + virtual void defaults(); + + private: + // General + QComboBox *m_treeOnLeft; + QComboBox *m_filterOnTop; + QCheckBox *m_usePassivePopup; + + // System Tray Icon + QCheckBox *m_useSystray; + QWidget *m_systray; + QCheckBox *m_showIconInSystray; + QCheckBox *m_hideOnMouseOut; + KIntNumInput *m_timeToHideOnMouseOut; + QCheckBox *m_showOnMouseIn; + KIntNumInput *m_timeToShowOnMouseIn; +}; + +class BasketsPage : public KCModule +{ + Q_OBJECT + public: + BasketsPage(QWidget * parent=0, const char * name=0); + BasketsPage() {}; + + virtual void load(); + virtual void save(); + virtual void defaults(); + + private: + // Appearance + QCheckBox *m_playAnimations; + QCheckBox *m_showNotesToolTip; + QCheckBox *m_bigNotes; + + // Behavior + QCheckBox *m_autoBullet; + QCheckBox *m_confirmNoteDeletion; + QCheckBox *m_exportTextTags; + QWidget *m_groupOnInsertionLineWidget; + QCheckBox *m_groupOnInsertionLine; + QComboBox *m_middleAction; + + // Protection + QCheckBox *m_useGnuPGAgent; + QCheckBox *m_enableReLockTimeoutMinutes; + KIntNumInput *m_reLockTimeoutMinutes; +}; + +class NewNotesPage : public KCModule +{ + Q_OBJECT + public: + NewNotesPage(QWidget * parent=0, const char * name=0); + NewNotesPage() {}; + + virtual void load(); + virtual void save(); + virtual void defaults(); + + private slots: + void visualize(); + + private: + // Notes Image Size + KIntNumInput *m_imgSizeX; + KIntNumInput *m_imgSizeY; + QPushButton *m_pushVisualize; + + // Note Addition + QComboBox *m_newNotesPlace; + QCheckBox *m_viewTextFileContent; + QCheckBox *m_viewHtmlFileContent; + QCheckBox *m_viewImageFileContent; + QCheckBox *m_viewSoundFileContent; +}; + +class NotesAppearancePage : public KCModule +{ + Q_OBJECT + public: + NotesAppearancePage(QWidget * parent=0, const char * name=0); + NotesAppearancePage() {}; + + virtual void load(); + virtual void save(); + virtual void defaults(); + + private: + // Link Looks + LinkLookEditWidget *m_soundLook; + LinkLookEditWidget *m_fileLook; + LinkLookEditWidget *m_localLinkLook; + LinkLookEditWidget *m_networkLinkLook; + LinkLookEditWidget *m_launcherLook; +}; + +class ApplicationsPage : public KCModule +{ + Q_OBJECT + public: + ApplicationsPage(QWidget * parent=0, const char * name=0); + ApplicationsPage() {}; + + virtual void load(); + virtual void save(); + virtual void defaults(); + + private: + // Applications + QCheckBox *m_htmlUseProg; + QCheckBox *m_imageUseProg; + QCheckBox *m_animationUseProg; + QCheckBox *m_soundUseProg; + RunCommandRequester *m_htmlProg; + RunCommandRequester *m_imageProg; + RunCommandRequester *m_animationProg; + RunCommandRequester *m_soundProg; +}; + +/** Handle all global variables (to avoid lot of extern declarations) + * @author S�astien Laot + */ +class Settings // FIXME: Distaptch new config events ? +{ + protected: + /** Main window */ + static bool s_treeOnLeft; + static bool s_filterOnTop; + static bool s_playAnimations; + static bool s_showNotesToolTip; + static bool s_confirmNoteDeletion; + static bool s_bigNotes; + static bool s_autoBullet; + static bool s_exportTextTags; + static bool s_useGnuPGAgent; + static bool s_usePassivePopup; + static int s_middleAction; // O:Nothing ; 1:Paste ; 2:Text ; 3:Html ; 4:Image ; 5:Link ; 6:Launcher ; 7:Color + static bool s_groupOnInsertionLine; + static bool s_spellCheckTextNotes; + static int s_basketTreeWidth; + static bool s_welcomeBasketsAdded; + static QString s_dataFolder; + static QDate s_lastBackup; + static QPoint s_mainWindowPosition; + static QSize s_mainWindowSize; + static bool s_showEmptyBasketInfo; + static bool s_blinkedFilter; + static bool s_enableReLockTimeout; + static int s_reLockTimeoutMinutes; + /** Note Addition */ + static int s_newNotesPlace; // 0:OnTop ; 1:OnBottom ; 2:AtCurrentNote + static int s_viewTextFileContent; + static int s_viewHtmlFileContent; + static int s_viewImageFileContent; + static int s_viewSoundFileContent; + /** System tray Icon */ + static bool s_useSystray; + static bool s_showIconInSystray; + static bool s_startDocked; + static bool s_hideOnMouseOut; + static int s_timeToHideOnMouseOut; + static bool s_showOnMouseIn; + static int s_timeToShowOnMouseIn; + /** Programs */ + static bool s_htmlUseProg; + static bool s_imageUseProg; + static bool s_animationUseProg; + static bool s_soundUseProg; + static QString s_htmlProg; + static QString s_imageProg; + static QString s_animationProg; + static QString s_soundProg; + /** Insert Note Default Values */ + static int s_defImageX; + static int s_defImageY; + static int s_defIconSize; + public: /* And the following methods are just getter / setters */ + /** App settings GET */ + static inline bool treeOnLeft() { return s_treeOnLeft; } + static inline bool filterOnTop() { return s_filterOnTop; } + static inline bool playAnimations() { return s_playAnimations; } + static inline bool showNotesToolTip() { return s_showNotesToolTip; } + static inline bool confirmNoteDeletion() { return s_confirmNoteDeletion; } + static inline bool bigNotes() { return s_bigNotes; } + static inline bool autoBullet() { return s_autoBullet; } + static inline bool exportTextTags() { return s_exportTextTags; } + static inline bool useGnuPGAgent() { return s_useGnuPGAgent; } + static inline bool blinkedFilter() { return s_blinkedFilter; } + static inline bool enableReLockTimeout() { return s_enableReLockTimeout; } + static inline int reLockTimeoutMinutes() { return s_reLockTimeoutMinutes; } + static inline bool useSystray() { return s_useSystray; } + static inline bool showIconInSystray() { return s_showIconInSystray; } + static inline bool startDocked() { return s_startDocked; } + static inline int middleAction() { return s_middleAction; } + static inline bool groupOnInsertionLine() { return s_groupOnInsertionLine; } + static inline bool spellCheckTextNotes() { return s_spellCheckTextNotes; } + static inline bool hideOnMouseOut() { return s_hideOnMouseOut; } + static inline int timeToHideOnMouseOut() { return s_timeToHideOnMouseOut; } + static inline bool showOnMouseIn() { return s_showOnMouseIn; } + static inline int timeToShowOnMouseIn() { return s_timeToShowOnMouseIn; } + static inline int basketTreeWidth() { return s_basketTreeWidth; } + static inline int dropTimeToShow() { return 7; } // TODO: 700 ; TODO: There is certainly a KGlobalConfig ??? + static inline bool usePassivePopup() { return s_usePassivePopup; } + static inline bool welcomeBasketsAdded() { return s_welcomeBasketsAdded; } + static inline QString dataFolder() { return s_dataFolder; } + static inline QDate lastBackup() { return s_lastBackup; } + static inline QPoint mainWindowPosition() { return s_mainWindowPosition; } + static inline QSize mainWindowSize() { return s_mainWindowSize; } + static inline bool showEmptyBasketInfo() { return s_showEmptyBasketInfo; } + /** Programs */ + static inline bool isHtmlUseProg() { return s_htmlUseProg; } + static inline bool isImageUseProg() { return s_imageUseProg; } + static inline bool isAnimationUseProg() { return s_animationUseProg; } + static inline bool isSoundUseProg() { return s_soundUseProg; } + static inline QString htmlProg() { return s_htmlProg; } + static inline QString imageProg() { return s_imageProg; } + static inline QString animationProg() { return s_animationProg; } + static inline QString soundProg() { return s_soundProg; } + /** Insert Note Default Values */ + static inline int defImageX() { return s_defImageX; } + static inline int defImageY() { return s_defImageY; } + static inline int defIconSize() { return s_defIconSize; } + /** Note Addition */ + static inline int newNotesPlace() { return s_newNotesPlace; } + static inline int viewTextFileContent() { return s_viewTextFileContent; } + static inline int viewHtmlFileContent() { return s_viewHtmlFileContent; } + static inline int viewImageFileContent() { return s_viewImageFileContent; } + static inline int viewSoundFileContent() { return s_viewSoundFileContent; } + + /** App settings SET */ + static void setTreeOnLeft(bool onLeft) + { + s_treeOnLeft = onLeft; + if (Global::bnpView) + Global::bnpView->setTreePlacement(onLeft); + } + static void setFilterOnTop(bool onTop) + { + if (s_filterOnTop != onTop) { + s_filterOnTop = onTop; + if (Global::bnpView) + Global::bnpView->filterPlacementChanged(onTop); + } + } + static void setShowNotesToolTip(bool show) + { + s_showNotesToolTip = show; + } + static void setUseSystray(bool useSystray) + { + if (s_useSystray != useSystray) { + s_useSystray = useSystray; + if (Global::systemTray != 0L) { + if (Settings::useSystray()) + Global::systemTray->show(); + else { + Global::systemTray->hide(); + if(Global::mainWindow()) Global::mainWindow()->show(); + } + } + if (Global::bnpView) + Global::bnpView->m_actHideWindow->setEnabled(useSystray); + } + } + static void setShowIconInSystray(bool show) + { + if (s_showIconInSystray != show) { + s_showIconInSystray = show; + if (Global::systemTray != 0L && Settings::useSystray()) + Global::systemTray->updateToolTip(); + } + } + static inline void setConfirmNoteDeletion(bool confirm) { s_confirmNoteDeletion = confirm; } + static void setBigNotes(bool big); + static void setAutoBullet(bool yes); + static inline void setExportTextTags(bool yes) { s_exportTextTags = yes; } + static inline void setUseGnuPGAgent(bool yes) { s_useGnuPGAgent = yes; } + static inline void setPlayAnimations(bool play) { s_playAnimations = play; } + static inline void setBlinkedFilter(bool blinked) { s_blinkedFilter = blinked; } + static inline void setEnableReLockTimeout(bool yes) { s_enableReLockTimeout = yes; } + static inline void setReLockTimeoutMinutes(int minutes) { s_reLockTimeoutMinutes = minutes; } + static inline void setStartDocked(bool docked) { s_startDocked = docked; } + static inline void setMiddleAction(int action) { s_middleAction = action; } + static inline void setGroupOnInsertionLine(bool yes) { s_groupOnInsertionLine = yes; } + static inline void setSpellCheckTextNotes(bool yes) { s_spellCheckTextNotes = yes; } + static inline void setHideOnMouseOut(bool hide) { s_hideOnMouseOut = hide; } + static inline void setTimeToHideOnMouseOut(int time) { s_timeToHideOnMouseOut = time; } + static inline void setShowOnMouseIn(bool show) { s_showOnMouseIn = show; } + static inline void setTimeToShowOnMouseIn(int time) { s_timeToShowOnMouseIn = time; } + static inline void setBasketTreeWidth(int width) { s_basketTreeWidth = width; } + static inline void setUsePassivePopup(bool enable) { s_usePassivePopup = enable; } + static inline void setWelcomeBasketsAdded(bool added) { s_welcomeBasketsAdded = added; } + static inline void setDataFolder(const QString &folder) { s_dataFolder = folder; } + static inline void setLastBackup(const QDate &date) { s_lastBackup = date; } + static inline void setMainWindowPosition(const QPoint &pos) { s_mainWindowPosition = pos; } + static inline void setMainWindowSize(const QSize &size) { s_mainWindowSize = size; } + static inline void setShowEmptyBasketInfo(bool show) { s_showEmptyBasketInfo = show; } + // Programs : + static inline void setIsHtmlUseProg(bool useProg) { s_htmlUseProg = useProg; } + static inline void setIsImageUseProg(bool useProg) { s_imageUseProg = useProg; } + static inline void setIsAnimationUseProg(bool useProg) { s_animationUseProg = useProg; } + static inline void setIsSoundUseProg(bool useProg) { s_soundUseProg = useProg; } + static inline void setHtmlProg(const QString &prog) { s_htmlProg = prog; } + static inline void setImageProg(const QString &prog) { s_imageProg = prog; } + static inline void setAnimationProg(const QString &prog) { s_animationProg = prog; } + static inline void setSoundProg(const QString &prog) { s_soundProg = prog; } + // Insert Note Default Values : + static inline void setDefImageX(int val) { s_defImageX = val; } + static inline void setDefImageY(int val) { s_defImageY = val; } + static inline void setDefIconSize(int val) { s_defIconSize = val; } + // Note Addition + static inline void setNewNotesPlace(int val) { s_newNotesPlace = val; } + static inline void setViewTextFileContent(bool view) { s_viewTextFileContent = view; } + static inline void setViewHtmlFileContent(bool view) { s_viewHtmlFileContent = view; } + static inline void setViewImageFileContent(bool view) { s_viewImageFileContent = view; } + static inline void setViewSoundFileContent(bool view) { s_viewSoundFileContent = view; } + public: + /* Save and load config */ + static void loadConfig(); + static void saveConfig(); + protected: + static void loadLinkLook(LinkLook *look, const QString &name, const LinkLook &defaultLook); + static void saveLinkLook(LinkLook *look, const QString &name); +}; + +#endif // SETTINGS_H diff --git a/src/softwareimporters.cpp b/src/softwareimporters.cpp new file mode 100644 index 0000000..9f276c7 --- /dev/null +++ b/src/softwareimporters.cpp @@ -0,0 +1,696 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qstring.h> +#include <qdir.h> +#include <kstandarddirs.h> +#include <klocale.h> +#include <kfiledialog.h> +#include <qptrstack.h> +#include <qlayout.h> +#include <qvbuttongroup.h> +#include <qradiobutton.h> +#include <kmessagebox.h> +#include <qtextedit.h> + +#include "softwareimporters.h" +#include "basket.h" +#include "basketfactory.h" +#include "notefactory.h" +#include "global.h" +#include "bnpview.h" +#include "xmlwork.h" +#include "tools.h" + +/** class TreeImportDialog: */ + +TreeImportDialog::TreeImportDialog(QWidget *parent) + : KDialogBase(KDialogBase::Swallow, i18n("Import Hierarchy"), KDialogBase::Ok | KDialogBase::Cancel, + KDialogBase::Ok, parent, /*name=*/"ImportHierarchy", /*modal=*/true, /*separator=*/false) +{ + QWidget *page = new QWidget(this); + QVBoxLayout *topLayout = new QVBoxLayout(page, /*margin=*/0, spacingHint()); + + m_choices = new QVButtonGroup(i18n("How to Import the Notes?"), page); + new QRadioButton(i18n("&Keep original hierarchy (all notes in separate baskets)"), m_choices); + new QRadioButton(i18n("&First level notes in separate baskets"), m_choices); + new QRadioButton(i18n("&All notes in one basket"), m_choices); + m_choices->setButton(0); + topLayout->addWidget(m_choices); + topLayout->addStretch(10); + + setMainWidget(page); +} + +TreeImportDialog::~TreeImportDialog() +{ +} + +int TreeImportDialog::choice() +{ + return m_choices->selectedId(); +} + +/** class TextFileImportDialog: */ + +TextFileImportDialog::TextFileImportDialog(QWidget *parent) + : KDialogBase(KDialogBase::Swallow, i18n("Import Text File"), KDialogBase::Ok | KDialogBase::Cancel, + KDialogBase::Ok, parent, /*name=*/"ImportTextFile", /*modal=*/true, /*separator=*/false) +{ + QWidget *page = new QWidget(this); + QVBoxLayout *topLayout = new QVBoxLayout(page, /*margin=*/0, spacingHint()); + + m_choices = new QVButtonGroup(i18n("Format of the Text File"), page); + new QRadioButton(i18n("Notes separated by an &empty line"), m_choices); + new QRadioButton(i18n("One ¬e per line"), m_choices); + new QRadioButton(i18n("Notes begin with a &dash (-)"), m_choices); + new QRadioButton(i18n("Notes begin with a &star (*)"), m_choices); + m_anotherSeparator = new QRadioButton(i18n("&Use another separator:"), m_choices); + + QWidget *indentedTextEdit = new QWidget(m_choices); + QHBoxLayout *hLayout = new QHBoxLayout(indentedTextEdit, /*margin=*/0, spacingHint()); + hLayout->addSpacing(20); + m_customSeparator = new QTextEdit(indentedTextEdit); + m_customSeparator->setTextFormat(Qt::PlainText); + hLayout->addWidget(m_customSeparator); + m_choices->insertChild(indentedTextEdit); + + new QRadioButton(i18n("&All in one note"), m_choices); + m_choices->setButton(0); + topLayout->addWidget(m_choices); + + connect( m_customSeparator, SIGNAL(textChanged()), this, SLOT(customSeparatorChanged()) ); + + setMainWidget(page); +} + +TextFileImportDialog::~TextFileImportDialog() +{ +} + +QString TextFileImportDialog::separator() +{ + switch (m_choices->selectedId()) { + default: + case 0: return "\n\n"; + case 1: return "\n"; + case 2: return "\n-"; + case 3: return "\n*"; + case 4: return m_customSeparator->text(); + case 5: return ""; + } +} + +void TextFileImportDialog::customSeparatorChanged() +{ + if (!m_anotherSeparator->isOn()) + m_anotherSeparator->toggle(); +} + +/** namespace SoftwareImporters: */ + +QString SoftwareImporters::fromICS(const QString &ics) +{ + QString result = ics; + + // Remove escaped '\' characters and append the text to the body + int pos = 0; + while ( (pos = result.find('\\', pos)) != -1 ) { + if ((uint)pos == result.length() - 1) // End of string + break; + if (result[pos+1] == 'n') { + result.replace(pos, 2, '\n'); + } else if (result[pos+1] == 'r') { + result.replace(pos, 2, '\r'); + } else if (result[pos+1] == 't') { + result.replace(pos, 2, '\t'); + } else if (result[pos] == '\\') { + result.remove(pos, 1); // Take care of "\\", "\,", "\;" and other escaped characters I haven't noticed + ++pos; + } + } + + return result; +} + +QString SoftwareImporters::fromTomboy(QString tomboy) +{ + // The first line is the note title, and we already have it, so we remove it (yes, that's pretty stupid to duplicate it in the content...): + tomboy = tomboy.mid(tomboy.find("\n")).stripWhiteSpace(); + + // Font styles and decorations: + tomboy.replace("<bold>", "<b>"); + tomboy.replace("</bold>", "</b>"); + tomboy.replace("<italic>", "<i>"); + tomboy.replace("</italic>", "</i>"); + tomboy.replace("<strikethrough>", "<span style='text-decoration: line-through'>"); + tomboy.replace("</strikethrough>", "</span>"); + + // Highlight not supported by QTextEdit: + tomboy.replace("<highlight>", "<span style='color:#ff0080'>"); + tomboy.replace("</highlight>", "</span>"); + + // Font sizes: + tomboy.replace("<size:small>", "<span style='font-size: 7pt'>"); + tomboy.replace("</size:small>", "</span>"); + tomboy.replace("<size:large>", "<span style='font-size: 16pt'>"); + tomboy.replace("</size:large>", "</span>"); + tomboy.replace("<size:huge>", "<span style='font-size: 20pt'>"); + tomboy.replace("</size:huge>", "</span>"); + + // Internal links to other notes aren't supported yet by BasKet Note Pads: + tomboy.replace("<link:internal>", ""); + tomboy.replace("</link:internal>", ""); + + // In the Tomboy file, new lines are "\n" and not "<br>": + tomboy.replace("\n", "<br>\n"); + + // Preserve consecutive spaces: + return "<html><head><meta name=\"qrichtext\" content=\"1\" /></head><body>" + tomboy + "</body></html>"; +} + +Note* SoftwareImporters::insertTitledNote(Basket *parent, const QString &title, const QString &content, Qt::TextFormat format/* = Qt::PlainText*/, Note *parentNote/* = 0*/) +{ + Note *nGroup = new Note(parent); + + Note *nTitle = NoteFactory::createNoteText(title, parent); + nTitle->addState(Tag::stateForId("title")); + + Note *nContent; + if (format == Qt::PlainText) + nContent = NoteFactory::createNoteText(content, parent); + else + nContent = NoteFactory::createNoteHtml(content, parent); + + if (parentNote == 0) + parentNote = parent->firstNote(); // In the first column! + parent->insertNote(nGroup, parentNote, Note::BottomColumn, QPoint(), /*animate=*/false); + parent->insertNote(nTitle, nGroup, Note::BottomColumn, QPoint(), /*animate=*/false); + parent->insertNote(nContent, nTitle, Note::BottomInsert, QPoint(), /*animate=*/false); + + return nGroup; +} + +void SoftwareImporters::finishImport(Basket *basket) +{ + // Unselect the last inserted group: + basket->unselectAll(); + + // Focus the FIRST note (the last inserted note is currently focused!): + basket->setFocusedNote(basket->firstNoteShownInStack()); + + // Relayout every notes at theire new place and simulate a load animation (because already loaded just after the creation). + // Without a relayouting, notes on the bottom would comes from the top (because they were inserted on top) and clutter the animation load: + basket->relayoutNotes(/*animate=*/false); + basket->animateLoad(); + basket->save(); +} + + + +void SoftwareImporters::importKJots() +{ + QString dirPath = locateLocal("appdata","") + "/../kjots/"; // I think the assumption is good + QDir dir(dirPath, QString::null, QDir::Name | QDir::IgnoreCase, QDir::Files | QDir::NoSymLinks); + + QStringList list = dir.entryList(); + if (list.isEmpty()) + return; + + BasketFactory::newBasket(/*icon=*/"kjots", /*name=*/i18n("From KJots"), /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/0); + Basket *kjotsBasket = Global::bnpView->currentBasket(); + + for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) { // For each file + QFile file(dirPath + *it); + if (file.open(IO_ReadOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::Locale); + QString buf = stream.readLine(); + + // IT IS A NOTEBOOK FILE, AT THE VERION 0.6.x and older: + if ( !buf.isNull() && buf.left(9) == "\\NewEntry") { + + // First create a basket for it: + BasketFactory::newBasket(/*icon=*/"kjots", /*name=*/KURL(file.name()).fileName(), /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/kjotsBasket); + Basket *basket = Global::bnpView->currentBasket(); + basket->load(); + + QString title, body; + bool haveAnEntry = false; + while (1) { + if (buf.left(9) == "\\NewEntry") { + if (haveAnEntry) // Do not add note the first time + insertTitledNote(basket, title, Tools::stripEndWhiteSpaces(body)); + title = buf.mid(10, buf.length()); // Problem : basket will be saved + body = ""; // New note will then be created // EACH time a note is imported + haveAnEntry = true; + } else if (buf.left(3) != "\\ID") { // Don't care of the ID + // Remove escaped '\' characters and append the text to the body + int pos = 0; + while ( (pos = buf.find('\\', pos)) != -1 ) + if (buf[++pos] == '\\') + buf.remove(pos, 1); + body.append(buf + "\n"); + } + buf = stream.readLine(); + if (buf.isNull()) // OEF + break; + } + // Add the ending note (there isn't any other "\\NewEntry" to do it): + if (haveAnEntry) + insertTitledNote(basket, title, Tools::stripEndWhiteSpaces(body)); + finishImport(basket); + + // IT IS A NOTEBOOK XML FILE, AT THE VERION 0.7.0 and later: + } else if ( (*it).endsWith(".book") /*&& !buf.isNull() && (buf.left(2) == "<!" / *<!DOCTYPE...* / || buf.left(2) == "<?" / *<?xml...* /)*/) { + + QDomDocument *doc = XMLWork::openFile("KJots", dirPath + *it); + if (doc == 0) + continue; + + QString bookTitle = XMLWork::getElementText(doc->documentElement(), "KJotsBook/Title"); + + // First create a basket for it: + BasketFactory::newBasket(/*icon=*/"kjots", /*name=*/bookTitle, /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/kjotsBasket); + Basket *basket = Global::bnpView->currentBasket(); + basket->load(); + + QDomElement docElem = XMLWork::getElement(doc->documentElement(), "KJotsBook"); + for ( QDomNode n = docElem.firstChild(); !n.isNull(); n = n.nextSibling() ) { + QDomElement e = n.toElement(); + if ( (!e.isNull()) && e.tagName() == "KJotsPage" ) + insertTitledNote(basket, XMLWork::getElementText(e, "Title"), XMLWork::getElementText(e, "Text")); + } + finishImport(basket); + + } + + file.close(); + } + } +} + +void SoftwareImporters::importKNotes() +{ + QString dirPath = locateLocal("appdata","") + "/../knotes/"; // I thing the assumption is good + QDir dir(dirPath, QString::null, QDir::Name | QDir::IgnoreCase, QDir::Files | QDir::NoSymLinks); + + QStringList list = dir.entryList(); + for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) { // For each file + if ( ! (*it).endsWith(".ics") ) // Don't process *.ics~ and otehr files + continue; + QFile file(dirPath + *it); + if (file.open(IO_ReadOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); + + // First create a basket for it: + BasketFactory::newBasket(/*icon=*/"knotes", /*name=*/i18n("From KNotes"), /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/0); + Basket *basket = Global::bnpView->currentBasket(); + basket->load(); + + bool inVJournal = false; + bool inDescription = false; + bool isRichText = false; + QString title, body; + QString buf; + while (1) { + buf = stream.readLine(); + if (buf.isNull()) // OEF + break; + + if ( !buf.isNull() && buf == "BEGIN:VJOURNAL") { + inVJournal = true; + } else if (inVJournal && buf.startsWith("SUMMARY:")) { + title = buf.mid(8, buf.length()); + } else if (inVJournal && buf.startsWith("DESCRIPTION:")) { + body = buf.mid(12, buf.length()); + inDescription = true; + } else if (inDescription && buf.startsWith(" ")) { + body += buf.mid(1, buf.length()); + } else if (buf.startsWith("X-KDE-KNotes-RichText:")) { + isRichText = XMLWork::trueOrFalse(buf.mid(22, buf.length() - 22).stripWhiteSpace(), "false"); + } else if (buf == "END:VJOURNAL") { + insertTitledNote(basket, fromICS(title), fromICS(body), (isRichText ? Qt::RichText : Qt::PlainText)); + inVJournal = false; + inDescription = false; + isRichText = false; + title = ""; + body = ""; + } else + inDescription = false; + } + + // Bouh : duplicate code + // In case of unvalide ICAL file! + if ( ! body.isEmpty() ) // Add the ending note + insertTitledNote(basket, fromICS(title), fromICS(body), (isRichText ? Qt::RichText : Qt::PlainText)); + file.close(); + finishImport(basket); + } + } +} + +void SoftwareImporters::importStickyNotes() +{ + // Sticky Notes file is usually located in ~/.gnome2/stickynotes_applet + // We will search all directories in "~/" that contain "gnome" in the name, + // and will search for "stickynotes_applet" file (that should be XML file with <stickynotes> root. + QDir dir(QDir::home().absPath(), QString::null, QDir::Name | QDir::IgnoreCase, + QDir::Dirs | QDir::NoSymLinks | QDir::Hidden); + QStringList founds; + + QStringList list = dir.entryList(); + for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) { // For each folder + if ( (*it).contains("gnome", false) ) { + QString fullPath = QDir::home().absPath() + "/" + (*it) + "/stickynotes_applet"; + if (dir.exists(fullPath)) + founds += fullPath; + } + } + + for ( QStringList::Iterator it = founds.begin(); it != founds.end(); ++it ) { // For each file + QFile file(*it); + QDomDocument *doc = XMLWork::openFile("stickynotes", *it); + if (doc == 0) + continue; + + // First create a basket for it: + BasketFactory::newBasket(/*icon=*/"gnome", /*name=*/i18n("From Sticky Notes"), /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/0); + Basket *basket = Global::bnpView->currentBasket(); + basket->load(); + + QDomElement docElem = doc->documentElement(); + for ( QDomNode n = docElem.firstChild(); !n.isNull(); n = n.nextSibling() ) { + QDomElement e = n.toElement(); + if ( (!e.isNull()) && e.tagName() == "note" ) + insertTitledNote(basket, e.attribute("title"), e.text()); + } + finishImport(basket); + } +} + + + +// TODO: FIXME: Code duplicated from notecontent.cpp but with UTF-8 encoding. +// TODO: FIXME: Later, merge! +QString loadUtf8FileToString(const QString &fileName) +{ + QFile file(fileName); + if (file.open(IO_ReadOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); + QString text; + text = stream.read(); + file.close(); + return text; + } else + return ""; +} + + +void SoftwareImporters::importTomboy() +{ + QString dirPath = QDir::home().absPath() + "/.tomboy/"; // I thing the assumption is good + QDir dir(dirPath, QString::null, QDir::Name | QDir::IgnoreCase, QDir::Files | QDir::NoSymLinks); + + Basket *basket = 0; // Create the basket ONLY if we found at least one note to add! + + QStringList list = dir.entryList(); + for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) { // For each file + if ( ! (*it).endsWith(".note") ) + continue; + QDomDocument *doc = XMLWork::openFile("note", dirPath + *it); + if (doc == 0) + continue; + + if (basket == 0) { + // First create a basket for it: + BasketFactory::newBasket(/*icon=*/"tomboy", /*name=*/i18n("From Tomboy"), /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/0); + basket = Global::bnpView->currentBasket(); + basket->load(); + } + + QDomElement docElem = doc->documentElement(); + QString title = XMLWork::getElementText(docElem, "title"); + + // DOES NOT REALLY WORKS: + //QDomElement contentElement = XMLWork::getElement(docElem, "text/note-content"); + //QString content = XMLWork::innerXml(contentElement); + + // Isolate "<note-content version="0.1">CONTENT</note-content>"! + QString xml = loadUtf8FileToString(dirPath + *it); + xml = xml.mid(xml.find("<note-content ")); + xml = xml.mid(xml.find(">") + 1); + xml = xml.mid(0, xml.find("</note-content>")); + + if (!title.isEmpty() && !/*content*/xml.isEmpty()) + insertTitledNote(basket, title, fromTomboy(xml/*content*/), Qt::RichText); + } + + if (basket) + finishImport(basket); +} + +void SoftwareImporters::importTextFile() +{ + QString fileName = KFileDialog::getOpenFileName(":ImportTextFile", "*|All files"); + if (fileName.isEmpty()) + return; + + TextFileImportDialog dialog; + if (dialog.exec() == QDialog::Rejected) + return; + QString separator = dialog.separator(); + + QFile file(fileName); + if (file.open(IO_ReadOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::Locale); + QString content = stream.read(); + QStringList list = (separator.isEmpty() + ? QStringList(content) + : QStringList::split(separator, content, /*allowEmptyEntries=*/false) + ); + + // First create a basket for it: + QString title = i18n("From TextFile.txt", "From %1").arg(KURL(fileName).fileName()); + BasketFactory::newBasket(/*icon=*/"txt", title, /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", /*createIn=*/0); + Basket *basket = Global::bnpView->currentBasket(); + basket->load(); + + // Import every notes: + for (QStringList::Iterator it = list.begin(); it != list.end(); ++it ) { + Note *note = NoteFactory::createNoteFromText((*it).stripWhiteSpace(), basket); + basket->insertNote(note, basket->firstNote(), Note::BottomColumn, QPoint(), /*animate=*/false); + } + + // Finish the export: + finishImport(basket); + } +} + +/** @author Petri Damsten <[email protected]> + */ +void SoftwareImporters::importKnowIt() +{ + KURL url = KFileDialog::getOpenURL(":ImportKnowIt", + "*.kno|KnowIt files\n*|All files"); + if (!url.isEmpty()) + { + QFile file(url.path()); + QFileInfo info(url.path()); + Basket* basket = 0; + QPtrStack<Basket> baskets; + QString text; + int hierarchy = 0; + + TreeImportDialog dialog; + if (dialog.exec() == QDialog::Rejected) + return; + + hierarchy = dialog.choice(); + + BasketFactory::newBasket(/*icon=*/"knowit", + /*name=*/info.baseName(), + /*backgroundImage=*/"", + /*backgroundColor=*/QColor(), + /*textColor=*/QColor(), + /*templateName=*/"1column", + /*createIn=*/0); + basket = Global::bnpView->currentBasket(); + basket->load(); + baskets.push(basket); + + if(file.open(IO_ReadOnly)) + { + QTextStream stream(&file); + uint level = 0; + QString name; + QString line; + QStringList links; + QStringList descriptions; + + stream.setEncoding(QTextStream::UnicodeUTF8); + while(1) + { + line = stream.readLine(); + + if(line.startsWith("\\NewEntry") || + line.startsWith("\\CurrentEntry") || stream.atEnd()) + { + while(level + 1 < baskets.count()) + baskets.pop(); + if(level + 1 > baskets.count()) + baskets.push(basket); + + if(!name.isEmpty()) + { + if((level == 0 && hierarchy == 1) || + (hierarchy == 0)) + { + BasketFactory::newBasket(/*icon=*/"knowit", + /*name=*/name, + /*backgroundImage=*/"", + /*backgroundColor=*/QColor(), + /*textColor=*/QColor(), + /*templateName=*/"1column", + /*createIn=*/baskets.top()); + basket = Global::bnpView->currentBasket(); + basket->load(); + } + + if(!text.stripWhiteSpace().isEmpty() || + hierarchy == 2 || + (hierarchy == 1 && level > 0)) + { + insertTitledNote(basket, name, text, Qt::RichText); + } + for(uint j = 0; j < links.count(); ++j) + { + Note* link; + if(descriptions.count() < j+1 || descriptions[j].isEmpty()) + link = NoteFactory::createNoteLink(links[j], basket); + else + link = NoteFactory::createNoteLink(links[j], + descriptions[j], basket); + basket->insertCreatedNote(link); + } + finishImport(basket); + } + if(stream.atEnd()) + break; + + int i = line.find("Entry") + 6; + int n = line.find(' ', i); + level = line.mid(i, n - i).toInt(); + name = line.mid(n+1); + text = ""; + links.clear(); + descriptions.clear(); + } + else if(line.startsWith("\\Link")) + { + links.append(line.mid(6)); + } + else if(line.startsWith("\\Descr")) + { + while(descriptions.count() < links.count() - 1) + descriptions.append(""); + descriptions.append(line.mid(7)); + } + else + { + text += line + "\n"; + } + } + file.close(); + } + } +} + +void SoftwareImporters::importTuxCards() +{ + QString fileName = KFileDialog::getOpenFileName(":ImportTuxCards", "*|All files"); + if (fileName.isEmpty()) + return; + + TreeImportDialog dialog; + if (dialog.exec() == QDialog::Rejected) + return; + + int hierarchy = dialog.choice(); + + QDomDocument *document = XMLWork::openFile("tuxcards_data_file"/*"InformationCollection"*/, fileName); + if (document == 0) { + KMessageBox::error(0, i18n("Can not import that file. It is either corrupted or not a TuxCards file."), i18n("Bad File Format")); + return; + } + + QDomElement collection = document->documentElement(); + int remainingHierarchy = (hierarchy == 0 ? 65000 : 3 - hierarchy); + importTuxCardsNode(collection, /*parentBasket=*/0, /*parentNote=*/0, remainingHierarchy); +} + +// TODO: <InformationElement isOpen="true" isEncripted="false" + +void SoftwareImporters::importTuxCardsNode(const QDomElement &element, Basket *parentBasket, Note *parentNote, int remainingHierarchy) +{ + for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) { + QDomElement e = n.toElement(); + if (e.isNull() || e.tagName() != "InformationElement") // Cannot handle that! + continue; + + QString icon = e.attribute("iconFileName"); + QString name = XMLWork::getElementText(e, "Description"); + QString content = XMLWork::getElementText(e, "Information"); + bool isRichText = (e.attribute("informationFormat") == "RTF"); + bool isEncrypted = (e.attribute("isEncripted") == "true"); + if (icon.isEmpty() || icon == "none") + icon = "tuxcards"; + Note *nContent; + + if (isEncrypted) { + KMessageBox::information(0, i18n("A note is encrypted. The importer does not yet support encrypted notes. Please remove the encryption with TuxCards and re-import the file."), i18n("Encrypted Notes not Supported Yet")); + isRichText = true; + content = i18n("<font color='red'><b>Encrypted note.</b><br>The importer do not support encrypted notes yet. Please remove the encryption with TuxCards and re-import the file.</font>"); + } + + if (remainingHierarchy > 0) { + BasketFactory::newBasket(icon, name, /*backgroundImage=*/"", /*backgroundColor=*/QColor(), /*textColor=*/QColor(), /*templateName=*/"1column", parentBasket); + Basket *basket = Global::bnpView->currentBasket(); + basket->load(); + + if (isRichText) + nContent = NoteFactory::createNoteHtml(content, basket); + else + nContent = NoteFactory::createNoteText(content, basket); + basket->insertNote(nContent, basket->firstNote(), Note::BottomColumn, QPoint(), /*animate=*/false); + + importTuxCardsNode(e, basket, 0, remainingHierarchy - 1); + finishImport(basket); + } else { + Note *nGroup = insertTitledNote(parentBasket, name, content, (isRichText ? Qt::RichText : Qt::PlainText), parentNote); + importTuxCardsNode(e, parentBasket, nGroup, remainingHierarchy - 1); + } + } +} + +#include "softwareimporters.moc" diff --git a/src/softwareimporters.h b/src/softwareimporters.h new file mode 100644 index 0000000..05df6f8 --- /dev/null +++ b/src/softwareimporters.h @@ -0,0 +1,91 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef SOFTWAREIMPORTERS_H +#define SOFTWAREIMPORTERS_H + +#include <qnamespace.h> +#include <kdialogbase.h> + +class QString; +class QVButtonGroup; +class QDomElement; +class KTextEdit; + +class Basket; +class Note; + +/** The dialog to ask how to import hierarchical data. + * @author Sébastien Laoût + */ +class TreeImportDialog : public KDialogBase +{ + Q_OBJECT + public: + TreeImportDialog(QWidget *parent = 0); + ~TreeImportDialog(); + int choice(); + private: + QVButtonGroup *m_choices; +}; + +/** The dialog to ask how to import text files. + * @author Sébastien Laoût + */ +class TextFileImportDialog : public KDialogBase +{ + Q_OBJECT + public: + TextFileImportDialog(QWidget *parent = 0); + ~TextFileImportDialog(); + QString separator(); + protected slots: + void customSeparatorChanged(); + private: + QVButtonGroup *m_choices; + QRadioButton *m_anotherSeparator; + QTextEdit *m_customSeparator; +}; + +/** Functions that import data from other softwares. + * @author Sébastien Laoût + */ +namespace SoftwareImporters +{ + // Useful methods to design importers: + QString fromICS(const QString &ics); + QString fromTomboy(QString tomboy); + Note* insertTitledNote(Basket *parent, const QString &title, const QString &content, Qt::TextFormat format = Qt::PlainText, Note *parentNote = 0); + void finishImport(Basket *basket); + + // The importers in themselves: + void importKNotes(); + void importKJots(); + void importKnowIt(); + void importTuxCards(); + void importStickyNotes(); + void importTomboy(); + void importTextFile(); + + // + void importTuxCardsNode(const QDomElement &element, Basket *parentBasket, Note *parentNote, int remainingHierarchy); +} + +#endif // SOFTWAREIMPORTERS_H diff --git a/src/systemtray.cpp b/src/systemtray.cpp new file mode 100644 index 0000000..d245a3a --- /dev/null +++ b/src/systemtray.cpp @@ -0,0 +1,463 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +/** KSystemTray2 */ + +// To draw the systray screenshot image: +#include <qdockwindow.h> +#include <qmovie.h> +#include <qvariant.h> +#include "linklabel.h" +#include "note.h" + +#include <qdesktopwidget.h> +#include <qmime.h> +#include <qpainter.h> +#include <qpoint.h> +#include <qpixmap.h> +// To know the program name: +#include <kglobal.h> +#include <kinstance.h> +#include <kaboutdata.h> +#include <kiconeffect.h> +// Others: +#include <kmessagebox.h> +#include <kmanagerselection.h> +#include <kdeversion.h> +#include <kapplication.h> +#include <kpopupmenu.h> +#include <kiconloader.h> +#include <kdebug.h> +#include "systemtray.h" +#include "basket.h" +#include "settings.h" +#include "global.h" +#include "tools.h" + +KSystemTray2::KSystemTray2(QWidget *parent, const char *name) + : KSystemTray(parent, name) +{ +} + +KSystemTray2::~KSystemTray2() +{ +} + +void KSystemTray2::displayCloseMessage(QString fileMenu) +{ + /* IDEAS OF IMPROVEMENTS: + * - Use queuedMessageBox() but it need a dontAskAgainName parameter + * and image in QMimeSourceFactory shouldn't be removed. + * - Sometimes the systray icon is covered (a passive popup...) + * Use XComposite extension, if available, to get the kicker pixmap. + * - Perhapse desaturate the area around the proper SysTray icon, + * helping bring it into sharper focus. Or draw the cicle with XOR + * brush. + * - Perhapse add the icon in the text (eg. "... in the + * system tray ([icon])."). Add some clutter to the dialog. + */ +#if KDE_IS_VERSION( 3, 1, 90 ) + // Don't do all the computations if they are unneeded: + if ( ! KMessageBox::shouldBeShownContinue("hideOnCloseInfo") ) + return; +#endif + // "Default parameter". Here, to avoid a i18n() call and dependancy in the .h + if (fileMenu.isEmpty()) + fileMenu = i18n("File"); + + // Some values we need: + QPoint g = mapToGlobal(pos()); + int desktopWidth = kapp->desktop()->width(); + int desktopHeight = kapp->desktop()->height(); + int tw = width(); + int th = height(); + + // We are triying to make a live screenshot of the systray icon to circle it + // and show it to the user. If no systray is used or if the icon is not visible, + // we should not show that screenshot but only a text! + + // 1. Determine if the user use a system tray area or not: + QCString screenstr; + screenstr.setNum(qt_xscreen()); + QCString trayatom = "_NET_SYSTEM_TRAY_S" + screenstr; + bool useSystray = (KSelectionWatcher(trayatom).owner() != 0L); + + // 2. And then if the icon is visible too (eg. this->show() has been called): + useSystray = useSystray && isVisible(); + + // 3. Kicker (or another systray manager) can be visible but masked out of + // the screen (ie. on right or on left of it). We check if the icon isn't + // out of screen. + if (useSystray) { + QRect deskRect(0, 0, desktopWidth, desktopHeight); + if ( !deskRect.contains(g.x(), g.y()) || + !deskRect.contains(g.x() + tw, g.y() + th) ) + useSystray = false; + } + + // 4. We raise the window containing the systray icon (typically the kicker) to + // have the most chances it is visible during the capture: +/* if (useSystray) { + // We are testing if one of the corners is hidden, and if yes, we would enter + // a time consuming process (raise kicker and wait some time): +// if (kapp->widgetAt(g) != this || +// kapp->widgetAt(g + QPoint(tw-1, 0)) != this || +// kapp->widgetAt(g + QPoint(0, th-1)) != this || +// kapp->widgetAt(g + QPoint(tw-1, th-1)) != this) { + int systrayManagerWinId = topLevelWidget()->winId(); + KWin::forceActiveWindow(systrayManagerWinId); + kapp->processEvents(); // Because without it the systrayManager is raised only after the messageBox is displayed +// KWin::activateWindow(systrayManagerWinId); +// kapp->processEvents(); // Because without it the systrayManager is raised only after the messageBox is displayed +// KWin::raiseWindow(systrayManagerWinId); +// kapp->processEvents(); // Because without it the systrayManager is raised only after the messageBox is displayed + sleep(1); + // TODO: Re-verify that at least one corner is now visible +// } + }*/ + +// KMessageBox::information(this, QString::number(g.x()) + ":" + QString::number(g.y()) + ":" + +// QString::number((int)(kapp->widgetAt(g+QPoint(1,1))))); + + QString message = i18n( + "<p>Closing the main window will keep %1 running in the system tray. " + "Use <b>Quit</b> from the <b>Basket</b> menu to quit the application.</p>" + ).arg(KGlobal::instance()->aboutData()->programName()); + // We are sure the systray icon is visible: ouf! + if (useSystray) { + // Compute size and position of the pixmap to be grabbed: + int w = desktopWidth / 4; + int h = desktopHeight / 9; + int x = g.x() + tw/2 - w/2; // Center the rectange in the systray icon + int y = g.y() + th/2 - h/2; + if (x < 0) x = 0; // Move the rectangle to stay in the desktop limits + if (y < 0) y = 0; + if (x + w > desktopWidth) x = desktopWidth - w; + if (y + h > desktopHeight) y = desktopHeight - h; + + // Grab the desktop and draw a circle arround the icon: + QPixmap shot = QPixmap::grabWindow(qt_xrootwin(), x, y, w, h); + QPainter painter(&shot); + const int CIRCLE_MARGINS = 6; + const int CIRCLE_WIDTH = 3; + const int SHADOW_OFFSET = 1; + const int IMAGE_BORDER = 1; + int ax = g.x() - x - CIRCLE_MARGINS - 1; + int ay = g.y() - y - CIRCLE_MARGINS - 1; + painter.setPen( QPen(KApplication::palette().active().dark(), CIRCLE_WIDTH) ); + painter.drawArc(ax + SHADOW_OFFSET, ay + SHADOW_OFFSET, + tw + 2*CIRCLE_MARGINS, th + 2*CIRCLE_MARGINS, 0, 16*360); + painter.setPen( QPen(Qt::red/*KApplication::palette().active().highlight()*/, CIRCLE_WIDTH) ); + painter.drawArc(ax, ay, tw + 2*CIRCLE_MARGINS, th + 2*CIRCLE_MARGINS, 0, 16*360); +#if 1 + // Draw the pixmap over the screenshot in case a window hide the icon: + painter.drawPixmap(g.x() - x, g.y() - y + 1, *pixmap()); +#endif + painter.end(); + + // Then, we add a border arround the image to make it more visible: + QPixmap finalShot(w + 2*IMAGE_BORDER, h + 2*IMAGE_BORDER); + finalShot.fill(KApplication::palette().active().foreground()); + painter.begin(&finalShot); + painter.drawPixmap(IMAGE_BORDER, IMAGE_BORDER, shot); + painter.end(); + + // Associate source to image and show the dialog: + QMimeSourceFactory::defaultFactory()->setPixmap("systray_shot", finalShot); + KMessageBox::information(kapp->activeWindow(), + message + "<p><center><img source=\"systray_shot\"></center></p>", + i18n("Docking in System Tray"), "hideOnCloseInfo"); + QMimeSourceFactory::defaultFactory()->setData("systray_shot", 0L); + } else { + KMessageBox::information(kapp->activeWindow(), + message, + i18n("Docking in System Tray"), "hideOnCloseInfo"); + } +} + +/** SystemTray */ + +SystemTray::SystemTray(QWidget *parent, const char *name) + : KSystemTray2(parent, name != 0 ? name : "SystemTray"), m_showTimer(0), m_autoShowTimer(0) +{ + setAcceptDrops(true); + + m_showTimer = new QTimer(this); + connect( m_showTimer, SIGNAL(timeout()), Global::bnpView, SLOT(setActive()) ); + + m_autoShowTimer = new QTimer(this); + connect( m_autoShowTimer, SIGNAL(timeout()), Global::bnpView, SLOT(setActive()) ); + + // Create pixmaps for the icon: + m_iconPixmap = loadIcon("basket"); +// FIXME: When main window is shown at start, the icon is loaded 1 pixel too high +// and then reloaded instantly after at the right position. +// setPixmap(m_iconPixmap); // Load it the sooner as possible to avoid flicker + QImage lockedIconImage = m_iconPixmap.convertToImage(); + QPixmap lockOverlayPixmap = loadIcon("lockoverlay"); + QImage lockOverlayImage = lockOverlayPixmap.convertToImage(); + KIconEffect::overlay(lockedIconImage, lockOverlayImage); + m_lockedIconPixmap.convertFromImage(lockedIconImage); + + updateToolTip(); // Set toolTip AND icon +} + +SystemTray::~SystemTray() +{ +} + +void SystemTray::mousePressEvent(QMouseEvent *event) +{ + if (event->button() & Qt::LeftButton) { // Prepare drag + m_pressPos = event->globalPos(); + m_canDrag = true; + event->accept(); + } else if (event->button() & Qt::MidButton) { // Paste + Global::bnpView->currentBasket()->setInsertPopupMenu(); + Global::bnpView->currentBasket()->pasteNote(QClipboard::Selection); + Global::bnpView->currentBasket()->cancelInsertPopupMenu(); + if (Settings::usePassivePopup()) + Global::bnpView->showPassiveDropped(i18n("Pasted selection to basket <i>%1</i>")); + event->accept(); + } else if (event->button() & Qt::RightButton) { // Popup menu + KPopupMenu menu(this); + menu.insertTitle( SmallIcon("basket"), kapp->aboutData()->programName() ); + + Global::bnpView->actNewBasket->plug(&menu); + Global::bnpView->actNewSubBasket->plug(&menu); + Global::bnpView->actNewSiblingBasket->plug(&menu); + menu.insertSeparator(); + Global::bnpView->m_actPaste->plug(&menu); + Global::bnpView->m_actGrabScreenshot->plug(&menu); + Global::bnpView->m_actColorPicker->plug(&menu); + + if(!Global::bnpView->isPart()) + { + KAction* action; + + menu.insertSeparator(); + + action = Global::bnpView->actionCollection()->action("options_configure_global_keybinding"); + if(action) + action->plug(&menu); + + action = Global::bnpView->actionCollection()->action("options_configure"); + if(action) + action->plug(&menu); + + menu.insertSeparator(); + + // Minimize / restore : since we manage the popup menu by ourself, we should do that work : + action = Global::bnpView->actionCollection()->action("minimizeRestore"); + if(action) + { + if (Global::mainWindow()->isVisible()) + action->setText(i18n("&Minimize")); + else + action->setText(i18n("&Restore")); + action->plug(&menu); + } + + action = Global::bnpView->actionCollection()->action("file_quit"); + if(action) + action->plug(&menu); + } + + Global::bnpView->currentBasket()->setInsertPopupMenu(); + connect( &menu, SIGNAL(aboutToHide()), Global::bnpView->currentBasket(), SLOT(delayedCancelInsertPopupMenu()) ); + menu.exec(event->globalPos()); + event->accept(); + } else + event->ignore(); +} + +void SystemTray::mouseMoveEvent(QMouseEvent *event) +{ + event->ignore(); +} + +void SystemTray::mouseReleaseEvent(QMouseEvent *event) +{ + m_canDrag = false; + if (event->button() == Qt::LeftButton) // Show / hide main window + if ( rect().contains(event->pos()) ) { // Accept only if released in systemTray + toggleActive(); + emit showPart(); + event->accept(); + } else + event->ignore(); +} + +void SystemTray::dragEnterEvent(QDragEnterEvent *event) +{ + m_showTimer->start( Settings::dropTimeToShow() * 100, true ); + Global::bnpView->currentBasket()->showFrameInsertTo(); +/// m_parentContainer->setStatusBarDrag(); // FIXME: move this line in Basket::showFrameInsertTo() ? + Basket::acceptDropEvent(event); +} + +void SystemTray::dragMoveEvent(QDragMoveEvent *event) +{ + Basket::acceptDropEvent(event); +} + +void SystemTray::dragLeaveEvent(QDragLeaveEvent*) +{ + m_showTimer->stop(); + m_canDrag = false; + Global::bnpView->currentBasket()->resetInsertTo(); + Global::bnpView->updateStatusBarHint(); +} + +#include <iostream> + +void SystemTray::dropEvent(QDropEvent *event) +{ + m_showTimer->stop(); + + Global::bnpView->currentBasket()->blindDrop(event); + +/* Basket *basket = Global::bnpView->currentBasket(); + if (!basket->isLoaded()) { + Global::bnpView->showPassiveLoading(basket); + basket->load(); + } + basket->contentsDropEvent(event); + std::cout << (long) basket->selectedNotes() << std::endl; + + if (Settings::usePassivePopup()) + Global::bnpView->showPassiveDropped(i18n("Dropped to basket <i>%1</i>"));*/ +} + +/* This function comes directly from JuK: */ + +/* + * This function copies the entirety of src into dest, starting in + * dest at x and y. This function exists because I was unable to find + * a function like it in either QImage or kdefx + */ +static bool copyImage(QImage &dest, QImage &src, int x, int y) +{ + if(dest.depth() != src.depth()) + return false; + if((x + src.width()) >= dest.width()) + return false; + if((y + src.height()) >= dest.height()) + return false; + + // We want to use KIconEffect::overlay to do this, since it handles + // alpha, but the images need to be the same size. We can handle that. + + QImage large_src(dest); + + // It would perhaps be better to create large_src based on a size, but + // this is the easiest way to make a new image with the same depth, size, + // etc. + + large_src.detach(); + + // However, we do have to specifically ensure that setAlphaBuffer is set + // to false + + large_src.setAlphaBuffer(false); + large_src.fill(0); // All transparent pixels + large_src.setAlphaBuffer(true); + + int w = src.width(); + int h = src.height(); + for(int dx = 0; dx < w; dx++) + for(int dy = 0; dy < h; dy++) + large_src.setPixel(dx + x, dy + y, src.pixel(dx, dy)); + + // Apply effect to image + + KIconEffect::overlay(dest, large_src); + + return true; +} + +void SystemTray::updateToolTip() +{ +// return; ///////////////////////////////////////////////////// + + Basket *basket = Global::bnpView->currentBasket(); + if (!basket) + return; + + if (basket->icon().isEmpty() || basket->icon() == "basket" || ! Settings::showIconInSystray()) + setPixmap(basket->isLocked() ? m_lockedIconPixmap : m_iconPixmap); + else { + // Code that comes from JuK: + QPixmap bgPix = loadIcon("basket"); + QPixmap fgPix = SmallIcon(basket->icon()); + + QImage bgImage = bgPix.convertToImage(); // Probably 22x22 + QImage fgImage = fgPix.convertToImage(); // Should be 16x16 + QImage lockOverlayImage = loadIcon("lockoverlay").convertToImage(); + + KIconEffect::semiTransparent(bgImage); + copyImage(bgImage, fgImage, (bgImage.width() - fgImage.width()) / 2, + (bgImage.height() - fgImage.height()) / 2); + if (basket->isLocked()) + KIconEffect::overlay(bgImage, lockOverlayImage); + + bgPix.convertFromImage(bgImage); + setPixmap(bgPix); + } + + //QTimer::singleShot( Container::c_delayTooltipTime, this, SLOT(updateToolTipDelayed()) ); + // No need to delay: it's be called when notes are changed: + updateToolTipDelayed(); +} + +void SystemTray::updateToolTipDelayed() +{ + Basket *basket = Global::bnpView->currentBasket(); + + QString tip = "<p><nobr>" + ( basket->isLocked() ? kapp->makeStdCaption(i18n("%1 (Locked)")) + : kapp->makeStdCaption( "%1") ) + .arg(Tools::textToHTMLWithoutP(basket->basketName())); + + QToolTip::add(this, tip); +} + +void SystemTray::wheelEvent(QWheelEvent *event) +{ + if (event->delta() > 0) + Global::bnpView->goToPreviousBasket(); + else + Global::bnpView->goToNextBasket(); + + if (Settings::usePassivePopup()) + Global::bnpView->showPassiveContent(); +} + +void SystemTray::enterEvent(QEvent*) +{ + if (Settings::showOnMouseIn()) + m_autoShowTimer->start(Settings::timeToShowOnMouseIn() * 100, true ); +} + +void SystemTray::leaveEvent(QEvent*) +{ + m_autoShowTimer->stop(); +} + +#include "systemtray.moc" diff --git a/src/systemtray.h b/src/systemtray.h new file mode 100644 index 0000000..d36bd9b --- /dev/null +++ b/src/systemtray.h @@ -0,0 +1,85 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef SYSTEMTRAY_H +#define SYSTEMTRAY_H + +#include <ksystemtray.h> + +class MainWindow; + +/** Convenient class to develop the displayCloseMessage() dialog + * hopefuly integrated in KDE 3.4 + * @author S�astien Laot + */ +class KSystemTray2 : public KSystemTray +{ + Q_OBJECT + public: + KSystemTray2(QWidget *parent = 0, const char *name = 0); + ~KSystemTray2(); + /** + * Call this method when the user clicked the close button of the window + * (the [x]) to inform him that the application sit in the system tray + * and willn't be closed (as he is used to). + * + * You usualy call it from reimplemented KMainWindow::queryClose() + * + * @since 3.4 + */ + void displayCloseMessage(QString fileMenu = ""); +}; + +/** This class provide a personalized system tray icon. + * @author S�astien Laot + */ +class SystemTray : public KSystemTray2 +{ + Q_OBJECT + public: + SystemTray(QWidget *parent = 0, const char *name = 0); + ~SystemTray(); + protected: + void mousePressEvent(QMouseEvent *event); + void mouseMoveEvent(QMouseEvent *event); + void mouseReleaseEvent(QMouseEvent *event); + virtual void dragEnterEvent(QDragEnterEvent *event); + virtual void dragMoveEvent(QDragMoveEvent* event); + virtual void dragLeaveEvent(QDragLeaveEvent*); + virtual void dropEvent(QDropEvent *event); + void wheelEvent(QWheelEvent *event); + void enterEvent(QEvent*); + void leaveEvent(QEvent*); + public slots: + void updateToolTip(); + protected slots: + void updateToolTipDelayed(); + signals: + void showPart(); + private: + QTimer *m_showTimer; + QTimer *m_autoShowTimer; + bool m_canDrag; + QPoint m_pressPos; + QPixmap m_iconPixmap; + QPixmap m_lockedIconPixmap; +}; + +#endif // SYSTEMTRAY_H diff --git a/src/tag.cpp b/src/tag.cpp new file mode 100644 index 0000000..a4ed7e6 --- /dev/null +++ b/src/tag.cpp @@ -0,0 +1,934 @@ +/*************************************************************************** + * Copyright (C) 2005 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kapplication.h> +#include <kstyle.h> +#include <kiconloader.h> +#include <qpainter.h> +#include <qfont.h> +#include <qdom.h> +#include <qdir.h> +#include <kglobalsettings.h> +#include <klocale.h> + +#include "tag.h" +#include "xmlwork.h" +#include "global.h" +#include "debugwindow.h" +#include "bnpview.h" +#include "tools.h" +#include "basket.h" + +#include <iostream> + +/** class State: */ + +State::State(const QString &id, Tag *tag) + : m_id(id), m_name(), m_emblem(), m_bold(false), m_italic(false), m_underline(false), m_strikeOut(false), + m_textColor(), m_fontName(), m_fontSize(-1), m_backgroundColor(), m_textEquivalent(), m_onAllTextLines(false), m_parentTag(tag) +{ +} + +State::~State() +{ +} + +State* State::nextState(bool cycle /*= true*/) +{ + if (!parentTag()) + return 0; + + List states = parentTag()->states(); + // The tag contains only one state: + if (states.count() == 1) + return 0; + // Find the next state: + for (List::iterator it = states.begin(); it != states.end(); ++it) + // Found the current state in the list: + if (*it == this) { + // Find the next state: + State *next = *(++it); + if (it == states.end()) + return (cycle ? states.first() : 0); + return next; + } + // Should not happens: + return 0; +} + +QString State::fullName() +{ + if (!parentTag() || parentTag()->states().count() == 1) + return (name().isEmpty() && parentTag() ? parentTag()->name() : name()); + return QString(i18n("%1: %2")).arg(parentTag()->name(), name()); +} + +QFont State::font(QFont base) +{ + if (bold()) + base.setBold(true); + if (italic()) + base.setItalic(true); + if (underline()) + base.setUnderline(true); + if (strikeOut()) + base.setStrikeOut(true); + if (!fontName().isEmpty()) + base.setFamily(fontName()); + if (fontSize() > 0) + base.setPointSize(fontSize()); + return base; +} + +QString State::toCSS(const QString &gradientFolderPath, const QString &gradientFolderName, const QFont &baseFont) +{ + QString css; + if (bold()) + css += " font-weight: bold;"; + if (italic()) + css += " font-style: italic;"; + if (underline() && strikeOut()) + css += " text-decoration: underline line-through;"; + else if (underline()) + css += " text-decoration: underline;"; + else if (strikeOut()) + css += " text-decoration: line-through;"; + if (textColor().isValid()) + css += " color: " + textColor().name() + ";"; + if (!fontName().isEmpty()) { + QString fontFamily = Tools::cssFontDefinition(fontName(), /*onlyFontFamily=*/true); + css += " font-family: " + fontFamily + ";"; + } + if (fontSize() > 0) + css += " font-size: " + QString::number(fontSize()) + "px;"; + if (backgroundColor().isValid()) { + // Get the colors of the gradient and the border: + QColor topBgColor; + QColor bottomBgColor; + Note::getGradientColors(backgroundColor(), &topBgColor, &bottomBgColor); + // Produce the CSS code: + QString gradientFileName = Basket::saveGradientBackground(backgroundColor(), font(baseFont), gradientFolderPath); + css += " background: " + bottomBgColor.name() + " url('" + gradientFolderName + gradientFileName + "') repeat-x;"; + css += " border-top: solid " + topBgColor.name() + " 1px;"; + css += " border-bottom: solid " + Tools::mixColor(topBgColor, bottomBgColor).name() + " 1px;"; + } + + if (css.isEmpty()) + return ""; + else + return " .tag_" + id() + " {" + css + " }\n"; +} + +void State::merge(const List &states, State *result, int *emblemsCount, bool *haveInvisibleTags, const QColor &backgroundColor) +{ + *result = State(); // Reset to default values. + *emblemsCount = 0; + *haveInvisibleTags = false; + + for (List::const_iterator it = states.begin(); it != states.end(); ++it) { + State *state = *it; + bool isVisible = false; + // For each propertie, if that properties have a value (is not default) is the current state of the list, + // and if it haven't been set to the result state by a previous state, then it's visible and we assign the propertie to the result state. + if (!state->emblem().isEmpty()) { + ++*emblemsCount; + isVisible = true; + } + if (state->bold() && !result->bold()) { + result->setBold(true); + isVisible = true; + } + if (state->italic() && !result->italic()) { + result->setItalic(true); + isVisible = true; + } + if (state->underline() && !result->underline()) { + result->setUnderline(true); + isVisible = true; + } + if (state->strikeOut() && !result->strikeOut()) { + result->setStrikeOut(true); + isVisible = true; + } + if (state->textColor().isValid() && !result->textColor().isValid()) { + result->setTextColor(state->textColor()); + isVisible = true; + } + if (!state->fontName().isEmpty() && result->fontName().isEmpty()) { + result->setFontName(state->fontName()); + isVisible = true; + } + if (state->fontSize() > 0 && result->fontSize() <= 0) { + result->setFontSize(state->fontSize()); + isVisible = true; + } + if (state->backgroundColor().isValid() && !result->backgroundColor().isValid() && state->backgroundColor() != backgroundColor) { // vv + result->setBackgroundColor(state->backgroundColor()); // This is particular: if the note background color is the same as the basket one, don't use that. + isVisible = true; + } + // If it's not visible, well, at least one tag is not visible: the note will display "..." at the tags arrow place to show that: + if (!isVisible) + *haveInvisibleTags = true; + } +} + +void State::copyTo(State *other) +{ + other->m_id = m_id; + other->m_name = m_name; + other->m_emblem = m_emblem; + other->m_bold = m_bold; + other->m_italic = m_italic; + other->m_underline = m_underline; + other->m_strikeOut = m_strikeOut; + other->m_textColor = m_textColor; + other->m_fontName = m_fontName; + other->m_fontSize = m_fontSize; + other->m_backgroundColor = m_backgroundColor; + other->m_textEquivalent = m_textEquivalent; + other->m_onAllTextLines = m_onAllTextLines; // TODO + //TODO: other->m_parentTag; +} + +/** class Tag: */ + +Tag::List Tag::all = Tag::List(); + +long Tag::nextStateUid = 1; + +long Tag::getNextStateUid() +{ + return nextStateUid++; // Return the next Uid and THEN increment the Uid +} + +Tag::Tag() +{ + static int tagNumber = 0; + ++tagNumber; + QString sAction = "tag_shortcut_number_" + QString::number(tagNumber); + m_action = new KAction("FAKE TEXT", "FAKE ICON", KShortcut(), Global::bnpView, SLOT(activatedTagShortcut()), Global::bnpView->actionCollection(), sAction); + m_action->setShortcutConfigurable(false); // We do it in the tag properties dialog + + m_inheritedBySiblings = false; +} + +Tag::~Tag() +{ + delete m_action; +} + +void Tag::setName(const QString &name) +{ + m_name = name; + m_action->setText("TAG SHORTCUT: " + name); // TODO: i18n (for debug purpose only by now). +} + +State* Tag::stateForId(const QString &id) +{ + for (List::iterator it = all.begin(); it != all.end(); ++it) + for (State::List::iterator it2 = (*it)->states().begin(); it2 != (*it)->states().end(); ++it2) + if ((*it2)->id() == id) + return *it2; + return 0; +} + +Tag* Tag::tagForKAction(KAction *action) +{ + for (List::iterator it = all.begin(); it != all.end(); ++it) + if ((*it)->m_action == action) + return *it; + return 0; +} + +QMap<QString, QString> Tag::loadTags(const QString &path/* = QString()*//*, bool merge = false*/) +{ + QMap<QString, QString> mergedStates; + + bool merge = !path.isEmpty(); + QString fullPath = (merge ? path : Global::savesFolder() + "tags.xml"); + QString doctype = "basketTags"; + + QDir dir; + if (!dir.exists(fullPath)) { + if (merge) + return mergedStates; + DEBUG_WIN << "Tags file does not exist: Creating it..."; + createDefaultTagsSet(fullPath); + } + + QDomDocument *document = XMLWork::openFile(doctype, fullPath); + if (!document) { + DEBUG_WIN << "<font color=red>FAILED to read the tags file</font>"; + return mergedStates; + } + + QDomElement docElem = document->documentElement(); + if (!merge) + nextStateUid = docElem.attribute("nextStateUid", QString::number(nextStateUid)).toLong(); + + QDomNode node = docElem.firstChild(); + while (!node.isNull()) { + QDomElement element = node.toElement(); + if ( (!element.isNull()) && element.tagName() == "tag" ) { + Tag *tag = new Tag(); + // Load properties: + QString name = XMLWork::getElementText(element, "name"); + QString shortcut = XMLWork::getElementText(element, "shortcut"); + QString inherited = XMLWork::getElementText(element, "inherited", "false"); + tag->setName(name); + tag->setShortcut(KShortcut(shortcut)); + tag->setInheritedBySiblings(XMLWork::trueOrFalse(inherited)); + // Load states: + QDomNode subNode = element.firstChild(); + while (!subNode.isNull()) { + QDomElement subElement = subNode.toElement(); + if ( (!subElement.isNull()) && subElement.tagName() == "state" ) { + State *state = new State(subElement.attribute("id"), tag); + state->setName( XMLWork::getElementText(subElement, "name") ); + state->setEmblem( XMLWork::getElementText(subElement, "emblem") ); + QDomElement textElement = XMLWork::getElement(subElement, "text"); + state->setBold( XMLWork::trueOrFalse(textElement.attribute("bold", "false")) ); + state->setItalic( XMLWork::trueOrFalse(textElement.attribute("italic", "false")) ); + state->setUnderline( XMLWork::trueOrFalse(textElement.attribute("underline", "false")) ); + state->setStrikeOut( XMLWork::trueOrFalse(textElement.attribute("strikeOut", "false")) ); + QString textColor = textElement.attribute("color", ""); + state->setTextColor(textColor.isEmpty() ? QColor() : QColor(textColor)); + QDomElement fontElement = XMLWork::getElement(subElement, "font"); + state->setFontName(fontElement.attribute("name", "")); + QString fontSize = fontElement.attribute("size", ""); + state->setFontSize(fontSize.isEmpty() ? -1 : fontSize.toInt()); + QString backgroundColor = XMLWork::getElementText(subElement, "backgroundColor", ""); + state->setBackgroundColor(backgroundColor.isEmpty() ? QColor() : QColor(backgroundColor)); + QDomElement textEquivalentElement = XMLWork::getElement(subElement, "textEquivalent"); + state->setTextEquivalent( textEquivalentElement.attribute("string", "") ); + state->setOnAllTextLines( XMLWork::trueOrFalse(textEquivalentElement.attribute("onAllTextLines", "false")) ); + tag->appendState(state); + } + subNode = subNode.nextSibling(); + } + // If the Tag is Valid: + if (tag->countStates() > 0) { + // Rename Things if Needed: + State *firstState = tag->states().first(); + if (tag->countStates() == 1 && firstState->name().isEmpty()) + firstState->setName(tag->name()); + if (tag->name().isEmpty()) + tag->setName(firstState->name()); + // Add or Merge the Tag: + if (!merge) { + all.append(tag); + } else { + Tag *similarTag = tagSimilarTo(tag); + // Tag does not exists, add it: + if (similarTag == 0) { + // We are merging the new states, so we should choose new and unique (on that computer) ids for those states: + for (State::List::iterator it = tag->states().begin(); it != tag->states().end(); ++it) { + State *state = *it; + QString uid = state->id(); + QString newUid = "tag_state_" + QString::number(getNextStateUid()); + state->setId(newUid); + mergedStates[uid] = newUid; + } + // TODO: if shortcut is already assigned to a previous note, do not import it, keep the user settings! + all.append(tag); + // Tag already exists, rename to theire ids: + } else { + State::List::iterator it2 = similarTag->states().begin(); + for (State::List::iterator it = tag->states().begin(); it != tag->states().end(); ++it, ++it2) { + State *state = *it; + State *similarState = *it2; + QString uid = state->id(); + QString newUid = similarState->id(); + if (uid != newUid) + mergedStates[uid] = newUid; + } + delete tag; // Already exists, not to be merged. Delete the shortcut and all. + } + } + } + } + node = node.nextSibling(); + } + + return mergedStates; +} + +Tag* Tag::tagSimilarTo(Tag *tagToTest) +{ + // Tags are considered similar if they have the same name, the same number of states, in the same order, and the same look. + // Keyboard shortcut, text equivalent and onEveryLines are user settings, and thus not considered during the comparision. + // Default tags (To Do, Important, Idea...) do not take into account the name of the tag and states during the comparision. + // Default tags are equal only if they have the same number of states, in the same order, and the same look. + // This is because default tag names are translated differently in every countries, but they are essentialy the same! + // User tags begins with "tag_state_" followed by a number. Default tags are the other ones. + + // Browse all tags: + for (List::iterator it = all.begin(); it != all.end(); ++it) { + Tag *tag = *it; + bool same = true; + bool sameName; + bool defaultTag = true; + // We test only name and look. Shorcut and whenever it is inherited by sibling new notes are user settings only! + sameName = tag->name() == tagToTest->name(); + if (tag->countStates() != tagToTest->countStates()) + continue; // Tag is different! + // We found a tag with same name, check if every states/look are same too: + State::List::iterator itTest = tagToTest->states().begin(); + for (State::List::iterator it2 = (*it)->states().begin(); it2 != (*it)->states().end(); ++it2, ++itTest) { + State *state = *it2; + State *stateToTest = *itTest; + if (state->id().startsWith("tag_state_") || stateToTest->id().startsWith("tag_state_")) { defaultTag = false; } + if (state->name() != stateToTest->name()) { sameName = false; } + if (state->emblem() != stateToTest->emblem()) { same = false; break; } + if (state->bold() != stateToTest->bold()) { same = false; break; } + if (state->italic() != stateToTest->italic()) { same = false; break; } + if (state->underline() != stateToTest->underline()) { same = false; break; } + if (state->strikeOut() != stateToTest->strikeOut()) { same = false; break; } + if (state->textColor() != stateToTest->textColor()) { same = false; break; } + if (state->fontName() != stateToTest->fontName()) { same = false; break; } + if (state->fontSize() != stateToTest->fontSize()) { same = false; break; } + if (state->backgroundColor() != stateToTest->backgroundColor()) { same = false; break; } + // Text equivalent (as well as onAllTextLines) is also a user setting! + } + // We found an existing tag that is "exactly" the same: + if (same && (sameName || defaultTag)) + return tag; + } + + // Not found: + return 0; +} + +void Tag::saveTags() +{ + DEBUG_WIN << "Saving tags..."; + saveTagsTo(all, Global::savesFolder() + "tags.xml"); +} + +void Tag::saveTagsTo(QValueList<Tag*> &list, const QString &fullPath) +{ + // Create Document: + QDomDocument document(/*doctype=*/"basketTags"); + QDomElement root = document.createElement("basketTags"); + root.setAttribute("nextStateUid", nextStateUid); + document.appendChild(root); + + // Save all tags: + for (List::iterator it = list.begin(); it != list.end(); ++it) { + Tag *tag = *it; + // Create tag node: + QDomElement tagNode = document.createElement("tag"); + root.appendChild(tagNode); + // Save tag properties: + XMLWork::addElement( document, tagNode, "name", tag->name() ); + XMLWork::addElement( document, tagNode, "shortcut", tag->shortcut().toStringInternal() ); + XMLWork::addElement( document, tagNode, "inherited", XMLWork::trueOrFalse(tag->inheritedBySiblings()) ); + // Save all states: + for (State::List::iterator it2 = (*it)->states().begin(); it2 != (*it)->states().end(); ++it2) { + State *state = *it2; + // Create state node: + QDomElement stateNode = document.createElement("state"); + tagNode.appendChild(stateNode); + // Save state properties: + stateNode.setAttribute("id", state->id()); + XMLWork::addElement( document, stateNode, "name", state->name() ); + XMLWork::addElement( document, stateNode, "emblem", state->emblem() ); + QDomElement textNode = document.createElement("text"); + stateNode.appendChild(textNode); + QString textColor = (state->textColor().isValid() ? state->textColor().name() : ""); + textNode.setAttribute( "bold", XMLWork::trueOrFalse(state->bold()) ); + textNode.setAttribute( "italic", XMLWork::trueOrFalse(state->italic()) ); + textNode.setAttribute( "underline", XMLWork::trueOrFalse(state->underline()) ); + textNode.setAttribute( "strikeOut", XMLWork::trueOrFalse(state->strikeOut()) ); + textNode.setAttribute( "color", textColor ); + QDomElement fontNode = document.createElement("font"); + stateNode.appendChild(fontNode); + fontNode.setAttribute( "name", state->fontName() ); + fontNode.setAttribute( "size", state->fontSize() ); + QString backgroundColor = (state->backgroundColor().isValid() ? state->backgroundColor().name() : ""); + XMLWork::addElement( document, stateNode, "backgroundColor", backgroundColor ); + QDomElement textEquivalentNode = document.createElement("textEquivalent"); + stateNode.appendChild(textEquivalentNode); + textEquivalentNode.setAttribute( "string", state->textEquivalent() ); + textEquivalentNode.setAttribute( "onAllTextLines", XMLWork::trueOrFalse(state->onAllTextLines()) ); + } + } + + // Write to Disk: + if (!Basket::safelySaveToFile(fullPath, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + document.toString())) + DEBUG_WIN << "<font color=red>FAILED to save tags</font>!"; +} + +void Tag::copyTo(Tag *other) +{ + other->m_name = m_name; + other->m_action->setShortcut(m_action->shortcut()); + other->m_inheritedBySiblings = m_inheritedBySiblings; +} + +void Tag::createDefaultTagsSet(const QString &fullPath) +{ + QString xml = QString( + "<!DOCTYPE basketTags>\n" + "<basketTags>\n" + " <tag>\n" + " <name>%1</name>\n" // "To Do" + " <shortcut>Ctrl+1</shortcut>\n" + " <inherited>true</inherited>\n" + " <state id=\"todo_unchecked\">\n" + " <name>%2</name>\n" // "Unchecked" + " <emblem>tag_checkbox</emblem>\n" + " <text bold=\"false\" italic=\"false\" underline=\"false\" strikeOut=\"false\" color=\"\" />\n" + " <font name=\"\" size=\"\" />\n" + " <backgroundColor></backgroundColor>\n" + " <textEquivalent string=\"[ ]\" onAllTextLines=\"false\" />\n" + " </state>\n" + " <state id=\"todo_done\">\n" + " <name>%3</name>\n" // "Done" + " <emblem>tag_checkbox_checked</emblem>\n" + " <text bold=\"false\" italic=\"false\" underline=\"false\" strikeOut=\"true\" color=\"\" />\n" + " <font name=\"\" size=\"\" />\n" + " <backgroundColor></backgroundColor>\n" + " <textEquivalent string=\"[x]\" onAllTextLines=\"false\" />\n" + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <name>%4</name>\n" // "Progress" + " <shortcut>Ctrl+2</shortcut>\n" + " <inherited>true</inherited>\n" + " <state id=\"progress_000\">\n" + " <name>%5</name>\n" // "0 %" + " <emblem>tag_progress_000</emblem>\n" + " <textEquivalent string=\"[ ]\" />\n" + " </state>\n" + " <state id=\"progress_025\">\n" + " <name>%6</name>\n" // "25 %" + " <emblem>tag_progress_025</emblem>\n" + " <textEquivalent string=\"[= ]\" />\n" + " </state>\n" + " <state id=\"progress_050\">\n" + " <name>%7</name>\n" // "50 %" + " <emblem>tag_progress_050</emblem>\n" + " <textEquivalent string=\"[== ]\" />\n" + " </state>\n" + " <state id=\"progress_075\">\n" + " <name>%8</name>\n" // "75 %" + " <emblem>tag_progress_075</emblem>\n" + " <textEquivalent string=\"[=== ]\" />\n" + " </state>\n" + " <state id=\"progress_100\">\n" + " <name>%9</name>\n" // "100 %" + " <emblem>tag_progress_100</emblem>\n" + " <textEquivalent string=\"[====]\" />\n" + " </state>\n" + " </tag>\n" + "\n") + .arg( i18n("To Do"), i18n("Unchecked"), i18n("Done") ) // %1 %2 %3 + .arg( i18n("Progress"), i18n("0 %"), i18n("25 %") ) // %4 %5 %6 + .arg( i18n("50 %"), i18n("75 %"), i18n("100 %") ) // %7 %8 %9 + + QString( + " <tag>\n" + " <name>%1</name>\n" // "Priority" + " <shortcut>Ctrl+3</shortcut>\n" + " <inherited>true</inherited>\n" + " <state id=\"priority_low\">\n" + " <name>%2</name>\n" // "Low" + " <emblem>tag_priority_low</emblem>\n" + " <textEquivalent string=\"{1}\" />\n" + " </state>\n" + " <state id=\"priority_medium\">\n" + " <name>%3</name>\n" // "Medium + " <emblem>tag_priority_medium</emblem>\n" + " <textEquivalent string=\"{2}\" />\n" + " </state>\n" + " <state id=\"priority_high\">\n" + " <name>%4</name>\n" // "High" + " <emblem>tag_priority_high</emblem>\n" + " <textEquivalent string=\"{3}\" />\n" + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <name>%5</name>\n" // "Preference" + " <shortcut>Ctrl+4</shortcut>\n" + " <inherited>true</inherited>\n" + " <state id=\"preference_bad\">\n" + " <name>%6</name>\n" // "Bad" + " <emblem>tag_preference_bad</emblem>\n" + " <textEquivalent string=\"(* )\" />\n" + " </state>\n" + " <state id=\"preference_good\">\n" + " <name>%7</name>\n" // "Good" + " <emblem>tag_preference_good</emblem>\n" + " <textEquivalent string=\"(** )\" />\n" + " </state>\n" + " <state id=\"preference_excelent\">\n" + " <name>%8</name>\n" // "Excellent" + " <emblem>tag_preference_excelent</emblem>\n" // "excelent": typo error, but we should keep compatibility with old versions. + " <textEquivalent string=\"(***)\" />\n" + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <name>%9</name>\n" // "Highlight" + " <shortcut>Ctrl+5</shortcut>\n" + " <state id=\"highlight\">\n" + " <backgroundColor>#ffffcc</backgroundColor>\n" + " <textEquivalent string=\"=>\" />\n" + " </state>\n" + " </tag>\n" + "\n") + .arg( i18n("Priority"), i18n("Low"), i18n("Medium") ) // %1 %2 %3 + .arg( i18n("High"), i18n("Preference"), i18n("Bad") ) // %4 %5 %6 + .arg( i18n("Good"), i18n("Excellent"), i18n("Highlight") ) // %7 %8 %9 + + QString( + " <tag>\n" + " <name>%1</name>\n" // "Important" + " <shortcut>Ctrl+6</shortcut>\n" + " <state id=\"important\">\n" + " <emblem>tag_important</emblem>\n" + " <backgroundColor>#ffcccc</backgroundColor>\n" + " <textEquivalent string=\"!!\" />\n" + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <name>%2</name>\n" // "Very Important" + " <shortcut>Ctrl+7</shortcut>\n" + " <state id=\"very_important\">\n" + " <emblem>tag_important</emblem>\n" + " <text color=\"#ffffff\" />\n" + " <backgroundColor>#ff0000</backgroundColor>\n" + " <textEquivalent string=\"/!\\\" />\n" + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <name>%3</name>\n" // "Information" + " <shortcut>Ctrl+8</shortcut>\n" + " <state id=\"information\">\n" + " <emblem>messagebox_info</emblem>\n" + " <textEquivalent string=\"(i)\" />\n" + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <name>%4</name>\n" // "Idea" + " <shortcut>Ctrl+9</shortcut>\n" + " <state id=\"idea\">\n" + " <emblem>ktip</emblem>\n" + " <textEquivalent string=\"%5\" />\n" // I. + " </state>\n" + " </tag>""\n" + "\n" + " <tag>\n" + " <name>%6</name>\n" // "Title" + " <shortcut>Ctrl+0</shortcut>\n" + " <state id=\"title\">\n" + " <text bold=\"true\" />\n" + " <textEquivalent string=\"##\" />\n" + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <name>%7</name>\n" // "Code" + " <state id=\"code\">\n" + " <font name=\"monospace\" />\n" + " <textEquivalent string=\"|\" onAllTextLines=\"true\" />\n" + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <state id=\"work\">\n" + " <name>%8</name>\n" // "Work" + " <text color=\"#ff8000\" />\n" + " <textEquivalent string=\"%9\" />\n" // W. + " </state>\n" + " </tag>""\n" + "\n") + .arg( i18n("Important"), i18n("Very Important"), i18n("Information") ) // %1 %2 %3 + .arg( i18n("Idea"), i18n("The initial of 'Idea'", "I."), i18n("Title") ) // %4 %5 %6 + .arg( i18n("Code"), i18n("Work"), i18n("The initial of 'Work'", "W.") ) // %7 %8 %9 + + QString( + " <tag>\n" + " <state id=\"personal\">\n" + " <name>%1</name>\n" // "Personal" + " <text color=\"#008000\" />\n" + " <textEquivalent string=\"%2\" />\n" // P. + " </state>\n" + " </tag>\n" + "\n" + " <tag>\n" + " <state id=\"funny\">\n" + " <name>%3</name>\n" // "Funny" + " <emblem>tag_fun</emblem>\n" + " </state>\n" + " </tag>\n" + "</basketTags>\n" + "") + .arg( i18n("Personal"), i18n("The initial of 'Personal'", "P."), i18n("Funny") ); // %1 %2 %3 + + // Write to Disk: + QFile file(fullPath); + if (file.open(IO_WriteOnly)) { + QTextStream stream(&file); + stream.setEncoding(QTextStream::UnicodeUTF8); + stream << "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"; + stream << xml; + file.close(); + } else + DEBUG_WIN << "<font color=red>FAILED to create the tags file</font>!"; +} + +#include <kapplication.h> +#include <qrect.h> +#include <qstyle.h> +#include <qcheckbox.h> +#include <qbitmap.h> +#include <kglobalsettings.h> +#include <qimage.h> +#include <qradiobutton.h> +#include <kiconeffect.h> + +/** class IndentedMenuItem: */ + +IndentedMenuItem::IndentedMenuItem(const QString &text, const QString &icon, const QString &shortcut) + : m_text(text), m_icon(icon), m_shortcut(shortcut) +{ +} + +IndentedMenuItem::~IndentedMenuItem() +{ +} + +void IndentedMenuItem::paint(QPainter *painter, const QColorGroup &cg, bool active, bool enabled, int x, int y, int w, int h) +{ + QPen pen = painter->pen(); + QFont font = painter->font(); + + int iconSize = KIcon::SizeSmall; + int iconMargin = StateMenuItem::iconMargin(); + + /* When an item is disabled, it often have a 3D sunken look. + * This is done by calling this paint routine two times, with different pen color and offset. + * A disabled item is first painted in the rect (x+1, y+1, w, h) and with pen of cg.light() color, + * It is then drawn a second time in the rect (x, y, w, h). + * But we don't want to draw the icon two times! So, we try to detect if we are in the "etched-text draw" state and then don't draw the icon. + * This doesn't work for every styles but it's already better than nothing (styles when it doesn't work are seldomly used, if used). + */ + bool drawingEtchedText = !enabled && !active && painter->pen().color() != cg.mid()/*== cg.foreground()*/; + if (drawingEtchedText) { + QString styleName = kapp->style().name(); + if (styleName == "plastik" || styleName == "lipstik") + painter->setPen(cg.light()); + drawingEtchedText = !enabled && !active && painter->pen().color() != cg.foreground(); + } else + drawingEtchedText = !enabled && !active && painter->pen().color() == cg.light(); + if (!m_icon.isEmpty() && !drawingEtchedText) { + QPixmap icon = kapp->iconLoader()->loadIcon(m_icon, KIcon::Small, iconSize, + (enabled ? (active ? KIcon::ActiveState : KIcon::DefaultState) : KIcon::DisabledState), + /*path_store=*/0L, /*canReturnNull=*/true); + painter->drawPixmap(x, y + (h-iconSize)/2, icon); + } + /* Pen and font are already set to the good ones, so we can directly draw the text. + * BUT, for the half of styles provided with KDE, the pen is not set for the Active state (when hovered by mouse of selected by keyboard). + * So, I set the pen myself. + * But it's certainly a bug in those styles because some other styles eg. just draw a 3D sunken rect when an item is selected + * and keep the background to white, drawing a white text over it is... very bad. But I can't see what can be done. + */ + if (active && enabled) + painter->setPen(KGlobalSettings::highlightedTextColor()); + painter->drawText(x + iconSize + iconMargin, y, w - iconSize - iconMargin, h, AlignLeft | AlignVCenter | DontClip | ShowPrefix, m_text/*painter->pen().color().name()*/); + + if (!m_shortcut.isEmpty()) { + painter->setPen(pen); + if (active && enabled) + painter->setPen(KGlobalSettings::highlightedTextColor()); + painter->setFont(font); + painter->setClipping(false); + painter->drawText(x + 5 + w, y, 3000, h, AlignLeft | AlignVCenter | DontClip | ShowPrefix, m_shortcut); + } +} + +QSize IndentedMenuItem::sizeHint() +{ + int iconSize = KIcon::SizeSmall; + int iconMargin = StateMenuItem::iconMargin(); + QSize textSize = QFontMetrics(KGlobalSettings::menuFont()).size( AlignLeft | AlignVCenter | ShowPrefix | DontClip, m_text ); + return QSize(iconSize + iconMargin + textSize.width(), textSize.height()); +} + +/** class StateMenuItem: */ + +StateMenuItem::StateMenuItem(State *state, const QString &shortcut, bool withTagName) + : m_state(state), m_shortcut(shortcut) +{ + m_name = (withTagName && m_state->parentTag() ? m_state->parentTag()->name() : m_state->name()); +} + +StateMenuItem::~StateMenuItem() +{ +} + +void StateMenuItem::paint(QPainter *painter, const QColorGroup &cg, bool active, bool enabled, int x, int y, int w, int h) +{ + QPen pen = painter->pen(); + QFont font = painter->font(); + + int iconSize = 16; // We use 16 instead of KIcon::SizeSmall (the size of icons in menus) because tags will always be 16*16 icons + + if (!active && m_state->backgroundColor().isValid()) + painter->fillRect(x/*-1*/, y/*-1*/, w/*+2*/, h/*+2*/, m_state->backgroundColor()); + /* When an item is disabled, it often have a 3D sunken look. + * This is done by calling this paint routine two times, with different pen color and offset. + * A disabled item is first painted in the rect (x+1, y+1, w, h) and with pen of cg.light() color, + * It is then drawn a second time in the rect (x, y, w, h). + * But we don't want to draw the icon two times! So, we try to detect if we are in the "etched-text draw" state and then don't draw the icon. + * This doesn't work for every styles but it's already better than nothing (styles when it doesn't work are seldomly used, if used). + */ + bool drawingEtchedText = !enabled && !active && painter->pen().color() != cg.mid()/*== cg.foreground()*/; + if (drawingEtchedText) { + QString styleName = kapp->style().name(); + if (styleName == "plastik" || styleName == "lipstik") + painter->setPen(cg.light()); + drawingEtchedText = !enabled && !active && painter->pen().color() != cg.foreground(); + } else + drawingEtchedText = !enabled && !active && painter->pen().color() == cg.light(); + if (!m_state->emblem().isEmpty() && !drawingEtchedText) { + QPixmap icon = kapp->iconLoader()->loadIcon(m_state->emblem(), KIcon::Small, iconSize, + (enabled ? (active ? KIcon::ActiveState : KIcon::DefaultState) : KIcon::DisabledState), + /*path_store=*/0L, /*canReturnNull=*/true); + painter->drawPixmap(x, y + (h-iconSize)/2, icon); + } + if (enabled && !active && m_state->textColor().isValid()) + painter->setPen(m_state->textColor()); + /* Pen and font are already set to the good ones, so we can directly draw the text. + * BUT, for the half of styles provided with KDE, the pen is not set for the Active state (when hovered by mouse of selected by keyboard). + * So, I set the pen myself. + * But it's certainly a bug in those styles because some other styles eg. just draw a 3D sunken rect when an item is selected + * and keep the background to white, drawing a white text over it is... very bad. But I can't see what can be done. + */ + if (active && enabled) + painter->setPen(KGlobalSettings::highlightedTextColor()); + painter->setFont( m_state->font(painter->font()) ); + painter->drawText(x + iconSize + iconMargin(), y, w - iconSize - iconMargin(), h, AlignLeft | AlignVCenter | DontClip | ShowPrefix, m_name); + + if (!m_shortcut.isEmpty()) { + painter->setPen(pen); + if (active && enabled) + painter->setPen(KGlobalSettings::highlightedTextColor()); + painter->setFont(font); + painter->setClipping(false); + painter->drawText(x + 5 + w, y, 3000, h, AlignLeft | AlignVCenter | DontClip | ShowPrefix, m_shortcut); + } +} + +QSize StateMenuItem::sizeHint() +{ + int iconSize = 16; // We use 16 instead of KIcon::SizeSmall (the size of icons in menus) because tags will always be 16*16 icons + QFont theFont = m_state->font(KGlobalSettings::menuFont()); + QSize textSize = QFontMetrics(theFont).size( AlignLeft | AlignVCenter | ShowPrefix | DontClip, m_name ); + return QSize(iconSize + iconMargin() + textSize.width(), textSize.height()); +} + +QIconSet StateMenuItem::checkBoxIconSet(bool checked, QColorGroup cg) +{ + int width = kapp->style().pixelMetric(QStyle::PM_IndicatorWidth, 0); + int height = kapp->style().pixelMetric(QStyle::PM_IndicatorHeight, 0); + QRect rect(0, 0, width, height); + + QColor menuBackgroundColor = (dynamic_cast<KStyle*>(&(kapp->style())) == NULL ? cg.background() : cg.background().light(103)); + + // Enabled, Not hovering + QPixmap pixmap(width, height); + pixmap.fill(menuBackgroundColor); // In case the pixelMetric() haven't returned a bigger rectangle than what drawPrimitive() draws + QPainter painter(&pixmap); + int style = QStyle::Style_Enabled | QStyle::Style_Active | (checked ? QStyle::Style_On : QStyle::Style_Off); + QColor background = cg.color(QColorGroup::Background); + kapp->style().drawPrimitive(QStyle::PE_Indicator, &painter, rect, cg, style); + painter.end(); + + // Enabled, Hovering + QPixmap pixmapHover(width, height); + pixmapHover.fill(menuBackgroundColor); // In case the pixelMetric() haven't returned a bigger rectangle than what drawPrimitive() draws + painter.begin(&pixmapHover); + style |= QStyle::Style_MouseOver; + cg.setColor(QColorGroup::Background, KGlobalSettings::highlightColor()); + kapp->style().drawPrimitive(QStyle::PE_Indicator, &painter, rect, cg, style); + painter.end(); + + // Disabled + QPixmap pixmapDisabled(width, height); + pixmapDisabled.fill(menuBackgroundColor); // In case the pixelMetric() haven't returned a bigger rectangle than what drawPrimitive() draws + painter.begin(&pixmapDisabled); + style = /*QStyle::Style_Enabled | */QStyle::Style_Active | (checked ? QStyle::Style_On : QStyle::Style_Off); + cg.setColor(QColorGroup::Background, background); + kapp->style().drawPrimitive(QStyle::PE_Indicator, &painter, rect, cg, style); + painter.end(); + + QIconSet iconSet(pixmap); + iconSet.setPixmap(pixmapHover, QIconSet::Automatic, QIconSet::Active); + iconSet.setPixmap(pixmapDisabled, QIconSet::Automatic, QIconSet::Disabled); + return iconSet; +} + +QIconSet StateMenuItem::radioButtonIconSet(bool checked, QColorGroup cg) +{ + int width = kapp->style().pixelMetric(QStyle::PM_ExclusiveIndicatorWidth, 0); + int height = kapp->style().pixelMetric(QStyle::PM_ExclusiveIndicatorHeight, 0); + QRect rect(0, 0, width, height); + + int style = QStyle::Style_Default | QStyle::Style_Enabled | (checked ? QStyle::Style_On : QStyle::Style_Off); + + QPixmap pixmap(width, height); + pixmap.fill(Qt::red); + QPainter painter(&pixmap); + /* We can't use that line of code (like for checkboxes): + * //kapp->style().drawPrimitive(QStyle::PE_ExclusiveIndicator, &painter, rect, cg, style); + * because Plastik (and derived styles) don't care of the QStyle::Style_On flag and will ALWAYS draw an unchecked radiobutton. + * So, we use another method: + */ + QRadioButton rb(0); + rb.setChecked(checked); + kapp->style().drawControl(QStyle::CE_RadioButton, &painter, &rb, rect, cg, style); + painter.end(); + /* Some styles like Plastik (and derived ones) have QStyle::PE_ExclusiveIndicator drawing a radiobutton disc, as wanted, + * and leave pixels ouside it untouched, BUT QStyle::PE_ExclusiveIndicatorMask is a fully black square. + * So, we can't apply the mask to make the radiobutton circle transparent outside. + * We're using an hack by filling the pixmap in Qt::red, drawing the radiobutton and then creating an heuristic mask. + * The heuristic mask is created using the 4 edge pixels (that are red) and by making transparent every pixels that are of this color: + */ + pixmap.setMask(pixmap.createHeuristicMask()); + + QPixmap pixmapHover(width, height); + pixmapHover.fill(Qt::red); + painter.begin(&pixmapHover); + //kapp->style().drawPrimitive(QStyle::PE_ExclusiveIndicator, &painter, rect, cg, style); + style |= QStyle::Style_MouseOver; + cg.setColor(QColorGroup::Background, KGlobalSettings::highlightColor()); + kapp->style().drawControl(QStyle::CE_RadioButton, &painter, &rb, rect, cg, style); + painter.end(); + pixmapHover.setMask(pixmapHover.createHeuristicMask()); + + QIconSet iconSet(pixmap); + iconSet.setPixmap(pixmapHover, QIconSet::Automatic, QIconSet::Active); + return iconSet; +} diff --git a/src/tag.h b/src/tag.h new file mode 100644 index 0000000..687e9d5 --- /dev/null +++ b/src/tag.h @@ -0,0 +1,198 @@ +/*************************************************************************** + * Copyright (C) 2005 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef TAG_H +#define TAG_H + +#include <qstring.h> +#include <qcolor.h> +#include <qfont.h> +#include <qvaluelist.h> + +#include <kaction.h> +#include <kshortcut.h> + +class QPainter; + +class Tag; + +/** + * @author S�astien Laot + */ +class State +{ + public: + /// LIST OF STATES: + typedef QValueList<State*> List; + + public: + /// CONSTRUCTOR AND DESTRUCTOR: + State(const QString &id = QString(), Tag *tag = 0); + ~State(); + /// SET PROPERTIES: + void setId(const QString &id) { m_id = id; } + void setName(const QString &name) { m_name = name; } + void setEmblem(const QString &emblem) { m_emblem = emblem; } + void setBold(bool bold) { m_bold = bold; } + void setItalic(bool italic) { m_italic = italic; } + void setUnderline(bool underline) { m_underline = underline; } + void setStrikeOut(bool strikeOut) { m_strikeOut = strikeOut; } + void setTextColor(const QColor &color) { m_textColor = color; } + void setFontName(const QString &font) { m_fontName = font; } + void setFontSize(int size) { m_fontSize = size; } + void setBackgroundColor(const QColor &color) { m_backgroundColor = color; } + void setTextEquivalent(const QString &text) { m_textEquivalent = text; } + void setOnAllTextLines(bool yes) { m_onAllTextLines = yes; } + void setParentTag(Tag *tag) { m_parentTag = tag; } + /// GET PROPERTIES: + QString id() const { return m_id; } + QString name() const { return m_name; } + QString emblem() const { return m_emblem; } + bool bold() const { return m_bold; } + bool italic() const { return m_italic; } + bool underline() const { return m_underline; } + bool strikeOut() const { return m_strikeOut; } + QColor textColor() const { return m_textColor; } + QString fontName() const { return m_fontName; } + int fontSize() const { return m_fontSize; } + QColor backgroundColor() const { return m_backgroundColor; } + QString textEquivalent() const { return m_textEquivalent; } + bool onAllTextLines() const { return m_onAllTextLines; } + Tag* parentTag() const { return m_parentTag; } + /// HELPING FUNCTIONS: + State *nextState(bool cycle = true); + QString fullName(); + QFont font(QFont base); + QString toCSS(const QString &gradientFolderPath, const QString &gradientFolderName, const QFont &baseFont); + static void merge(const List &states, State *result, int *emblemsCount, bool *haveInvisibleTags, const QColor &backgroundColor); + void copyTo(State *other); + private: + /// PROPERTIES: + QString m_id; + QString m_name; + QString m_emblem; + bool m_bold; + bool m_italic; + bool m_underline; + bool m_strikeOut; + QColor m_textColor; + QString m_fontName; + int m_fontSize; + QColor m_backgroundColor; + QString m_textEquivalent; + bool m_onAllTextLines; + Tag *m_parentTag; +}; + +/** A Tag is a category of Notes. + * A Note can have 0, 1 or more Tags. + * A Tag can have a unique State or several States. + * @author S�astien Laot + */ +class Tag +{ + public: + /// LIST OF ALL TAGS IN THE APPLICATION: + typedef QValueList<Tag*> List; + static Tag::List all; + static State* stateForId(const QString &id); + static Tag* tagForKAction(KAction *action); + static Tag* tagSimilarTo(Tag *tagToTest); + static QMap<QString, QString> loadTags(const QString &path = QString()/*, bool merge = false*/); /// << Load the tags contained in the XML file @p path or those in the application settings if @p path isEmpty(). If @p merge is true and a tag with the id of a tag that should be loaded already exist, the tag will get a new id. Otherwise, the tag will be dismissed. + static void saveTags(); + static void saveTagsTo(QValueList<Tag*> &list, const QString &fullPath); + static void createDefaultTagsSet(const QString &file); + static long getNextStateUid(); + private: + static long nextStateUid; + + public: + /// CONSTRUCTOR AND DESTRUCTOR: + Tag(/*State *firstState, const QString &name, bool inheritedBySiblings*/); + ~Tag(); + /// SET PROPERTIES: + void setName(const QString &name); + void setShortcut(const KShortcut &shortcut) { m_action->setShortcut(shortcut); } + void setInheritedBySiblings(bool inherited) { m_inheritedBySiblings = inherited; } + void appendState(State *state) { m_states.append(state); state->setParentTag(this); } + void removeState(State *state) { m_states.remove(state); state->setParentTag(0); } + /// GET PROPERTIES: + QString name() const { return m_name; } + KShortcut shortcut() const { return m_action->shortcut(); } + bool inheritedBySiblings() const { return m_inheritedBySiblings; } + State::List& states() const { return (State::List&)m_states; } + int countStates() const { return m_states.count(); } + void copyTo(Tag *other); + private: + /// PROPERTIES: + QString m_name; + KAction *m_action; + bool m_inheritedBySiblings; + State::List m_states; +}; + +#include <qiconset.h> +#include <qmenudata.h> +#include <qstring.h> + +class QPainter; + +/** A menu item to indent icon and text (to keep place for a checkbox or a radiobutton on left). + * You should not set any icon when adding this entry to the menu. + * Instead, the constructor take the icon and the item take care to draw it itself. + * Better suited to be used with StateMenuItem (or TagMenuItem). + * @author S�astien Laot + */ +class IndentedMenuItem : public QCustomMenuItem +{ + public: + IndentedMenuItem(const QString &text, const QString &icon = "", const QString &shortcut = ""); + ~IndentedMenuItem(); + void paint(QPainter *painter, const QColorGroup &cg, bool active, bool enabled, int x, int y, int w, int h); + QSize sizeHint(); + bool fullSpan() { return true; } + private: + QString m_text; + QString m_icon; + QString m_shortcut; +}; + +/** A menu item representing a State or a Tag. + * @author S�astien Laot + */ +class StateMenuItem : public QCustomMenuItem +{ + public: + StateMenuItem(State *state, const QString &shortcut, bool withTagName = false); + ~StateMenuItem(); + void paint(QPainter *painter, const QColorGroup &cg, bool active, bool enabled, int x, int y, int w, int h); + QSize sizeHint(); + bool fullSpan() { return true; } + private: + State *m_state; + QString m_name; + QString m_shortcut; + public: + static QIconSet checkBoxIconSet(bool checked, QColorGroup cg); + static QIconSet radioButtonIconSet(bool checked, QColorGroup cg); + static int iconMargin() { return 5; } +}; + +#endif // TAG_H diff --git a/src/tagsedit.cpp b/src/tagsedit.cpp new file mode 100644 index 0000000..c327758 --- /dev/null +++ b/src/tagsedit.cpp @@ -0,0 +1,1206 @@ +/*************************************************************************** + * Copyright (C) 2005 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qtooltip.h> +#include <qlabel.h> +#include <qlineedit.h> +#include <kfontcombo.h> +#include <qlayout.h> +#include <kkeybutton.h> +#include <kicondialog.h> +#include <kiconloader.h> +#include <kapplication.h> +#include <kglobalsettings.h> +#include <qcheckbox.h> +#include <kpushbutton.h> +#include <qgroupbox.h> +#include <qheader.h> +#include <qvaluelist.h> +#include <klocale.h> +#include <kstandarddirs.h> +#include <kseparator.h> +#include <kstringhandler.h> +#include <qpainter.h> +#include <qaction.h> +#include <kmessagebox.h> +#include <qtimer.h> + +#include <iostream> + +#include "tag.h" +#include "tagsedit.h" +#include "kcolorcombo2.h" +#include "variouswidgets.h" +#include "global.h" +#include "bnpview.h" + +/** class StateCopy: */ + +StateCopy::StateCopy(State *old/* = 0*/) +{ + oldState = old; + newState = new State(); + if (oldState) + oldState->copyTo(newState); +} + +StateCopy::~StateCopy() +{ +} + +void StateCopy::copyBack() +{ +} + +/** class TagCopy: */ + +TagCopy::TagCopy(Tag *old/* = 0*/) +{ + oldTag = old; + newTag = new Tag(); + if (oldTag) + oldTag->copyTo(newTag); + + if (old) + for (State::List::iterator it = old->states().begin(); it != old->states().end(); ++it) + stateCopies.append(new StateCopy(*it)); + else + stateCopies.append(new StateCopy()); +} + +TagCopy::~TagCopy() +{ +} + +void TagCopy::copyBack() +{ +} + +bool TagCopy::isMultiState() +{ + return (stateCopies.count() > 1); +} + +/** class TagListViewItem: */ + +TagListViewItem::TagListViewItem(QListView *parent, TagCopy *tagCopy) + : QListViewItem(parent), m_tagCopy(tagCopy), m_stateCopy(0) +{ + setText(0, tagCopy->newTag->name()); +} + +TagListViewItem::TagListViewItem(QListViewItem *parent, TagCopy *tagCopy) + : QListViewItem(parent), m_tagCopy(tagCopy), m_stateCopy(0) +{ + setText(0, tagCopy->newTag->name()); +} + +TagListViewItem::TagListViewItem(QListView *parent, QListViewItem *after, TagCopy *tagCopy) + : QListViewItem(parent, after), m_tagCopy(tagCopy), m_stateCopy(0) +{ + setText(0, tagCopy->newTag->name()); +} + +TagListViewItem::TagListViewItem(QListViewItem *parent, QListViewItem *after, TagCopy *tagCopy) + : QListViewItem(parent, after), m_tagCopy(tagCopy), m_stateCopy(0) +{ + setText(0, tagCopy->newTag->name()); +} + +/* */ + +TagListViewItem::TagListViewItem(QListView *parent, StateCopy *stateCopy) + : QListViewItem(parent), m_tagCopy(0), m_stateCopy(stateCopy) +{ + setText(0, stateCopy->newState->name()); +} + +TagListViewItem::TagListViewItem(QListViewItem *parent, StateCopy *stateCopy) + : QListViewItem(parent), m_tagCopy(0), m_stateCopy(stateCopy) +{ + setText(0, stateCopy->newState->name()); +} + +TagListViewItem::TagListViewItem(QListView *parent, QListViewItem *after, StateCopy *stateCopy) + : QListViewItem(parent, after), m_tagCopy(0), m_stateCopy(stateCopy) +{ + setText(0, stateCopy->newState->name()); +} + +TagListViewItem::TagListViewItem(QListViewItem *parent, QListViewItem *after, StateCopy *stateCopy) + : QListViewItem(parent, after), m_tagCopy(0), m_stateCopy(stateCopy) +{ + setText(0, stateCopy->newState->name()); +} + +/* */ + +TagListViewItem::~TagListViewItem() +{ +} + +TagListViewItem* TagListViewItem::lastChild() +{ + TagListViewItem *child = (TagListViewItem*)firstChild(); + while (child) { + if (child->nextSibling()) + child = (TagListViewItem*)(child->nextSibling()); + else + return child; + } + return 0; +} + +bool TagListViewItem::isEmblemObligatory() +{ + return m_stateCopy != 0; // It's a state of a multi-state +} + +TagListViewItem* TagListViewItem::prevSibling() +{ + TagListViewItem *item = this; + while (item) { + if (item->nextSibling() == this) + return item; + item = (TagListViewItem*)(item->itemAbove()); + } + return 0; +} + +TagListViewItem* TagListViewItem::parent() const +{ + return (TagListViewItem*) QListViewItem::parent(); +} + +// TODO: TagListViewItem:: +int TAG_ICON_SIZE = 16; +int TAG_MARGIN = 1; + +int TagListViewItem::width(const QFontMetrics &/* fontMetrics */, const QListView */*listView*/, int /* column */) const +{ + return listView()->visibleWidth(); +} + +void TagListViewItem::setup() +{ + QString text = (m_tagCopy ? m_tagCopy->newTag->name() : m_stateCopy->newState->name()); + State *state = (m_tagCopy ? m_tagCopy->stateCopies[0]->newState : m_stateCopy->newState); + + if (m_tagCopy && !m_tagCopy->newTag->shortcut().isNull()) + text = i18n("Tag name (shortcut)", "%1 (%2)").arg(text, m_tagCopy->newTag->shortcut().toString()); + + QFont font = state->font(listView()->font()); + + QRect textRect = QFontMetrics(font).boundingRect(0, 0, /*width=*/1, 500000, Qt::AlignAuto | Qt::AlignTop, text); + + widthChanged(); + int height = TAG_MARGIN + QMAX(TAG_ICON_SIZE, textRect.height()) + TAG_MARGIN; + setHeight(height); + + repaint(); +} + +void TagListViewItem::paintCell(QPainter *painter, const QColorGroup &/*colorGroup*/, int /*column*/, int width, int /*align*/) +{ + bool withIcon = m_stateCopy || (m_tagCopy && !m_tagCopy->isMultiState()); + QString text = (m_tagCopy ? m_tagCopy->newTag->name() : m_stateCopy->newState->name()); + State *state = (m_tagCopy ? m_tagCopy->stateCopies[0]->newState : m_stateCopy->newState); + + if (m_tagCopy && !m_tagCopy->newTag->shortcut().isNull()) + text = i18n("Tag name (shortcut)", "%1 (%2)").arg(text, m_tagCopy->newTag->shortcut().toString()); + + QFont font = (withIcon ? state->font(listView()->font()) : listView()->font()); + + QFontMetrics fontMetrics(font); + QRect textRect = fontMetrics.boundingRect(0, 0, /*width=*/1, 500000, Qt::AlignAuto | Qt::AlignTop, text); + + QPixmap emblem = (withIcon ? kapp->iconLoader()->loadIcon(state->emblem(), KIcon::NoGroup, 16, KIcon::DefaultState, 0L, /*canReturnNull=*/true) : QPixmap()); + + QColor backgroundColor = (isSelected() ? KGlobalSettings::highlightColor() + : (withIcon && state->backgroundColor().isValid() ? state->backgroundColor() + : listView()->paletteBackgroundColor())); + QColor textColor = (isSelected() ? KGlobalSettings::highlightedTextColor() + : (withIcon && state->textColor().isValid() ? state->textColor() + : listView()->paletteForegroundColor())); + + // Bufferize the drawing of items (otherwize, resizing the splitter make the tree act like a Christmas Tree ;-D ): + QPixmap theBuffer(width, height()); + theBuffer.fill(backgroundColor); + QPainter thePainter(&theBuffer); + + if (withIcon) + thePainter.drawPixmap(TAG_MARGIN, (height() - emblem.height()) / 2, emblem); + + int xText = TAG_MARGIN + (withIcon ? TAG_ICON_SIZE + TAG_MARGIN : 0); + thePainter.setPen(textColor); + thePainter.setFont(font); + int textWidth = width - xText; + if (thePainter.fontMetrics().width(text) > textWidth) + text = KStringHandler::rPixelSqueeze(text, fontMetrics, textWidth); + thePainter.drawText(xText, 0, textWidth, height(), Qt::AlignAuto | Qt::AlignVCenter | Qt::ShowPrefix, text); + + // Apply the buffer: + thePainter.end(); + painter->drawPixmap(0, 0, theBuffer); +} + +/** class TagListView: */ + +TagListView::TagListView(QWidget *parent, const char *name, WFlags flags) + : QListView(parent, name, flags) +{ +} + +TagListView::~TagListView() +{ +} + +void TagListView::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Delete) + emit deletePressed(); + else if (event->key() != Qt::Key_Left || (selectedItem() && selectedItem()->parent())) + // Do not allow to open/close first-level items + QListView::keyPressEvent(event); +} + +void TagListView::contentsMouseDoubleClickEvent(QMouseEvent *event) +{ + // Ignore this event! Do not open/close first-level items! + + // But trigger edit (change focus to name) when double-click an item: + if (itemAt(contentsToViewport(event->pos())) != 0) + emit doubleClickedItem(); +} + +void TagListView::contentsMousePressEvent(QMouseEvent *event) +{ + // When clicking on an empty space, QListView would unselect the current item! We forbid that! + if (itemAt(contentsToViewport(event->pos())) != 0) + QListView::contentsMousePressEvent(event); +} + +void TagListView::contentsMouseReleaseEvent(QMouseEvent *event) +{ + // When clicking on an empty space, QListView would unselect the current item! We forbid that! + if (itemAt(contentsToViewport(event->pos())) != 0) + QListView::contentsMouseReleaseEvent(event); +} + +TagListViewItem* TagListView::currentItem() const +{ + return (TagListViewItem*) QListView::currentItem(); +} + +TagListViewItem* TagListView::firstChild() const +{ + return (TagListViewItem*) QListView::firstChild(); +} + +TagListViewItem* TagListView::lastItem() const +{ + return (TagListViewItem*) QListView::lastItem(); +} + +/** class TagsEditDialog: */ + +TagsEditDialog::TagsEditDialog(QWidget *parent, State *stateToEdit, bool addNewTag) + : KDialogBase(KDialogBase::Plain, i18n("Customize Tags"), KDialogBase::Ok | KDialogBase::Cancel, + KDialogBase::Ok, parent, /*name=*/"CustomizeTags", /*modal=*/true, /*separator=*/true), + m_loading(false) +{ + QHBoxLayout *layout = new QHBoxLayout(plainPage(), /*margin=*/0, spacingHint()); + + /* Left part: */ + + QPushButton *newTag = new QPushButton(i18n("Ne&w Tag"), plainPage()); + QPushButton *newState = new QPushButton(i18n("New St&ate"), plainPage()); + + connect( newTag, SIGNAL(clicked()), this, SLOT(newTag()) ); + connect( newState, SIGNAL(clicked()), this, SLOT(newState()) ); + + m_tags = new TagListView(plainPage()); + m_tags->header()->hide(); + m_tags->setRootIsDecorated(false); + m_tags->addColumn(""); + m_tags->setSorting(-1); // Sort column -1, so disabled sorting + m_tags->setResizeMode(QListView::LastColumn); + + m_moveUp = new KPushButton( KGuiItem("", "1uparrow"), plainPage() ); + m_moveDown = new KPushButton( KGuiItem("", "1downarrow"), plainPage() ); + m_deleteTag = new KPushButton( KGuiItem("", "editdelete"), plainPage() ); + + QToolTip::add( m_moveUp, i18n("Move Up (Ctrl+Shift+Up)") ); + QToolTip::add( m_moveDown, i18n("Move Down (Ctrl+Shift+Down)") ); + QToolTip::add( m_deleteTag, i18n("Delete") ); + + connect( m_moveUp, SIGNAL(clicked()), this, SLOT(moveUp()) ); + connect( m_moveDown, SIGNAL(clicked()), this, SLOT(moveDown()) ); + connect( m_deleteTag, SIGNAL(clicked()), this, SLOT(deleteTag()) ); + + QHBoxLayout *topLeftLayout = new QHBoxLayout(0, /*margin=*/0, spacingHint()); + topLeftLayout->addWidget(m_moveUp); + topLeftLayout->addWidget(m_moveDown); + topLeftLayout->addWidget(m_deleteTag); + + QVBoxLayout *leftLayout = new QVBoxLayout(0, /*margin=*/0, spacingHint()); + leftLayout->addWidget(newTag); + leftLayout->addWidget(newState); + leftLayout->addWidget(m_tags); + leftLayout->addLayout(topLeftLayout); + + layout->addLayout(leftLayout); + + /* Right part: */ + + QWidget *rightWidget = new QWidget(plainPage()); + + m_tagBox = new QGroupBox(1, Qt::Horizontal, i18n("Tag"), rightWidget); + QWidget *tagWidget = new QWidget(m_tagBox); + + m_tagName = new QLineEdit(tagWidget); + QLabel *tagNameLabel = new QLabel(m_tagName, i18n("&Name:"), tagWidget); + + m_shortcut = new KKeyButton(tagWidget); + m_removeShortcut = new QPushButton(i18n("Remove tag shortcut", "&Remove"), tagWidget); + QLabel *shortcutLabel = new QLabel(m_shortcut, i18n("S&hortcut:"), tagWidget); + connect( m_shortcut, SIGNAL(capturedShortcut(const KShortcut&)), this, SLOT(capturedShortcut(const KShortcut&)) ); + connect( m_removeShortcut, SIGNAL(clicked()), this, SLOT(removeShortcut()) ); + + m_inherit = new QCheckBox(i18n("&Inherited by new sibling notes"), tagWidget); + + QGridLayout *tagGrid = new QGridLayout(tagWidget, /*rows=*/3, /*cols=*/4, /*border=*/0, /*spacing=*/spacingHint()); + tagGrid->addWidget(tagNameLabel, 0, 0); + tagGrid->addMultiCellWidget(m_tagName, /*fromRow=*/0, /*toRow=*/0, /*fromCol=*/1, /*toCol=*/3); + tagGrid->addWidget(shortcutLabel, 1, 0); + tagGrid->addWidget(m_shortcut, 1, 1); + tagGrid->addWidget(m_removeShortcut, 1, 2); + tagGrid->addMultiCellWidget(m_inherit, /*fromRow=*/2, /*toRow=*/2, /*fromCol=*/0, /*toCol=*/3); + tagGrid->setColStretch(/*col=*/3, /*stretch=*/255); + + m_stateBox = new QGroupBox(1, Qt::Horizontal, i18n("State"), rightWidget); + QWidget *stateWidget = new QWidget(m_stateBox); + + m_stateName = new QLineEdit(stateWidget); + m_stateNameLabel = new QLabel(m_stateName, i18n("Na&me:"), stateWidget); + + QWidget *emblemWidget = new QWidget(stateWidget); + m_emblem = new KIconButton(emblemWidget); + m_emblem->setIconType(KIcon::NoGroup, KIcon::Action); + m_emblem->setIconSize(16); + m_emblem->setIcon("editdelete"); + m_removeEmblem = new QPushButton(i18n("Remove tag emblem", "Remo&ve"), emblemWidget); + QLabel *emblemLabel = new QLabel(m_emblem, i18n("&Emblem:"), stateWidget); + connect( m_removeEmblem, SIGNAL(clicked()), this, SLOT(removeEmblem()) ); // m_emblem.resetIcon() is not a slot! + + // Make the icon button and the remove button the same height: + int height = QMAX(m_emblem->sizeHint().width(), m_emblem->sizeHint().height()); + height = QMAX(height, m_removeEmblem->sizeHint().height()); + m_emblem->setFixedSize(height, height); // Make it square + m_removeEmblem->setFixedHeight(height); + m_emblem->resetIcon(); + + QHBoxLayout *emblemLayout = new QHBoxLayout(emblemWidget, /*margin=*/0, spacingHint()); + emblemLayout->addWidget(m_emblem); + emblemLayout->addWidget(m_removeEmblem); + emblemLayout->addStretch(); + + m_backgroundColor = new KColorCombo2(QColor(), KGlobalSettings::baseColor(), stateWidget); + QLabel *backgroundColorLabel = new QLabel(m_backgroundColor, i18n("&Background:"), stateWidget); + + QHBoxLayout *backgroundColorLayout = new QHBoxLayout(0, /*margin=*/0, spacingHint()); + backgroundColorLayout->addWidget(m_backgroundColor); + backgroundColorLayout->addStretch(); + + QIconSet boldIconSet = kapp->iconLoader()->loadIconSet("text_bold", KIcon::Small); + m_bold = new QPushButton(boldIconSet, "", stateWidget); + m_bold->setToggleButton(true); + int size = QMAX(m_bold->sizeHint().width(), m_bold->sizeHint().height()); + m_bold->setFixedSize(size, size); // Make it square! + QToolTip::add(m_bold, i18n("Bold")); + + QIconSet underlineIconSet = kapp->iconLoader()->loadIconSet("text_under", KIcon::Small); + m_underline = new QPushButton(underlineIconSet, "", stateWidget); + m_underline->setToggleButton(true); + m_underline->setFixedSize(size, size); // Make it square! + QToolTip::add(m_underline, i18n("Underline")); + + QIconSet italicIconSet = kapp->iconLoader()->loadIconSet("text_italic", KIcon::Small); + m_italic = new QPushButton(italicIconSet, "", stateWidget); + m_italic->setToggleButton(true); + m_italic->setFixedSize(size, size); // Make it square! + QToolTip::add(m_italic, i18n("Italic")); + + QIconSet strikeIconSet = kapp->iconLoader()->loadIconSet("text_strike", KIcon::Small); + m_strike = new QPushButton(strikeIconSet, "", stateWidget); + m_strike->setToggleButton(true); + m_strike->setFixedSize(size, size); // Make it square! + QToolTip::add(m_strike, i18n("Strike Through")); + + QLabel *textLabel = new QLabel(m_bold, i18n("&Text:"), stateWidget); + + QHBoxLayout *textLayout = new QHBoxLayout(0, /*margin=*/0, spacingHint()); + textLayout->addWidget(m_bold); + textLayout->addWidget(m_underline); + textLayout->addWidget(m_italic); + textLayout->addWidget(m_strike); + textLayout->addStretch(); + + m_textColor = new KColorCombo2(QColor(), KGlobalSettings::textColor(), stateWidget); + QLabel *textColorLabel = new QLabel(m_textColor, i18n("Co&lor:"), stateWidget); + + m_font = new KFontCombo(stateWidget); + m_font->insertItem(i18n("(Default)"), 0); + QLabel *fontLabel = new QLabel(m_font, i18n("&Font:"), stateWidget); + + m_fontSize = new FontSizeCombo(/*rw=*/true, /*withDefault=*/true, stateWidget); + QLabel *fontSizeLabel = new QLabel(m_fontSize, i18n("&Size:"), stateWidget); + + m_textEquivalent = new QLineEdit(stateWidget); + QLabel *textEquivalentLabel = new QLabel(m_textEquivalent, i18n("Te&xt equivalent:"), stateWidget); + QFont font = m_textEquivalent->font(); + font.setFamily("monospace"); + m_textEquivalent->setFont(font); + + QPixmap textEquivalentPixmap(KGlobal::dirs()->findResource("data", "basket/images/tag_export_help.png")); + QMimeSourceFactory::defaultFactory()->setPixmap("__resource_help_tag_export.png", textEquivalentPixmap); + HelpLabel *textEquivalentHelp = new HelpLabel( + i18n("When does this apply?"), + "<p>" + i18n("It does apply when you copy and paste, or drag and drop notes to a text editor.") + "</p>" + + "<p>" + i18n("If filled, this property lets you paste this tag or this state as textual equivalent.") + "<br>" + + i18n("For instance, a list of notes with the <b>To Do</b> and <b>Done</b> tags are exported as lines preceded by <b>[ ]</b> or <b>[x]</b>, " + "representing an empty checkbox and a checked box.") + "</p>" + + "<p align='center'><img src=\"__resource_help_tag_export.png\"></p>", + stateWidget); + QHBoxLayout *textEquivalentHelpLayout = new QHBoxLayout((QWidget*)0, /*border=*/0, spacingHint()); + textEquivalentHelpLayout->addWidget(textEquivalentHelp); + textEquivalentHelpLayout->addStretch(255); + + m_onEveryLines = new QCheckBox(i18n("On ever&y line"), stateWidget); + + QPixmap onEveryLinesPixmap(KGlobal::dirs()->findResource("data", "basket/images/tag_export_on_every_lines_help.png")); + QMimeSourceFactory::defaultFactory()->setPixmap("__resource_help_tag_export_on_every_lines.png", onEveryLinesPixmap); + HelpLabel *onEveryLinesHelp = new HelpLabel( + i18n("What does it mean?"), + "<p>" + i18n("When a note has several lines, you can choose to export the tag or the state on the first line or on every line of the note.") + "</p>" + + "<p align='center'><img src=\"__resource_help_tag_export_on_every_lines.png\"></p>" + + "<p>" + i18n("In the example above, the tag of the top note is only exported on the first line, while the tag of the bottom note is exported on every line of the note."), + stateWidget); + QHBoxLayout *onEveryLinesHelpLayout = new QHBoxLayout((QWidget*)0, /*border=*/0, spacingHint()); + onEveryLinesHelpLayout->addWidget(onEveryLinesHelp); + onEveryLinesHelpLayout->addStretch(255); + + QGridLayout *textEquivalentGrid = new QGridLayout(0, /*rows=*/2, /*cols=*/4, /*border=*/0, /*spacing=*/spacingHint()); + textEquivalentGrid->addWidget(textEquivalentLabel, 0, 0); + textEquivalentGrid->addWidget(m_textEquivalent, 0, 1); + textEquivalentGrid->addLayout(textEquivalentHelpLayout, 0, 2); + textEquivalentGrid->addWidget(m_onEveryLines, 1, 1); + textEquivalentGrid->addLayout(onEveryLinesHelpLayout, 1, 2); + textEquivalentGrid->setColStretch(/*col=*/3, /*stretch=*/255); + + KSeparator *separator = new KSeparator(Qt::Horizontal, stateWidget); + + QGridLayout *stateGrid = new QGridLayout(stateWidget, /*rows=*/6, /*cols=*/7, /*border=*/0, /*spacing=*/spacingHint()); + stateGrid->addWidget(m_stateNameLabel, 0, 0); + stateGrid->addMultiCellWidget(m_stateName, /*fromRow=*/0, /*toRow=*/0, /*fromCol=*/1, /*toCol=*/6); + stateGrid->addWidget(emblemLabel, 1, 0); + stateGrid->addMultiCellWidget(emblemWidget, /*fromRow=*/1, /*toRow=*/1, /*fromCol=*/1, /*toCol=*/6); + stateGrid->addWidget(backgroundColorLabel, 1, 5); + stateGrid->addMultiCellLayout(backgroundColorLayout, /*fromRow=*/1, /*toRow=*/1, /*fromCol=*/6, /*toCol=*/6); + stateGrid->addWidget(textLabel, 2, 0); + stateGrid->addMultiCellLayout(textLayout, /*fromRow=*/2, /*toRow=*/2, /*fromCol=*/1, /*toCol=*/4); + stateGrid->addWidget(textColorLabel, 2, 5); + stateGrid->addWidget(m_textColor, 2, 6); + stateGrid->addWidget(fontLabel, 3, 0); + stateGrid->addMultiCellWidget(m_font, /*fromRow=*/3, /*toRow=*/3, /*fromCol=*/1, /*toCol=*/4); + stateGrid->addWidget(fontSizeLabel, 3, 5); + stateGrid->addWidget(m_fontSize, 3, 6); + stateGrid->addMultiCellWidget(separator, /*fromRow=*/4, /*toRow=*/4, /*fromCol=*/0, /*toCol=*/6); + stateGrid->addMultiCellLayout(textEquivalentGrid, /*fromRow=*/5, /*toRow=*/5, /*fromCol=*/0, /*toCol=*/6); + + QVBoxLayout *rightLayout = new QVBoxLayout(rightWidget, /*margin=*/0, spacingHint()); + rightLayout->addWidget(m_tagBox); + rightLayout->addWidget(m_stateBox); + rightLayout->addStretch(); + + layout->addWidget(rightWidget); + rightWidget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding); + + // Equalize the width of the first column of the two grids: + int maxWidth = tagNameLabel->sizeHint().width(); + maxWidth = QMAX(maxWidth, shortcutLabel->sizeHint().width()); + maxWidth = QMAX(maxWidth, m_stateNameLabel->sizeHint().width()); + maxWidth = QMAX(maxWidth, emblemLabel->sizeHint().width()); + maxWidth = QMAX(maxWidth, textLabel->sizeHint().width()); + maxWidth = QMAX(maxWidth, fontLabel->sizeHint().width()); + maxWidth = QMAX(maxWidth, backgroundColorLabel->sizeHint().width()); + maxWidth = QMAX(maxWidth, textEquivalentLabel->sizeHint().width()); + + tagNameLabel->setFixedWidth(maxWidth); + m_stateNameLabel->setFixedWidth(maxWidth); + textEquivalentLabel->setFixedWidth(maxWidth); + + // Load Tags: + for (Tag::List::iterator tagIt = Tag::all.begin(); tagIt != Tag::all.end(); ++tagIt) + m_tagCopies.append(new TagCopy(*tagIt)); + + TagListViewItem *lastInsertedItem = 0; + TagListViewItem *lastInsertedSubItem; + TagListViewItem *item; + TagListViewItem *subItem; + for (TagCopy::List::iterator tagCopyIt = m_tagCopies.begin(); tagCopyIt != m_tagCopies.end(); ++tagCopyIt) { + // New List View Item: + if (lastInsertedItem) + item = new TagListViewItem(m_tags, lastInsertedItem, *tagCopyIt); + else + item = new TagListViewItem(m_tags, *tagCopyIt); + item->setOpen(true); + lastInsertedItem = item; + // Load + if ((*tagCopyIt)->isMultiState()) { + lastInsertedSubItem = 0; + StateCopy::List stateCopies = item->tagCopy()->stateCopies; + for (StateCopy::List::iterator stateCopyIt = stateCopies.begin(); stateCopyIt != stateCopies.end(); ++stateCopyIt) { + if (lastInsertedSubItem) + subItem = new TagListViewItem(item, lastInsertedSubItem, *stateCopyIt); + else + subItem = new TagListViewItem(item, *stateCopyIt); + lastInsertedSubItem = subItem; + } + } + } + + // Connect Signals: + connect( m_tagName, SIGNAL(textChanged(const QString&)), this, SLOT(modified()) ); + connect( m_shortcut, SIGNAL(capturedShortcut(const KShortcut&)), this, SLOT(modified()) ); + connect( m_inherit, SIGNAL(stateChanged(int)), this, SLOT(modified()) ); + connect( m_stateName, SIGNAL(textChanged(const QString&)), this, SLOT(modified()) ); + connect( m_emblem, SIGNAL(iconChanged(QString)), this, SLOT(modified()) ); + connect( m_backgroundColor, SIGNAL(changed(const QColor&)), this, SLOT(modified()) ); + connect( m_bold, SIGNAL(toggled(bool)), this, SLOT(modified()) ); + connect( m_underline, SIGNAL(toggled(bool)), this, SLOT(modified()) ); + connect( m_italic, SIGNAL(toggled(bool)), this, SLOT(modified()) ); + connect( m_strike, SIGNAL(toggled(bool)), this, SLOT(modified()) ); + connect( m_textColor, SIGNAL(changed(const QColor&)), this, SLOT(modified()) ); + connect( m_font, SIGNAL(textChanged(const QString&)), this, SLOT(modified()) ); + connect( m_fontSize, SIGNAL(textChanged(const QString&)), this, SLOT(modified()) ); + connect( m_textEquivalent, SIGNAL(textChanged(const QString&)), this, SLOT(modified()) ); + connect( m_onEveryLines, SIGNAL(stateChanged(int)), this, SLOT(modified()) ); + + connect( m_tags, SIGNAL(currentChanged(QListViewItem*)), this, SLOT(currentItemChanged(QListViewItem*)) ); + connect( m_tags, SIGNAL(deletePressed()), this, SLOT(deleteTag()) ); + connect( m_tags, SIGNAL(doubleClickedItem()), this, SLOT(renameIt()) ); + + QListViewItem *firstItem = m_tags->firstChild(); + if (stateToEdit != 0) { + TagListViewItem *item = itemForState(stateToEdit); + if (item) + firstItem = item; + } + // Select the first tag unless the first tag is a multi-state tag. + // In this case, select the first state, as it let customize the state AND the associated tag. + if (firstItem) { + if (firstItem->firstChild()) + firstItem = firstItem->firstChild(); + firstItem->setSelected(true); + m_tags->setCurrentItem(firstItem); + currentItemChanged(firstItem); + if (stateToEdit == 0) + m_tags->ensureVisible(0, 0); + m_tags->setFocus(); + } else { + m_moveUp->setEnabled(false); + m_moveDown->setEnabled(false); + m_deleteTag->setEnabled(false); + m_tagBox->setEnabled(false); + m_stateBox->setEnabled(false); + } + // TODO: Disabled both boxes if no tag!!! + + // Some keyboard shortcuts: // Ctrl+arrows instead of Alt+arrows (same as Go menu in the main window) because Alt+Down is for combo boxes + QAction *selectAbove = new QAction(this); + selectAbove->setAccel(QString("Ctrl+Up")); + connect( selectAbove, SIGNAL(activated()), this, SLOT(selectUp()) ); + + QAction *selectBelow = new QAction(this); + selectBelow->setAccel(QString("Ctrl+Down")); + connect( selectBelow, SIGNAL(activated()), this, SLOT(selectDown()) ); + + QAction *selectLeft = new QAction(this); + selectLeft->setAccel(QString("Ctrl+Left")); + connect( selectLeft, SIGNAL(activated()), this, SLOT(selectLeft()) ); + + QAction *selectRight = new QAction(this); + selectRight->setAccel(QString("Ctrl+Right")); + connect( selectRight, SIGNAL(activated()), this, SLOT(selectRight()) ); + + QAction *moveAbove = new QAction(this); + moveAbove->setAccel(QString("Ctrl+Shift+Up")); + connect( moveAbove, SIGNAL(activated()), this, SLOT(moveUp()) ); + + QAction *moveBelow = new QAction(this); + moveBelow->setAccel(QString("Ctrl+Shift+Down")); + connect( moveBelow, SIGNAL(activated()), this, SLOT(moveDown()) ); + + QAction *rename = new QAction(this); + rename->setAccel(QString("F2")); + connect( rename, SIGNAL(activated()), this, SLOT(renameIt()) ); + + m_tags->setMinimumSize( + m_tags->sizeHint().width() * 2, + m_tagBox->sizeHint().height() + m_stateBox->sizeHint().height() + ); + + if (addNewTag) + QTimer::singleShot(0, this, SLOT(newTag()) ); + else + // Once the window initial size is computed and the window show, allow the user to resize it down: + QTimer::singleShot(0, this, SLOT(resetTreeSizeHint()) ); +} + +TagsEditDialog::~TagsEditDialog() +{ +} + +void TagsEditDialog::resetTreeSizeHint() +{ + m_tags->setMinimumSize(m_tags->sizeHint()); +} + +TagListViewItem* TagsEditDialog::itemForState(State *state) +{ + // Browse all tags: + QListViewItemIterator it(m_tags); + while (it.current()) { + QListViewItem *item = it.current(); + + // Return if we found the tag item: + TagListViewItem *tagItem = (TagListViewItem*)item; + if (tagItem->tagCopy() && tagItem->tagCopy()->oldTag && tagItem->tagCopy()->stateCopies[0]->oldState == state) + return tagItem; + + // Browser all sub-states: + QListViewItemIterator it2(item); + while (it2.current()) { + QListViewItem *subItem = it2.current(); + + // Return if we found the state item: + TagListViewItem *stateItem = (TagListViewItem*)subItem; + if (stateItem->stateCopy() && stateItem->stateCopy()->oldState && stateItem->stateCopy()->oldState == state) + return stateItem; + + ++it2; + } + + ++it; + } + return 0; +} + +void TagsEditDialog::newTag() +{ + // Add to the "model": + TagCopy *newTagCopy = new TagCopy(); + newTagCopy->stateCopies[0]->newState->setId("tag_state_" + QString::number( Tag::getNextStateUid() )); //TODO: Check if it's really unique + m_tagCopies.append(newTagCopy); + m_addedStates.append(newTagCopy->stateCopies[0]->newState); + + // Add to the "view": + TagListViewItem *item; + if (m_tags->firstChild()) { + // QListView::lastItem is the last item in the tree. If we the last item is a state item, the new tag gets appended to the begin of the list. + TagListViewItem *last = m_tags->lastItem(); + if (last->parent()) + last = last->parent(); + item = new TagListViewItem(m_tags, last, newTagCopy); + } else + item = new TagListViewItem(m_tags, newTagCopy); + + m_deleteTag->setEnabled(true); + m_tagBox->setEnabled(true); + + // Add to the "controler": + m_tags->setCurrentItem(item); + currentItemChanged(item); + item->setSelected(true); + m_tagName->setFocus(); +} + +void TagsEditDialog::newState() +{ + TagListViewItem *tagItem = m_tags->currentItem(); + if (tagItem->parent()) + tagItem = ((TagListViewItem*)(tagItem->parent())); + tagItem->setOpen(true); // Show sub-states if we're adding them for the first time! + + State *firstState = tagItem->tagCopy()->stateCopies[0]->newState; + + // Add the first state to the "view". From now on, it's a multi-state tag: + if (tagItem->firstChild() == 0) { + firstState->setName( tagItem->tagCopy()->newTag->name() ); + // Force emblem to exists for multi-state tags: + if (firstState->emblem().isEmpty()) + firstState->setEmblem("empty"); + new TagListViewItem(tagItem, tagItem->tagCopy()->stateCopies[0]); + } + + // Add to the "model": + StateCopy *newStateCopy = new StateCopy(); + firstState->copyTo(newStateCopy->newState); + newStateCopy->newState->setId("tag_state_" + QString::number( Tag::getNextStateUid() )); //TODO: Check if it's really unique + newStateCopy->newState->setName(""); // We copied it too but it's likely the name will not be the same + tagItem->tagCopy()->stateCopies.append(newStateCopy); + m_addedStates.append(newStateCopy->newState); + + // Add to the "view": + TagListViewItem *item = new TagListViewItem(tagItem, tagItem->lastChild(), newStateCopy); + + // Add to the "controler": + m_tags->setCurrentItem(item); + currentItemChanged(item); + m_stateName->setFocus(); +} + +void TagsEditDialog::moveUp() +{ + if (!m_moveUp->isEnabled()) // Trggered by keyboard shortcut + return; + + TagListViewItem *tagItem = m_tags->currentItem(); + TagListViewItem *prevTagItem = ((TagListViewItem*)( tagItem->prevSibling() )); + + // Move in the list view: + prevTagItem->moveItem(tagItem); + + // Move in the value list: + if (tagItem->tagCopy()) { + int pos = m_tagCopies.findIndex(tagItem->tagCopy()); + m_tagCopies.remove(tagItem->tagCopy()); + int i = 0; + for (TagCopy::List::iterator it = m_tagCopies.begin(); it != m_tagCopies.end(); ++it, ++i) + if (i == pos - 1) { + m_tagCopies.insert(it, tagItem->tagCopy()); + break; + } + } else { + StateCopy::List &stateCopies = ((TagListViewItem*)( tagItem->parent() ))->tagCopy()->stateCopies; + int pos = stateCopies.findIndex(tagItem->stateCopy()); + stateCopies.remove(tagItem->stateCopy()); + int i = 0; + for (StateCopy::List::iterator it = stateCopies.begin(); it != stateCopies.end(); ++it, ++i) + if (i == pos - 1) { + stateCopies.insert(it, tagItem->stateCopy()); + break; + } + } + + ensureCurrentItemVisible(); + + m_moveDown->setEnabled( tagItem->nextSibling() != 0 ); + m_moveUp->setEnabled( tagItem->prevSibling() != 0 ); +} + +void TagsEditDialog::moveDown() +{ + if (!m_moveDown->isEnabled()) // Trggered by keyboard shortcut + return; + + TagListViewItem *tagItem = m_tags->currentItem(); + + // Move in the list view: + tagItem->moveItem(tagItem->nextSibling()); + + // Move in the value list: + if (tagItem->tagCopy()) { + uint pos = m_tagCopies.findIndex(tagItem->tagCopy()); + m_tagCopies.remove(tagItem->tagCopy()); + if (pos == m_tagCopies.count() - 1) // Insert at end: iterator does not go there + m_tagCopies.append(tagItem->tagCopy()); + else { + uint i = 0; + for (TagCopy::List::iterator it = m_tagCopies.begin(); it != m_tagCopies.end(); ++it, ++i) + if (i == pos + 1) { + m_tagCopies.insert(it, tagItem->tagCopy()); + break; + } + } + } else { + StateCopy::List &stateCopies = ((TagListViewItem*)( tagItem->parent() ))->tagCopy()->stateCopies; + uint pos = stateCopies.findIndex(tagItem->stateCopy()); + stateCopies.remove(tagItem->stateCopy()); + if (pos == stateCopies.count() - 1) // Insert at end: iterator does not go there + stateCopies.append(tagItem->stateCopy()); + else { + uint i = 0; + for (StateCopy::List::iterator it = stateCopies.begin(); it != stateCopies.end(); ++it, ++i) + if (i == pos + 1) { + stateCopies.insert(it, tagItem->stateCopy()); + break; + } + } + } + + ensureCurrentItemVisible(); + + m_moveDown->setEnabled( tagItem->nextSibling() != 0 ); + m_moveUp->setEnabled( tagItem->prevSibling() != 0 ); +} + +void TagsEditDialog::selectUp() +{ + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Up, 0, 0); + QApplication::postEvent(m_tags, keyEvent); +} + +void TagsEditDialog::selectDown() +{ + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Down, 0, 0); + QApplication::postEvent(m_tags, keyEvent); +} + +void TagsEditDialog::selectLeft() +{ + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Left, 0, 0); + QApplication::postEvent(m_tags, keyEvent); +} + +void TagsEditDialog::selectRight() +{ + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Right, 0, 0); + QApplication::postEvent(m_tags, keyEvent); +} + +void TagsEditDialog::deleteTag() +{ + if (!m_deleteTag->isEnabled()) + return; + + TagListViewItem *item = m_tags->currentItem(); + + int result = KMessageBox::Continue; + if (item->tagCopy() && item->tagCopy()->oldTag) + result = KMessageBox::warningContinueCancel( + this, + i18n("Deleting the tag will remove it from every note it is currently assigned to."), + i18n("Confirm Delete Tag"), + KGuiItem(i18n("Delete Tag"), "editdelete") + ); + else if (item->stateCopy() && item->stateCopy()->oldState) + result = KMessageBox::warningContinueCancel( + this, + i18n("Deleting the state will remove the tag from every note the state is currently assigned to."), + i18n("Confirm Delete State"), + KGuiItem(i18n("Delete State"), "editdelete") + ); + if (result != KMessageBox::Continue) + return; + + if (item->tagCopy()) { + StateCopy::List stateCopies = item->tagCopy()->stateCopies; + for (StateCopy::List::iterator stateCopyIt = stateCopies.begin(); stateCopyIt != stateCopies.end(); ++stateCopyIt) { + StateCopy *stateCopy = *stateCopyIt; + if (stateCopy->oldState) { + m_deletedStates.append(stateCopy->oldState); + m_addedStates.remove(stateCopy->oldState); + } + m_addedStates.remove(stateCopy->newState); + } + m_tagCopies.remove(item->tagCopy()); + // Remove the new tag, to avoid keyboard-shortcut clashes: + delete item->tagCopy()->newTag; + } else { + TagListViewItem *parentItem = item->parent(); + // Remove the state: + parentItem->tagCopy()->stateCopies.remove(item->stateCopy()); + if (item->stateCopy()->oldState) { + m_deletedStates.append(item->stateCopy()->oldState); + m_addedStates.remove(item->stateCopy()->oldState); + } + m_addedStates.remove(item->stateCopy()->newState); + delete item; + item = 0; + // Transform to single-state tag if needed: + if (parentItem->childCount() == 1) { + delete parentItem->firstChild(); + m_tags->setCurrentItem(parentItem); + } + } + + delete item; + if (m_tags->currentItem()) + m_tags->currentItem()->setSelected(true); + + if (!m_tags->firstChild()) { + m_deleteTag->setEnabled(false); + m_tagBox->setEnabled(false); + m_stateBox->setEnabled(false); + } +} + +void TagsEditDialog::renameIt() +{ + if (m_tags->currentItem()->tagCopy()) + m_tagName->setFocus(); + else + m_stateName->setFocus(); +} + +void TagsEditDialog::capturedShortcut(const KShortcut &shortcut) +{ + // TODO: Validate it! + m_shortcut->setShortcut(shortcut, /*bQtShortcut=*/true); +} + +void TagsEditDialog::removeShortcut() +{ + m_shortcut->setShortcut(KShortcut(), /*bQtShortcut=*/true); + modified(); +} + +void TagsEditDialog::removeEmblem() +{ + m_emblem->resetIcon(); + modified(); +} + +void TagsEditDialog::modified() +{ + if (m_loading) + return; + + TagListViewItem *tagItem = m_tags->currentItem(); + if (tagItem == 0) + return; + + if (tagItem->tagCopy()) { + if (tagItem->tagCopy()->isMultiState()) { + saveTagTo(tagItem->tagCopy()->newTag); + } else { + saveTagTo(tagItem->tagCopy()->newTag); + saveStateTo(tagItem->tagCopy()->stateCopies[0]->newState); + } + } else if (tagItem->stateCopy()) { + saveTagTo(((TagListViewItem*)(tagItem->parent()))->tagCopy()->newTag); + saveStateTo(tagItem->stateCopy()->newState); + } + + m_tags->currentItem()->setup(); + if (m_tags->currentItem()->parent()) + m_tags->currentItem()->parent()->setup(); + + m_removeShortcut->setEnabled(!m_shortcut->shortcut().isNull()); + m_removeEmblem->setEnabled(!m_emblem->icon().isEmpty() && !m_tags->currentItem()->isEmblemObligatory()); + m_onEveryLines->setEnabled(!m_textEquivalent->text().isEmpty()); +} + +void TagsEditDialog::currentItemChanged(QListViewItem *item) +{ + if (item == 0) + return; + + m_loading = true; + + TagListViewItem *tagItem = (TagListViewItem*)item; + if (tagItem->tagCopy()) { + if (tagItem->tagCopy()->isMultiState()) { + loadTagFrom(tagItem->tagCopy()->newTag); + loadBlankState(); + m_stateBox->setEnabled(false); + m_stateBox->setTitle(i18n("State")); + m_stateNameLabel->setEnabled(true); + m_stateName->setEnabled(true); + } else { + loadTagFrom(tagItem->tagCopy()->newTag); // TODO: No duplicat + loadStateFrom(tagItem->tagCopy()->stateCopies[0]->newState); + m_stateBox->setEnabled(true); + m_stateBox->setTitle(i18n("Appearance")); + m_stateName->setText(""); + m_stateNameLabel->setEnabled(false); + m_stateName->setEnabled(false); + } + } else if (tagItem->stateCopy()) { + loadTagFrom(((TagListViewItem*)(tagItem->parent()))->tagCopy()->newTag); + loadStateFrom(tagItem->stateCopy()->newState); + m_stateBox->setEnabled(true); + m_stateBox->setTitle(i18n("State")); + m_stateNameLabel->setEnabled(true); + m_stateName->setEnabled(true); + } + + ensureCurrentItemVisible(); + + m_loading = false; +} + +void TagsEditDialog::ensureCurrentItemVisible() +{ + TagListViewItem *tagItem = m_tags->currentItem(); + + // Ensure the tag and the states (if available) to be visible, but if there is a looooot of states, + // ensure the tag is still visible, even if the last states are not... + int y = m_tags->itemPos(tagItem); + int height = tagItem->totalHeight(); + int bottom = y + QMIN(height, m_tags->visibleHeight()); + int xMiddle = m_tags->contentsX() + m_tags->visibleWidth() / 2; + m_tags->ensureVisible( xMiddle, bottom, 0,0 ); + m_tags->ensureVisible( xMiddle, y, 0,0 ); + + m_moveDown->setEnabled( tagItem->nextSibling() != 0 ); + m_moveUp->setEnabled( tagItem->prevSibling() != 0 ); +} + +void TagsEditDialog::loadBlankState() +{ + m_stateName->setText(""); + m_emblem->resetIcon(); + m_removeEmblem->setEnabled(false); + m_backgroundColor->setColor(QColor()); + m_bold->setOn(false); + m_underline->setOn(false); + m_italic->setOn(false); + m_strike->setOn(false); + m_textColor->setColor(QColor()); + m_font->setCurrentItem(0); + m_fontSize->setCurrentItem(0); + m_textEquivalent->setText(""); + m_onEveryLines->setChecked(false); +} + +void TagsEditDialog::loadStateFrom(State *state) +{ + m_stateName->setText(state->name()); + if (state->emblem().isEmpty()) + m_emblem->resetIcon(); + else + m_emblem->setIcon(state->emblem()); + m_removeEmblem->setEnabled(!state->emblem().isEmpty() && !m_tags->currentItem()->isEmblemObligatory()); + m_backgroundColor->setColor(state->backgroundColor()); + m_bold->setOn(state->bold()); + m_underline->setOn(state->underline()); + m_italic->setOn(state->italic()); + m_strike->setOn(state->strikeOut()); + m_textColor->setColor(state->textColor()); + m_textEquivalent->setText(state->textEquivalent()); + m_onEveryLines->setChecked(state->onAllTextLines()); + + if (state->fontName().isEmpty()) + m_font->setCurrentItem(0); + else + m_font->setCurrentFont(state->fontName()); + + if (state->fontSize() == -1) + m_fontSize->setCurrentItem(0); + else + m_fontSize->setCurrentText(QString::number(state->fontSize())); +} + +void TagsEditDialog::loadTagFrom(Tag *tag) +{ + m_tagName->setText(tag->name()); + m_shortcut->setShortcut(tag->shortcut(), /*bQtShortcut=*/false); + m_removeShortcut->setEnabled(!tag->shortcut().isNull()); + m_inherit->setChecked(tag->inheritedBySiblings()); +} + +void TagsEditDialog::saveStateTo(State *state) +{ + state->setName(m_stateName->text()); + state->setEmblem(m_emblem->icon()); + state->setBackgroundColor(m_backgroundColor->color()); + state->setBold(m_bold->isOn()); + state->setUnderline(m_underline->isOn()); + state->setItalic(m_italic->isOn()); + state->setStrikeOut(m_strike->isOn()); + state->setTextColor(m_textColor->color()); + state->setTextEquivalent(m_textEquivalent->text()); + state->setOnAllTextLines(m_onEveryLines->isChecked()); + + if (m_font->currentItem() == 0) + state->setFontName(""); + else + state->setFontName(m_font->currentFont()); + + bool conversionOk; + int fontSize = m_fontSize->currentText().toInt(&conversionOk); + if (conversionOk) + state->setFontSize(fontSize); + else + state->setFontSize(-1); +} + +void TagsEditDialog::saveTagTo(Tag *tag) +{ + tag->setName(m_tagName->text()); + tag->setShortcut(m_shortcut->shortcut()); + tag->setInheritedBySiblings(m_inherit->isChecked()); +} + +void TagsEditDialog::slotCancel() +{ + // All copies of tag have a shortcut, that is stored as a QAction. + // So, shortcuts are duplicated, and if the user press one tag keyboard-shortcut in the main window, there is a conflict. + // We then should delete every copies: + for (TagCopy::List::iterator tagCopyIt = m_tagCopies.begin(); tagCopyIt != m_tagCopies.end(); ++tagCopyIt) { + delete (*tagCopyIt)->newTag; + } + + KDialogBase::slotCancel(); +} + +void TagsEditDialog::slotOk() +{ + Tag::all.clear(); + for (TagCopy::List::iterator tagCopyIt = m_tagCopies.begin(); tagCopyIt != m_tagCopies.end(); ++tagCopyIt) { + TagCopy *tagCopy = *tagCopyIt; + // Copy changes to the tag and append in the list of tags:: + if (tagCopy->oldTag) { + tagCopy->newTag->copyTo(tagCopy->oldTag); + delete tagCopy->newTag; + } + Tag *tag = (tagCopy->oldTag ? tagCopy->oldTag : tagCopy->newTag); + Tag::all.append(tag); + // And change all states: + State::List &states = tag->states(); + StateCopy::List &stateCopies = tagCopy->stateCopies; + states.clear(); + for (StateCopy::List::iterator stateCopyIt = stateCopies.begin(); stateCopyIt != stateCopies.end(); ++stateCopyIt) { + StateCopy *stateCopy = *stateCopyIt; + // Copy changes to the state and append in the list of tags: + if (stateCopy->oldState) + stateCopy->newState->copyTo(stateCopy->oldState); + State *state = (stateCopy->oldState ? stateCopy->oldState : stateCopy->newState); + states.append(state); + state->setParentTag(tag); + } + } + Tag::saveTags(); + + // Notify removed states and tags, and then remove them: + if (!m_deletedStates.isEmpty()) + Global::bnpView->removedStates(m_deletedStates); + + // Update every note (change colors, size because of font change or added/removed emblems...): + Global::bnpView->relayoutAllBaskets(); + Global::bnpView->recomputeAllStyles(); + + KDialogBase::slotOk(); +} + +#include "tagsedit.moc" diff --git a/src/tagsedit.h b/src/tagsedit.h new file mode 100644 index 0000000..0986bac --- /dev/null +++ b/src/tagsedit.h @@ -0,0 +1,185 @@ +/*************************************************************************** + * Copyright (C) 2005 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef TAGEDIT_H +#define TAGEDIT_H + +#include <kdialogbase.h> +#include <kcombobox.h> +#include <qlistview.h> +#include <qvaluelist.h> + +class QGroupBox; +class QLineEdit; +class QCheckBox; +class KPushButton; +class KKeyButton; +class KIconButton; +class KFontCombo; +class QLabel; +class KShortcut; + +class KColorCombo2; + +class FontSizeCombo; + +class Tag; +class State; + +class StateCopy +{ + public: + typedef QValueList<StateCopy*> List; + StateCopy(State *old = 0); + ~StateCopy(); + State *oldState; + State *newState; + void copyBack(); +}; + +class TagCopy +{ + public: + typedef QValueList<TagCopy*> List; + TagCopy(Tag *old = 0); + ~TagCopy(); + Tag *oldTag; + Tag *newTag; + StateCopy::List stateCopies; + void copyBack(); + bool isMultiState(); +}; + +class TagListViewItem : public QListViewItem +{ + public: + TagListViewItem(QListView *parent, TagCopy *tagCopy); + TagListViewItem(QListViewItem *parent, TagCopy *tagCopy); + TagListViewItem(QListView *parent, QListViewItem *after, TagCopy *tagCopy); + TagListViewItem(QListViewItem *parent, QListViewItem *after, TagCopy *tagCopy); + TagListViewItem(QListView *parent, StateCopy *stateCopy); + TagListViewItem(QListViewItem *parent, StateCopy *stateCopy); + TagListViewItem(QListView *parent, QListViewItem *after, StateCopy *stateCopy); + TagListViewItem(QListViewItem *parent, QListViewItem *after, StateCopy *stateCopy); + ~TagListViewItem(); + TagCopy* tagCopy() { return m_tagCopy; } + StateCopy* stateCopy() { return m_stateCopy; } + bool isEmblemObligatory(); + TagListViewItem* lastChild(); + TagListViewItem* prevSibling(); + TagListViewItem* parent() const; // Reimplemented to cast the return value + int width(const QFontMetrics &fontMetrics, const QListView *listView, int column) const; + void setup(); + void paintCell(QPainter *painter, const QColorGroup &colorGroup, int column, int width, int align); + + private: + TagCopy *m_tagCopy; + StateCopy *m_stateCopy; +}; + +class TagListView : public QListView +{ + Q_OBJECT + public: + TagListView(QWidget *parent = 0, const char *name = 0, WFlags flags = 0); + ~TagListView(); + void keyPressEvent(QKeyEvent *event); + void contentsMouseDoubleClickEvent(QMouseEvent *event); + void contentsMousePressEvent(QMouseEvent *event); + void contentsMouseReleaseEvent(QMouseEvent *event); + TagListViewItem* currentItem() const; // Reimplemented to cast the return value + TagListViewItem* firstChild() const; // Reimplemented to cast the return value + TagListViewItem* lastItem() const; // Reimplemented to cast the return value + signals: + void deletePressed(); + void doubleClickedItem(); +}; + +/** + * @author S�astien Laot + */ +class TagsEditDialog : public KDialogBase +{ + Q_OBJECT + public: + TagsEditDialog(QWidget *parent = 0, State *stateToEdit = 0, bool addNewTag = false); + ~TagsEditDialog(); + State::List deletedStates() { return m_deletedStates; } + State::List addedStates() { return m_addedStates; } + TagListViewItem* itemForState(State *state); + private slots: + void newTag(); + void newState(); + void moveUp(); + void moveDown(); + void deleteTag(); + void renameIt(); + void capturedShortcut(const KShortcut &shortcut); + void removeShortcut(); + void removeEmblem(); + void modified(); + void currentItemChanged(QListViewItem *item); + void slotCancel(); + void slotOk(); + void selectUp(); + void selectDown(); + void selectLeft(); + void selectRight(); + void resetTreeSizeHint(); + private: + void loadBlankState(); + void loadStateFrom(State *state); + void loadTagFrom(Tag *tag); + void saveStateTo(State *state); + void saveTagTo(Tag *tag); + void ensureCurrentItemVisible(); + TagListView *m_tags; + KPushButton *m_moveUp; + KPushButton *m_moveDown; + KPushButton *m_deleteTag; + QLineEdit *m_tagName; + KKeyButton *m_shortcut; + QPushButton *m_removeShortcut; + QCheckBox *m_inherit; + QGroupBox *m_tagBox; + QGroupBox *m_stateBox; + QLabel *m_stateNameLabel; + QLineEdit *m_stateName; + KIconButton *m_emblem; + QPushButton *m_removeEmblem; + QPushButton *m_bold; + QPushButton *m_underline; + QPushButton *m_italic; + QPushButton *m_strike; + KColorCombo2 *m_textColor; + KFontCombo *m_font; + FontSizeCombo *m_fontSize; + KColorCombo2 *m_backgroundColor; + QLineEdit *m_textEquivalent; + QCheckBox *m_onEveryLines; + + TagCopy::List m_tagCopies; + State::List m_deletedStates; + State::List m_addedStates; + + bool m_loading; +}; + +#endif // TAGEDIT_H diff --git a/src/tools.cpp b/src/tools.cpp new file mode 100644 index 0000000..9acbda7 --- /dev/null +++ b/src/tools.cpp @@ -0,0 +1,446 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�astien Laot * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <kdebug.h> +#include <qstring.h> +#include <qpixmap.h> +#include <qimage.h> +#include <qstylesheet.h> +#include <qregexp.h> +#include <qvaluestack.h> +#include <qfileinfo.h> +#include <qdir.h> +#include <qmime.h> +#include <qfont.h> +#include <qfontinfo.h> +#include <qobjectlist.h> + +#include "tools.h" + +QMemArray<QTime> StopWatch::starts; +QMemArray<double> StopWatch::totals; +QMemArray<uint> StopWatch::counts; + +void StopWatch::start(uint id) +{ + if (id >= starts.size()) { + totals.resize(id + 1); + counts.resize(id + 1); + for (uint i = starts.size(); i <= id; i++) { + totals[i] = 0; + counts[i] = 0; + } + starts.resize(id + 1); + } + starts[id] = QTime::currentTime(); +} + +void StopWatch::check(uint id) +{ + if (id >= starts.size()) + return; + double time = starts[id].msecsTo(QTime::currentTime()) / 1000.0; + totals[id] += time; + counts[id]++; + kdDebug() << k_funcinfo << "Timer_" << id << ": " << time << " s [" << counts[id] << " times, total: " << totals[id] << " s, average: " << totals[id] / counts[id] << " s]" << endl; +} + +QString Tools::textToHTML(const QString &text) +{ + if (text.isEmpty()) + return "<p></p>"; + if (/*text.isEmpty() ||*/ text == " " || text == " ") + return "<p> </p>"; + + // convertFromPlainText() replace "\n\n" by "</p>\n<p>": we don't want that + QString htmlString = QStyleSheet::convertFromPlainText(text, QStyleSheetItem::WhiteSpaceNormal); + return htmlString.replace("</p>\n", "<br>\n<br>\n").replace("\n<p>", "\n"); // Don't replace first and last tags +} + +QString Tools::textToHTMLWithoutP(const QString &text) +{ + // textToHTML(text) return "<p>HTMLizedText</p>". We remove the strating "<p>" and ending </p>" + QString HTMLizedText = textToHTML(text); + return HTMLizedText.mid(3, HTMLizedText.length() - 3 - 4); +} + +QString Tools::htmlToParagraph(const QString &html) +{ + QString result = html; + bool startedBySpan = false; + + // Remove the <html> start tag, all the <head> and the <body> start + // Because <body> can contain style="..." parameter, we transform it to <span> + int pos = result.find("<body"); + if (pos != -1) { + result = "<span" + result.mid(pos + 5); + startedBySpan = true; + } + + // Remove the ending "</p>\n</body></html>", each tag can be separated by space characters (%s) + // "</p>" can be omitted (eg. if the HTML doesn't contain paragraph but tables), as well as "</body>" (optinal) + pos = result.find(QRegExp("(?:(?:</p>[\\s\\n\\r\\t]*)*</body>[\\s\\n\\r\\t]*)*</html>", false)); // Case unsensitive + if (pos != -1) + result = result.left(pos); + + if (startedBySpan) + result += "</span>"; + + return result.replace("</p>", "<br><br>").replace("<p>", ""); +} + +// The following is adapted from KStringHanlder::tagURLs +// The adaptation lies in the change to urlEx +// Thanks to Richard Heck +QString Tools::tagURLs(const QString &text) +{ + QRegExp urlEx("(www\\.(?!\\.)|([a-zA-z]+)://)[\\d\\w\\./,:_~\\?=&;#@\\-\\+\\%\\$]+[\\d\\w/]"); + + QString richText(text); + int urlPos = 0; + int urlLen; + while ((urlPos = urlEx.search(richText, urlPos)) >= 0) { + urlLen = urlEx.matchedLength(); + QString href = richText.mid(urlPos, urlLen); + // Qt doesn't support (?<=pattern) so we do it here + if ((urlPos > 0) && richText[urlPos-1].isLetterOrNumber()) { + urlPos++; + continue; + } + QString anchor = "<a href=\"" + href + "\">" + href + "</a>"; + richText.replace(urlPos, urlLen, anchor); + urlPos += anchor.length(); + } + return richText; +} + +QString Tools::htmlToText(const QString &html) +{ + QString text = htmlToParagraph(html); + text.remove("\n"); + text.replace("</h1>", "\n"); + text.replace("</h2>", "\n"); + text.replace("</h3>", "\n"); + text.replace("</h4>", "\n"); + text.replace("</h5>", "\n"); + text.replace("</h6>", "\n"); + text.replace("</li>", "\n"); + text.replace("</dt>", "\n"); + text.replace("</dd>", "\n"); + text.replace("<dd>", " "); + text.replace("</div>","\n"); + text.replace("</blockquote>","\n"); + text.replace("</caption>","\n"); + text.replace("</tr>", "\n"); + text.replace("</th>", " "); + text.replace("</td>", " "); + text.replace("<br>", "\n"); + text.replace("<br />","\n"); + // FIXME: Format <table> tags better, if possible + // TODO: Replace é and co. by theire equivalent! + + // To manage tags: + int pos = 0; + int pos2; + QString tag, tag3; + // To manage lists: + int deep = 0; // The deep of the current line in imbriqued lists + QValueStack<bool> ul; // true if current list is a <ul> one, false if it's an <ol> one + QValueStack<int> lines; // The line number if it is an <ol> list + // We're removing every other tags, or replace them in the case of li: + while ( (pos = text.find("<"), pos) != -1 ) { + // What is the current tag? + tag = text.mid(pos + 1, 2); + tag3 = text.mid(pos + 1, 3); + // Lists work: + if (tag == "ul") { + deep++; + ul.push(true); + lines.push(-1); + } else if (tag == "ol") { + deep++; + ul.push(false); + lines.push(0); + } else if (tag3 == "/ul" || tag3 == "/ol") { + deep--; + ul.pop(); + lines.pop(); + } + // Where the tag closes? + pos2 = text.find(">"); + if (pos2 != -1) { + // Remove the tag: + text.remove(pos, pos2 - pos + 1); + // And replace li with "* ", "x. "... without forbidding to indent that: + if (tag == "li") { + // How many spaces before the line (indentation): + QString spaces = ""; + for (int i = 1; i < deep; i++) + spaces += " "; + // The bullet or number of the line: + QString bullet = "* "; + if (ul.top() == false) { + lines.push(lines.pop() + 1); + bullet = QString::number(lines.top()) + ". "; + } + // Insertion: + text.insert(pos, spaces + bullet); + } + if ( (tag3 == "/ul" || tag3 == "/ol") && deep == 0 ) + text.insert(pos, "\n"); // Empty line before and after a set of lists + } + ++pos; + } + + text.replace(">", ">"); + text.replace("<", "<"); + text.replace(""", "\""); + text.replace(" ", " "); + text.replace("&", "&"); // CONVERT IN LAST!! + + return text; +} + +QString Tools::cssFontDefinition(const QFont &font, bool onlyFontFamily) +{ + // The font definition: + QString definition = QString(font.italic() ? "italic " : "") + + QString(font.bold() ? "bold " : "") + + QString::number(QFontInfo(font).pixelSize()) + "px "; + + // Then, try to match the font name with a standard CSS font family: + QString genericFont = ""; + if (definition.contains("serif", false) || definition.contains("roman", false)) + genericFont = "serif"; + // No "else if" because "sans serif" must be counted as "sans". So, the order between "serif" and "sans" is important + if (definition.contains("sans", false) || definition.contains("arial", false) || definition.contains("helvetica", false)) + genericFont = "sans-serif"; + if (definition.contains("mono", false) || definition.contains("courier", false) || + definition.contains("typewriter", false) || definition.contains("console", false) || + definition.contains("terminal", false) || definition.contains("news", false)) + genericFont = "monospace"; + + // Eventually add the generic font family to the definition: + QString fontDefinition = "\"" + font.family() + "\""; + if (!genericFont.isEmpty()) + fontDefinition += ", " + genericFont; + + if (onlyFontFamily) + return fontDefinition; + + return definition + fontDefinition; +} + +QString Tools::stripEndWhiteSpaces(const QString &string) +{ + uint length = string.length(); + uint i; + for (i = length; i > 0; --i) + if (!string[i-1].isSpace()) + break; + if (i == 0) + return ""; + else + return string.left(i); +} + + + +bool Tools::isWebColor(const QColor &color) +{ + int r = color.red(); // The 216 web colors are those colors whose RGB (Red, Green, Blue) + int g = color.green(); // values are all in the set (0, 51, 102, 153, 204, 255). + int b = color.blue(); + + return ( ( r == 0 || r == 51 || r == 102 || + r == 153 || r == 204 || r == 255 ) && + ( g == 0 || g == 51 || g == 102 || + g == 153 || g == 204 || g == 255 ) && + ( b == 0 || b == 51 || b == 102 || + b == 153 || b == 204 || b == 255 ) ); +} + +QColor Tools::mixColor(const QColor &color1, const QColor &color2) +{ + QColor mixedColor; + mixedColor.setRgb( (color1.red() + color2.red()) / 2, + (color1.green() + color2.green()) / 2, + (color1.blue() + color2.blue()) / 2 ); + return mixedColor; +} + +bool Tools::tooDark(const QColor &color) +{ + int dontCare, value; + color.getHsv(/*hue:*/dontCare, /*saturation:*/dontCare, value); + return value < 175; +} + + +// TODO: Use it for all indentPixmap() +QPixmap Tools::normalizePixmap(const QPixmap &pixmap, int width, int height) +{ + if (height <= 0) + height = width; + + if (pixmap.isNull() || (pixmap.width() == width && pixmap.height() == height)) + return pixmap; + + return pixmap; +} + +QPixmap Tools::indentPixmap(const QPixmap &source, int depth, int deltaX) +{ + // Verify if it is possible: + if (depth <= 0 || source.isNull()) + return source; + + // Compute the number of pixels to indent: + if (deltaX <= 0) + deltaX = 2 * source.width() / 3; + int indent = depth * deltaX; + + // Create the images: + QImage resultImage(indent + source.width(), source.height(), 32); + QImage sourceImage = source.convertToImage(); + resultImage.setAlphaBuffer(true); + + // Clear the indent part (the left part) by making it fully transparent: + uint *p; + for (int row = 0; row < resultImage.height(); ++row) { + for (int column = 0; column < resultImage.width(); ++column) { + p = (uint *)resultImage.scanLine(row) + column; + *p = 0; // qRgba(0, 0, 0, 0) + } + } + + // Copy the source image byte per byte to the right part: + uint *q; + for (int row = 0; row < sourceImage.height(); ++row) { + for (int column = 0; column < sourceImage.width(); ++column) { + p = (uint *)resultImage.scanLine(row) + indent + column; + q = (uint *)sourceImage.scanLine(row) + column; + *p = *q; + } + } + + // And return the result: + QPixmap result; + result.convertFromImage(resultImage); + return result; +} + +#include <iostream> + +void Tools::deleteRecursively(const QString &folderOrFile) +{ + if (folderOrFile.isEmpty()) + return; + + QFileInfo fileInfo(folderOrFile); + if (fileInfo.isDir()) { + // Delete the child files: + QDir dir(folderOrFile, QString::null, QDir::Name | QDir::IgnoreCase, QDir::All | QDir::Hidden); + QStringList list = dir.entryList(); + for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) + if ( *it != "." && *it != ".." ) + deleteRecursively(folderOrFile + "/" + *it); + // And then delete the folder: + dir.rmdir(folderOrFile); + } else + // Delete the file: + QFile::remove(folderOrFile); +} + +QString Tools::fileNameForNewFile(const QString &wantedName, const QString &destFolder) +{ + QString fileName = wantedName; + QString fullName = destFolder + fileName; + QString extension = ""; + int number = 2; + QDir dir; + + // First check if the file do not exists yet (simplier and more often case) + dir = QDir(fullName); + if ( ! dir.exists(fullName) ) + return fileName; + + // Find the file extension, if it exists : Split fileName in fileName and extension + // Example : fileName == "note5-3.txt" => fileName = "note5-3" and extension = ".txt" + int extIndex = fileName.findRev('.'); + if (extIndex != -1 && extIndex != int(fileName.length()-1)) { // Extension found and fileName do not ends with '.' ! + extension = fileName.mid(extIndex); + fileName.truncate(extIndex); + } // else fileName = fileName and extension = "" + + // Find the file number, if it exists : Split fileName in fileName and number + // Example : fileName == "note5-3" => fileName = "note5" and number = 3 + int extNumber = fileName.findRev('-'); + if (extNumber != -1 && extNumber != int(fileName.length()-1)) { // Number found and fileName do not ends with '-' ! + bool isANumber; + int theNumber = fileName.mid(extNumber + 1).toInt(&isANumber); + if (isANumber) { + number = theNumber; + fileName.truncate(extNumber); + } // else : + } // else fileName = fileName and number = 2 (because if the file already exists, the genereated name is at last the 2nd) + + QString finalName; + for (/*int number = 2*/; ; ++number) { // TODO: FIXME: If overflow ??? + finalName = fileName + "-" + QString::number(number) + extension; + fullName = destFolder + finalName; + dir = QDir(fullName); + if ( ! dir.exists(fullName) ) + break; + } + + return finalName; +} + + +// TODO: Move it from NoteFactory +/*QString NoteFactory::iconForURL(const KURL &url) +{ + QString icon = KMimeType::iconForURL(url.url()); + if ( url.protocol() == "mailto" ) + icon = "message"; + return icon; +}*/ + +bool Tools::isAFileCut(QMimeSource *source) +{ + if (source->provides("application/x-kde-cutselection")) { + QByteArray array = source->encodedData("application/x-kde-cutselection"); + return !array.isEmpty() && QCString(array.data(), array.size() + 1).at(0) == '1'; + } else + return false; +} + +void Tools::printChildren(QObject* parent) +{ + const QObjectList* objs = parent->children(); + QObjectListIt it(*objs); + QObject *obj; + + while((obj = it.current())!= 0){ + ++it; + kdDebug() << k_funcinfo << obj->className() << ": " << obj->name() << endl; + } +} diff --git a/src/tools.h b/src/tools.h new file mode 100644 index 0000000..338abfd --- /dev/null +++ b/src/tools.h @@ -0,0 +1,95 @@ +/*************************************************************************** + * Copyright (C) 2003 by Sébastien Laoût * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef TOOLS_H +#define TOOLS_H + +class QString; +class QColor; +class QMimeSource; + +class StopWatch +{ + public: + static void start(uint id); + static void check(uint id); + private: + static QMemArray<QTime> starts; + static QMemArray<double> totals; + static QMemArray<uint> counts; +}; + +/** Some useful functions for that application. + * @author Sébastien Laoût + */ +namespace Tools +{ + // Text <-> HTML Conversions and Tools: + QString textToHTML(const QString &text); + QString textToHTMLWithoutP(const QString &text); + QString htmlToParagraph(const QString &html); + QString htmlToText(const QString &html); + QString tagURLs(const QString &test); + QString cssFontDefinition(const QFont &font, bool onlyFontFamily = false); + + // String Manipulations: + QString stripEndWhiteSpaces(const QString &string); + + // Pixmap Manipulations: + /** @Return true if it is a Web color + */ + bool isWebColor(const QColor &color); + /** @Return a color that is 50% of @p color1 and 50% of @p color2. + */ + QColor mixColor(const QColor &color1, const QColor &color2); + /** @Return true if the color is too dark to be darkened one more time. + */ + bool tooDark(const QColor &color); + /** Make sure the @p pixmap is of the size (@p width, @p height) and @return a pixmap of this size. + * If @p height <= 0, then width will be used to make the picture square. + */ + QPixmap normalizePixmap(const QPixmap &pixmap, int width, int height = 0); + /** @Return the pixmap @p source with depth*deltaX transparent pixels added to the left.\n + * If @p deltaX is <= 0, then an indent delta is computed depending on the @p source width. + */ + QPixmap indentPixmap(const QPixmap &source, int depth, int deltaX = 0); + + // File and Folder Manipulations: + /** Delete the folder @p folderOrFile recursively (to remove sub-folders and child files too). + */ + void deleteRecursively(const QString &folderOrFile); + /** @Return a new filename that doesn't already exist in @p destFolder. + * If @p wantedName alread exist in @p destFolder, a dash and a number will be added before the extenssion. + * Id there were already such a number in @p wantedName, it is incremented until a free filename is found. + */ + QString fileNameForNewFile(const QString &wantedName, const QString &destFolder); + + // Other: + //void iconForURL(const KURL &url); + /** @Return true if the source is from a file cutting in Konqueror. + * @Return false if it was just a copy or if it was a drag. + */ + bool isAFileCut(QMimeSource *source); + + // Debug + void printChildren(QObject* parent); +} + +#endif // TOOLS_H diff --git a/src/variouswidgets.cpp b/src/variouswidgets.cpp new file mode 100644 index 0000000..3d0fa74 --- /dev/null +++ b/src/variouswidgets.cpp @@ -0,0 +1,325 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qlayout.h> +#include <qlineedit.h> +#include <qlabel.h> +#include <qsizegrip.h> +#include <qpushbutton.h> +#include <qstring.h> +#include <qsizepolicy.h> +#include <kopenwith.h> +#include <klocale.h> +#include <qwhatsthis.h> +#include <kiconview.h> +#include <kiconloader.h> +#include <qdragobject.h> +#include <qfontdatabase.h> + +#include "variouswidgets.h" + +/** class RunCommandRequester: */ + +RunCommandRequester::RunCommandRequester(const QString &runCommand, const QString &message, QWidget *parent, const char *name) + : QWidget(parent, name) +{ + m_message = message; + + QHBoxLayout *layout = new QHBoxLayout(this, /*margin=*/0, KDialogBase::spacingHint()); + m_runCommand = new QLineEdit(runCommand, this); + QPushButton *pb = new QPushButton(/*"C&hoose..."*/i18n("..."), this); + + pb->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + layout->addWidget(m_runCommand); + layout->addWidget(pb); + + connect( pb, SIGNAL(clicked()), this, SLOT(slotSelCommand()) ); +} + +RunCommandRequester::~RunCommandRequester() +{ +} + +void RunCommandRequester::slotSelCommand() +{ + KOpenWithDlg *dlg = new KOpenWithDlg(KURL::List(), m_message, m_runCommand->text(), this); + dlg->exec(); + if ( ! dlg->text().isEmpty() ) + m_runCommand->setText(dlg->text()); +} + +QString RunCommandRequester::runCommand() +{ + return m_runCommand->text(); +} + +void RunCommandRequester::setRunCommand(const QString &runCommand) +{ + m_runCommand->setText(runCommand); +} + +/** class IconSizeCombo: */ + +IconSizeCombo::IconSizeCombo(bool rw, QWidget *parent, const char *name) + : QComboBox(rw, parent, name) +{ + insertItem(i18n("16 by 16 pixels")); + insertItem(i18n("22 by 22 pixels")); + insertItem(i18n("32 by 32 pixels")); + insertItem(i18n("48 by 48 pixels")); + insertItem(i18n("64 by 64 pixels")); + insertItem(i18n("128 by 128 pixels")); + setCurrentItem(2); +} + +IconSizeCombo::~IconSizeCombo() +{ +} + +int IconSizeCombo::iconSize() +{ + switch (currentItem()) { + default: + case 0: return 16; + case 1: return 22; + case 2: return 32; + case 3: return 48; + case 4: return 64; + case 5: return 128; + } +} + +void IconSizeCombo::setSize(int size) +{ + switch (size) { + default: + case 16: setCurrentItem(0); break; + case 22: setCurrentItem(1); break; + case 32: setCurrentItem(2); break; + case 48: setCurrentItem(3); break; + case 64: setCurrentItem(4); break; + case 128: setCurrentItem(5); break; + } +} + +/** class ViewSizeDialog: */ + +ViewSizeDialog::ViewSizeDialog(QWidget *parent, int w, int h) + : QDialog(parent, "ViewSizeDialog") +{ + QLabel *label = new QLabel(i18n( + "Resize the window to select the image size\n" + "and close it or press Escape to accept changes."), this); + label->move(8, 8); + label->setFixedSize( label->sizeHint() ); + + // setSizeGripEnabled(true) doesn't work (the grip stay at the same place), so we emulate it: + m_sizeGrip = new QSizeGrip(this); + m_sizeGrip->setFixedSize( m_sizeGrip->sizeHint() ); + + setGeometry(x(), y(), w, h); +} + +ViewSizeDialog::~ViewSizeDialog() +{ +} + +void ViewSizeDialog::resizeEvent(QResizeEvent *) +{ + setCaption( i18n("%1 by %2 pixels").arg(QString::number(width())).arg(QString::number(height())) ); + m_sizeGrip->move( width() - m_sizeGrip->width(), height() - m_sizeGrip->height() ); +} + +/** class HelpLabel: */ + +HelpLabel::HelpLabel(const QString &text, const QString &message, QWidget *parent) + : KURLLabel(parent), m_message(message) +{ + setText(text); + connect( this, SIGNAL(leftClickedURL()), this, SLOT(showMessage()) ); +} + +HelpLabel::~HelpLabel() +{ +} + +void HelpLabel::showMessage() +{ + QWhatsThis::display(m_message, mapToGlobal( QPoint(width() / 2, height()) )); +} + +void HelpLabel::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Space) + showMessage(); + else + KURLLabel::keyPressEvent(event); +} + +/** class IconSizeDialog: */ + +class UndraggableKIconView : public KIconView +{ + public: + UndraggableKIconView(QWidget * parent = 0, const char * name = 0, WFlags f = 0) : KIconView(parent, name, f) {} + QDragObject* dragObject() { return 0; } +}; + +IconSizeDialog::IconSizeDialog(const QString &caption, const QString &message, const QString &icon, int iconSize, QWidget *parent) + : KDialogBase(KDialogBase::Swallow, caption, KDialogBase::Ok | KDialogBase::Cancel, + KDialogBase::Ok, parent, /*name=*/0, /*modal=*/true, /*separator=*/false) +{ + QWidget *page = new QWidget(this); + QVBoxLayout *topLayout = new QVBoxLayout(page, /*margin=*/0, spacingHint()); + + QLabel *label = new QLabel(message, page); + topLayout->addWidget(label); + + KIconView *iconView = new UndraggableKIconView(page); + iconView->setItemsMovable(false); + iconView->setSelectionMode(KIconView::Single); + m_size16 = new KIconViewItem(iconView, 0, i18n("16 by 16 pixels"), DesktopIcon(icon, 16)); + m_size22 = new KIconViewItem(iconView, m_size16, i18n("22 by 22 pixels"), DesktopIcon(icon, 22)); + m_size32 = new KIconViewItem(iconView, m_size22, i18n("32 by 32 pixels"), DesktopIcon(icon, 32)); + m_size48 = new KIconViewItem(iconView, m_size32, i18n("48 by 48 pixels"), DesktopIcon(icon, 48)); + m_size64 = new KIconViewItem(iconView, m_size48, i18n("64 by 64 pixels"), DesktopIcon(icon, 64)); + m_size128 = new KIconViewItem(iconView, m_size64, i18n("128 by 128 pixels"), DesktopIcon(icon, 128)); + iconView->setMinimumWidth(m_size16->width() + m_size22->width() + m_size32->width() + m_size48->width() + m_size64->width() + m_size128->width() + + (6+2) * iconView->spacing() + 20); + iconView->setMinimumHeight(m_size128->height() + 2 * iconView->spacing() + 20); + topLayout->addWidget(iconView); + switch (iconSize) { + case 16: iconView->setSelected(m_size16, true); m_iconSize = 16; break; + case 22: iconView->setSelected(m_size22, true); m_iconSize = 22; break; + default: + case 32: iconView->setSelected(m_size32, true); m_iconSize = 32; break; + case 48: iconView->setSelected(m_size48, true); m_iconSize = 48; break; + case 64: iconView->setSelected(m_size64, true); m_iconSize = 64; break; + case 128: iconView->setSelected(m_size128, true); m_iconSize = 128; break; + } + + connect( iconView, SIGNAL(executed(QIconViewItem*)), this, SLOT(choose(QIconViewItem*)) ); + connect( iconView, SIGNAL(returnPressed(QIconViewItem*)), this, SLOT(choose(QIconViewItem*)) ); + connect( iconView, SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged()) ); + + setMainWidget(page); +} + +IconSizeDialog::~IconSizeDialog() +{ +} + +void IconSizeDialog::slotSelectionChanged() +{ + // Change m_iconSize to the new selected one: + if (m_size16->isSelected()) { m_iconSize = 16; return; } + if (m_size22->isSelected()) { m_iconSize = 22; return; } + if (m_size32->isSelected()) { m_iconSize = 32; return; } + if (m_size48->isSelected()) { m_iconSize = 48; return; } + if (m_size64->isSelected()) { m_iconSize = 64; return; } + if (m_size128->isSelected()) { m_iconSize = 128; return; } + + // But if user unselected the item (by eg. right clicking a free space), reselect the last one: + switch (m_iconSize) { + case 16: m_size16->setSelected(true); m_iconSize = 16; break; + case 22: m_size22->setSelected(true); m_iconSize = 22; break; + default: + case 32: m_size32->setSelected(true); m_iconSize = 32; break; + case 48: m_size48->setSelected(true); m_iconSize = 48; break; + case 64: m_size64->setSelected(true); m_iconSize = 64; break; + case 128: m_size128->setSelected(true); m_iconSize = 128; break; + } +} + +void IconSizeDialog::choose(QIconViewItem*) +{ + actionButton(KDialogBase::Ok)->animateClick(); +} + +void IconSizeDialog::slotCancel() +{ + m_iconSize = -1; + KDialogBase::slotCancel(); +} + +/** class FontSizeCombo: */ + +FontSizeCombo::FontSizeCombo(bool rw, bool withDefault, QWidget *parent, const char *name) + : KComboBox(rw, parent, name), m_withDefault(withDefault) +{ + if (m_withDefault) + insertItem(i18n("(Default)")); + + QFontDatabase fontDB; + QValueList<int> sizes = fontDB.standardSizes(); + for (QValueList<int>::Iterator it = sizes.begin(); it != sizes.end(); ++it) + insertItem(QString::number(*it)); + +// connect( this, SIGNAL(acivated(const QString&)), this, SLOT(textChangedInCombo(const QString&)) ); + connect( this, SIGNAL(textChanged(const QString&)), this, SLOT(textChangedInCombo(const QString&)) ); + + // TODO: 01617 void KFontSizeAction::setFontSize( int size ) +} + +FontSizeCombo::~FontSizeCombo() +{ +} + +void FontSizeCombo::textChangedInCombo(const QString &text) +{ + bool ok = false; + int size = text.toInt(&ok); + if (ok) + emit sizeChanged(size); +} + +void FontSizeCombo::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_Escape) + emit escapePressed(); + else if (event->key() == Qt::Key_Return) + emit returnPressed2(); + else + KComboBox::keyPressEvent(event); +} + +void FontSizeCombo::setFontSize(int size) +{ + setCurrentText(QString::number(size)); + + // TODO: SEE KFontSizeAction::setFontSize( int size ) !!! for a more complete method! +} + +int FontSizeCombo::fontSize() +{ + bool ok = false; + int size = currentText().toInt(&ok); + if (ok) + return size; + + size = text(currentItem()).toInt(&ok); + if (ok) + return size; + + return font().pointSize(); +} + +#include "variouswidgets.moc" diff --git a/src/variouswidgets.h b/src/variouswidgets.h new file mode 100644 index 0000000..c579b72 --- /dev/null +++ b/src/variouswidgets.h @@ -0,0 +1,151 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef VARIOUSWIDGETS_H +#define VARIOUSWIDGETS_H + +#include <qwidget.h> +#include <kcombobox.h> +#include <qdialog.h> +#include <kurllabel.h> +#include <qstring.h> +#include <kdialogbase.h> + +class QLineEdit; +class KIconViewItem; +class QIconViewItem; + +class Basket; + +/** A widget to select a command to run, + * with a QLineEdit and a QPushButton. + * @author S�bastien Lao�t + */ +class RunCommandRequester : public QWidget +{ + Q_OBJECT + public: + RunCommandRequester(const QString &runCommand, const QString &message, QWidget *parent = 0, const char *name = 0); + ~RunCommandRequester(); + QString runCommand(); + void setRunCommand(const QString &runCommand); + QLineEdit *lineEdit() { return m_runCommand; } + private slots: + void slotSelCommand(); + private: + QLineEdit *m_runCommand; + QString m_message; +}; + +/** QComboBox to ask icon size + * @author S�bastien Lao�t + */ +class IconSizeCombo : public QComboBox +{ + Q_OBJECT + public: + IconSizeCombo(bool rw, QWidget *parent = 0, const char *name = 0); + ~IconSizeCombo(); + int iconSize(); + void setSize(int size); +}; + +/** A window that the user resize to graphically choose a new image size + * TODO: Create a SizePushButton or even SizeWidget + * @author S�bastien Lao�t + */ +class ViewSizeDialog : public QDialog +{ + Q_OBJECT + public: + ViewSizeDialog(QWidget *parent, int w, int h); + ~ViewSizeDialog(); + private: + virtual void resizeEvent(QResizeEvent *); + QWidget *m_sizeGrip; +}; + +/** A label displaying a link that, once clicked, offer a What's This messageBox to help users. + * @author S�bastien Lao�t + */ +class HelpLabel : public KURLLabel +{ + Q_OBJECT + public: + HelpLabel(const QString &text, const QString &message, QWidget *parent); + ~HelpLabel(); + QString message() { return m_message; } + public slots: + void setMessage(const QString &message) { m_message = message; } + void showMessage(); + protected: + void keyPressEvent(QKeyEvent *event); + private: + QString m_message; +}; + +/** A dialog to choose the size of an icon. + * @author S�bastien Lao�t + */ +class IconSizeDialog : public KDialogBase +{ + Q_OBJECT + public: + IconSizeDialog(const QString &caption, const QString &message, const QString &icon, int iconSize, QWidget *parent); + ~IconSizeDialog(); + int iconSize() { return m_iconSize; } /// << @return the choosen icon size (16, 32, ...) or -1 if canceled! + protected slots: + void slotCancel(); + void slotSelectionChanged(); + void choose(QIconViewItem*); + private: + KIconViewItem *m_size16; + KIconViewItem *m_size22; + KIconViewItem *m_size32; + KIconViewItem *m_size48; + KIconViewItem *m_size64; + KIconViewItem *m_size128; + int m_iconSize; +}; + +/** + * A missing class from KDE (and Qt): a combobox to select a font size! + */ +class FontSizeCombo : public KComboBox +{ + Q_OBJECT + public: + FontSizeCombo(bool rw, bool withDefault, QWidget *parent = 0, const char *name = 0); + ~FontSizeCombo(); + void setFontSize(int size); + int fontSize(); + protected: + void keyPressEvent(QKeyEvent *event); + signals: + void sizeChanged(int size); + void escapePressed(); + void returnPressed2(); + private slots: + void textChangedInCombo(const QString &text); + private: + bool m_withDefault; +}; + +#endif // VARIOUSWIDGETS_H diff --git a/src/xmlwork.cpp b/src/xmlwork.cpp new file mode 100644 index 0000000..92c2aca --- /dev/null +++ b/src/xmlwork.cpp @@ -0,0 +1,110 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#include <qstring.h> +#include <qdom.h> +#include <qstringlist.h> +#include <qfile.h> + +#include "xmlwork.h" + +QDomDocument* XMLWork::openFile(const QString &name, const QString &filePath) +{ + QDomDocument *doc = new QDomDocument(name); + QFile file(filePath); + if ( ! file.open(IO_ReadOnly) ) { + // QMessageBox::information(this, "Load an XML file", "Error : un-openable file"); + delete doc; + return 0; + } + if ( ! doc->setContent(&file) ) { + // QMessageBox::information(this, "Load an XML file", "Error : malformed content"); + file.close(); + delete doc; + return 0; + } + file.close(); + return doc; +} + +QDomElement XMLWork::getElement(const QDomElement &startElement, const QString &elementPath) +{ + QStringList elements = QStringList::split("/", elementPath, false); + QDomNode n = startElement.firstChild(); + for (unsigned int i = 0; i < elements.count(); ++i) { // For each elements + while ( ! n.isNull() ) { // Browse theire sub elements + QDomElement e = n.toElement(); // and search the good one + if ( (!e.isNull()) && e.tagName() == *elements.at(i) ) { // If found + if ( i + 1 == elements.count() ) // And if it is the asked element + return e; // Return the first corresponding + else { // Or if it is an intermediate element + n = e.firstChild(); // Continue with the next sub element + break; + } + } + n = n.nextSibling(); + } + } + return QDomElement(); // Not found ! +} + +QString XMLWork::getElementText(const QDomElement &startElement, const QString &elementPath, const QString &defaultTxt) +{ + QDomElement e = getElement(startElement, elementPath); + if (e.isNull()) + return defaultTxt; + else + return e.text(); +} + +void XMLWork::addElement(QDomDocument &document, QDomElement &parent, const QString &name, const QString &text) +{ + QDomElement tag = document.createElement(name); + parent.appendChild(tag); + QDomText content = document.createTextNode(text); + tag.appendChild(content); +} + +bool XMLWork::trueOrFalse(const QString &value, bool defaultValue) +{ + if ( value == "true" || value == "1" || value == "on" || value == "yes" ) + return true; + if ( value == "false" || value == "0" || value == "off" || value == "no" ) + return false; + return defaultValue; +} + +QString XMLWork::trueOrFalse(bool value) +{ + return value ? "true" : "false"; +} + +QString XMLWork::innerXml(QDomElement &element) +{ + QString inner; + for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) + if (n.isCharacterData()) + inner += n.toCharacterData().data(); + else if (n.isElement()) { + QDomElement e = n.toElement(); + inner += "<" + e.tagName() + ">" + innerXml(e) + "</" + e.tagName() + ">"; + } + return inner; +} diff --git a/src/xmlwork.h b/src/xmlwork.h new file mode 100644 index 0000000..e4d1cae --- /dev/null +++ b/src/xmlwork.h @@ -0,0 +1,45 @@ +/*************************************************************************** + * Copyright (C) 2003 by S�bastien Lao�t * + * [email protected] * + * * + * 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, Fifth Floor, Boston, MA 02110-1301, USA. * + ***************************************************************************/ + +#ifndef XMLWORKXMLWORK_H +#define XMLWORKXMLWORK_H + +class QString; +class QDomDocument; +class QDomElement; + +/** All related functions to manage XML files and trees + * @author S�bastien Lao�t + */ +namespace XMLWork +{ + // Manage XML files : + QDomDocument* openFile(const QString &name, const QString &filePath); + // Manage XML trees : + QDomElement getElement(const QDomElement &startElement, const QString &elementPath); + QString getElementText(const QDomElement &startElement, const QString &elementPath, const QString &defaultTxt = ""); + void addElement(QDomDocument &document, QDomElement &parent, const QString &name, const QString &text); + QString innerXml(QDomElement &element); + // Not directly related to XML : + bool trueOrFalse(const QString &value, bool defaultValue = true); + QString trueOrFalse(bool value); +} + +#endif // XMLWORKXMLWORK_H |