diff --git a/.gitignore b/.gitignore new file mode 100644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.kdev4 +*.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ find_package(KdepimLibs REQUIRED) find_package(Boost REQUIRED) find_package(Baloo REQUIRED) +find_package(Grantlee REQUIRED) macro_log_feature(KDEPIMLIBS_FOUND "kdepimlibs" "KDE PIM libraries" "The KDE PIM libs are required to build Zanshin" "http://www.kde.org") include(KDE4Defaults) diff --git a/Messages.sh b/Messages.sh --- a/Messages.sh +++ b/Messages.sh @@ -1,5 +1,6 @@ #! /usr/bin/env bash +scripts/extract_strings_gettext.py `find . -name \*.html` >> html.cpp $EXTRACTRC `find . -name \*.rc -o -name \*.ui` >> rc.cpp $XGETTEXT `find . -name \*.h -o -name \*.cpp | grep -v '/tests/'` -o $podir/zanshin.pot $XGETTEXT_QT `find . -name \*.h -o -name \*.cpp | grep -v '/tests/'` -j -o $podir/zanshin.pot -rm -f rc.cpp +rm -f rc.cpp html.cpp diff --git a/scripts/extract_strings_gettext.py b/scripts/extract_strings_gettext.py new file mode 100755 --- /dev/null +++ b/scripts/extract_strings_gettext.py @@ -0,0 +1,47 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +## +# Copyright 2010 Stephen Kelly +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## + + +from strings_extractor_gettext import GettextExtractStrings +import os, sys, glob, operator + +if __name__ == "__main__": + ex = GettextExtractStrings() + + outputfile = sys.stdout + + files = reduce(operator.add, map(glob.glob, sys.argv[1:])) + + for filename in files: + f = open(filename, "r") + ex.translate(f, outputfile) + + outputfile.write("\n") + + + diff --git a/scripts/strings_extractor.py b/scripts/strings_extractor.py new file mode 100755 --- /dev/null +++ b/scripts/strings_extractor.py @@ -0,0 +1,365 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +## +# Copyright 2010,2011 Stephen Kelly +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## + +## Parts of this file are reproduced from the Django framework. The Django licence appears below. + +## +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of Django nor the names of its contributors may be used +# to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## + +import re +import os.path + +# == Introduction to the template syntax == +# +# The template syntax looks like this: +# (For more see here: http://grantlee.org/apidox/for_themers.html ) +# +# This is plain text +# This is text with a {{ value }} substitution +# This is {% if condition_is_met %}a conditional{% endif %} +# {# This is a comment #} +# This is a {% comment %} multi-line +# comment +# {% endcomment %} +# +# That is, we have plain text. +# We have value substitution with {{ }} +# We have comments with {# #} +# We have control tags with {% %} +# +# The first token inside {% %} syntax is called a tag name. Above, we have +# an if tag and a comment tag. +# +# The 'value' in {{ value }} is called a filter expression. In the above case +# the filter expression is a simple value which was inserted into the context. +# In other cases it can be {{ value|upper }}, that is the value can be passed +# through a filter called 'upper' with the '|', or filter expression can +# be {{ value|join:"-" }}, that is it can be passed through the join filter +# which takes an argument. In this case, the 'value' would actually be a list, +# and the join filter would concatenate them with a dash. A filter can have +# either no arguments, like upper, or it can take one argument, delimited by +# a colon (';'). A filter expression can consist of a value followed by a +# chain of filters, such as {{ value|join:"-"|upper }}. A filter expression +# can appear one time inside {{ }} but may appear multiple times inside {% %} +# For example {% cycle foo|upper bar|join:"-" bat %} contains 3 filter +# expressions, 'foo|upper', 'bar|join:"-"' and 'bat'. +# +# Comments are ignored in the templates. +# +# == i18n in templates == +# +# The purpose of this script is to extract translatable strings from templates +# The aim is to allow template authors to write templates like this: +# +# This is a {{ _("translatable string") }} in the template. +# This is a {% i18n "translatable string about %1" something %} +# This is a {% i18nc "Some context information" "string about %1" something %} +# This is a {% i18np "%1 string about %2" numthings something %} +# This is a {% i18ncp "some context" "%1 string about %2" numthings something %} +# +# That is, simple translation with _(), and i18n* tags to allow for variable +# substitution, context messages and plurals. Translatable strings may appear +# in a filter expression, either as the value begin filtered, or as the argument +# or both: +# +# {{ _("hello")|upper }} +# {{ list|join:_("and") }} +# +# == How the strings are extracted == +# +# The strings are extracted by parsing the template with regular expressions. +# The tag_re regular expression breaks the template into a stream of tokens +# containing plain text, {{ values }} and {% tags %}. +# That work is done by the tokenize method with the create_token method. +# Each token is then processed to extract the translatable strings from +# the filter expressions. + + +# The original context of much of this script is in the django template system: +# http://code.djangoproject.com/browser/django/trunk/django/template/base.py + + +TOKEN_TEXT = 0 +TOKEN_VAR = 1 +TOKEN_BLOCK = 2 +TOKEN_COMMENT = 3 + +# template syntax constants +FILTER_SEPARATOR = '|' +FILTER_ARGUMENT_SEPARATOR = ':' +BLOCK_TAG_START = '{%' +BLOCK_TAG_END = '%}' +VARIABLE_TAG_START = '{{' +VARIABLE_TAG_END = '}}' +COMMENT_TAG_START = '{#' +COMMENT_TAG_END = '#}' + +# match a variable or block tag and capture the entire tag, including start/end delimiters +tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), + re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END))) + + +# Expression to match some_token and some_token="with spaces" (and similarly +# for single-quoted strings). +smart_split_re = re.compile(r""" + ((?: + [^\s'"]* + (?: + (?:"(?:[^"\\]|\\.)*" | '(?:[^'\\]|\\.)*') + [^\s'"]* + )+ + ) | \S+) +""", re.VERBOSE) + +def smart_split(text): + r""" + Generator that splits a string by spaces, leaving quoted phrases together. + Supports both single and double quotes, and supports escaping quotes with + backslashes. In the output, strings will keep their initial and trailing + quote marks and escaped quotes will remain escaped (the results can then + be further processed with unescape_string_literal()). + + >>> list(smart_split(r'This is "a person\'s" test.')) + [u'This', u'is', u'"a person\\\'s"', u'test.'] + >>> list(smart_split(r"Another 'person\'s' test.")) + [u'Another', u"'person\\'s'", u'test.'] + >>> list(smart_split(r'A "\"funky\" style" test.')) + [u'A', u'"\\"funky\\" style"', u'test.'] + """ + for bit in smart_split_re.finditer(text): + yield bit.group(0) + + +# This only matches constant *strings* (things in quotes or marked for +# translation). + +constant_string = r"(?:%(strdq)s|%(strsq)s)" % { + 'strdq': r'"[^"\\]*(?:\\.[^"\\]*)*"', # double-quoted string + 'strsq': r"'[^'\\]*(?:\\.[^'\\]*)*'", # single-quoted string + } + +filter_raw_string = r"""^%(i18n_open)s(?P%(constant_string)s)%(i18n_close)s""" % { + 'constant_string': constant_string, + 'i18n_open' : re.escape("_("), + 'i18n_close' : re.escape(")"), + } + +filter_re = re.compile(filter_raw_string, re.UNICODE|re.VERBOSE) + +class TemplateSyntaxError(Exception): + pass + +class TranslatableString: + _string = '' + context = '' + plural = '' + + def __repr__(self): + return "String('%s', '%s', '%s')" % (self._string, self.context, self.plural) + +class Token(object): + def __init__(self, token_type, contents): + # token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or TOKEN_COMMENT. + self.token_type, self.contents = token_type, contents + + def __str__(self): + return '<%s token: "%s...">' % \ + ({TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block', TOKEN_COMMENT: 'Comment'}[self.token_type], + self.contents[:20].replace('\n', '')) + +def create_token(token_string, in_tag): + """ + Convert the given token string into a new Token object and return it. + If in_tag is True, we are processing something that matched a tag, + otherwise it should be treated as a literal string. + """ + if in_tag: + if token_string.startswith(VARIABLE_TAG_START): + token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) + elif token_string.startswith(BLOCK_TAG_START): + token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) + elif token_string.startswith(COMMENT_TAG_START): + token = Token(TOKEN_COMMENT, '') + else: + token = Token(TOKEN_TEXT, token_string) + return token + +def tokenize(template_string): + + in_tag = False + result = [] + for bit in tag_re.split(template_string): + if bit: + result.append(create_token(bit, in_tag)) + in_tag = not in_tag + return result + +class TranslationOutputter: + translatable_strings = [] + + def get_translatable_filter_args(self, token): + """ + Find the filter expressions in token and extract the strings in it. + """ + matches = filter_re.finditer(token) + upto = 0 + var_obj = False + for match in matches: + l10nable = match.group("l10nable") + + if l10nable: + # Make sure it's a quoted string + if l10nable.startswith('"') and l10nable.endswith('"') \ + or l10nable.startswith("'") and l10nable.endswith("'"): + ts = TranslatableString() + ts._string = l10nable[1:-1] + self.translatable_strings.append(ts) + + def get_contextual_strings(self, token): + split = [] + _bits = smart_split(token.contents) + _bit = _bits.next() + if _bit =="i18n" or _bit == "i18n_var": + # {% i18n "A one %1, a two %2, a three %3" var1 var2 var3 %} + # {% i18n_var "A one %1, a two %2, a three %3" var1 var2 var3 as result %} + _bit = _bits.next() + if not _bit.startswith("'") and not _bit.startswith('"'): + return + + sentinal = _bit[0] + if not _bit.endswith(sentinal): + return + + translatable_string = TranslatableString() + translatable_string._string = _bit[1:-1] + self.translatable_strings.append(translatable_string) + elif _bit =="i18nc" or _bit == "i18nc_var": + # {% i18nc "An email send operation failed." "%1 Failed!" var1 %} + # {% i18nc_var "An email send operation failed." "%1 Failed!" var1 as result %} + _bit = _bits.next() + if not _bit.startswith("'") and not _bit.startswith('"'): + return + + sentinal = _bit[0] + if not _bit.endswith(sentinal): + return + + translatable_string = TranslatableString() + translatable_string.context = _bit[1:-1] + _bit = _bits.next() + translatable_string._string = _bit[1:-1] + self.translatable_strings.append(translatable_string) + elif _bit =="i18np" or _bit =="i18np_var": + # {% i18np "An email send operation failed." "%1 email send operations failed. Error : % 2." count count errorMsg %} + # {% i18np_var "An email send operation failed." "%1 email send operations failed. Error : % 2." count count errorMsg as result %} + _bit = _bits.next() + if not _bit.startswith("'") and not _bit.startswith('"'): + return + + sentinal = _bit[0] + if not _bit.endswith(sentinal): + return + + translatable_string = TranslatableString() + translatable_string._string = _bit[1:-1] + _bit = _bits.next() + translatable_string.plural = _bit[1:-1] + self.translatable_strings.append(translatable_string) + elif _bit =="i18ncp" or _bit =="i18ncp_var": + # {% i18np "The user tried to send an email, but that failed." "An email send operation failed." "%1 email send operation failed." count count %} + # {% i18np_var "The user tried to send an email, but that failed." "An email send operation failed." "%1 email send operation failed." count count as result %} + + _bit = _bits.next() + if not _bit.startswith("'") and not _bit.startswith('"'): + return + + sentinal = _bit[0] + if not _bit.endswith(sentinal): + return + + translatable_string = TranslatableString() + translatable_string.context = _bit[1:-1] + _bit = _bits.next() + translatable_string._string = _bit[1:-1] + _bit = _bits.next() + translatable_string.plural = _bit[1:-1] + self.translatable_strings.append(translatable_string) + else: + return + + for _bit in _bits: + + if (_bit == "as"): + return + self.get_translatable_filter_args(_bit) + + def get_plain_strings(self, token): + split = [] + bits = iter(smart_split(token.contents)) + for bit in bits: + self.get_translatable_filter_args(bit) + + def translate(self, template_file, outputfile): + template_string = template_file.read() + self.translatable_strings = [] + for token in tokenize(template_string): + if token.token_type == TOKEN_VAR or token.token_type == TOKEN_BLOCK: + self.get_plain_strings(token) + if token.token_type == TOKEN_BLOCK: + self.get_contextual_strings(token) + self.createOutput(os.path.relpath(template_file.name), self.translatable_strings, outputfile) + + def createOutput(self, template_filename, translatable_strings, outputfile): + pass diff --git a/scripts/strings_extractor_gettext.py b/scripts/strings_extractor_gettext.py new file mode 100755 --- /dev/null +++ b/scripts/strings_extractor_gettext.py @@ -0,0 +1,46 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +## +# Copyright 2010 Stephen Kelly +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## + +from strings_extractor import TranslationOutputter + +class GettextExtractStrings(TranslationOutputter): + def createOutput(self, template_filename, context_strings, outputfile): + for context_string in context_strings: + outputfile.write("// i18n: file: " + template_filename + "\n") + if context_string.context: + if not context_string.plural: + outputfile.write("pgettext(\"" + context_string.context + "\", \"" + context_string._string + "\");\n") + else: + outputfile.write("npgettext(\"" + context_string.context + "\", \"" + context_string._string + "\", \"" + context_string.plural + "\");\n") + else: + if context_string.plural: + outputfile.write("ngettext(\"" + context_string._string + "\", \"" + context_string.plural + "\");\n") + else: + outputfile.write("gettext(\"" + context_string._string + "\");\n") + + diff --git a/src/domain/task.h b/src/domain/task.h --- a/src/domain/task.h +++ b/src/domain/task.h @@ -197,6 +197,7 @@ Q_OBJECT Q_PROPERTY(QDateTime startDate READ startDate WRITE setStartDate NOTIFY startDateChanged) Q_PROPERTY(QDateTime dueDate READ dueDate WRITE setDueDate NOTIFY dueDateChanged) + Q_PROPERTY(bool done READ isDone) Q_PROPERTY(Domain::Task::Delegate delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) Q_PROPERTY(Domain::Recurrence::Ptr recurrence READ recurrence WRITE setRecurrence NOTIFY recurrenceChanged) Q_PROPERTY(Domain::Alarm::List alarms READ alarms WRITE setAlarms NOTIFY alarmsChanged) diff --git a/src/kontact/notes_part.cpp b/src/kontact/notes_part.cpp --- a/src/kontact/notes_part.cpp +++ b/src/kontact/notes_part.cpp @@ -66,14 +66,17 @@ splitter->addWidget(components->editorView()); setWidget(splitter); + setXMLFile(KStandardDirs::locate("data", "zanshin/zanshin-notes_part.rc")); + foreach (QAction *action, components->configureActions()) { actionCollection()->addAction(action->objectName(), action); } foreach (QAction *action, components->editorView()->actions()) { actionCollection()->addAction(action->objectName(), action); } - - setXMLFile(KStandardDirs::locate("data", "zanshin/zanshin-notes_part.rc")); + foreach (QAction *action, components->pageView()->actions()) { + actionCollection()->addAction(action->objectName(), action); + } } NotesPart::~NotesPart() diff --git a/src/kontact/tasks_part.cpp b/src/kontact/tasks_part.cpp --- a/src/kontact/tasks_part.cpp +++ b/src/kontact/tasks_part.cpp @@ -66,14 +66,18 @@ splitter->addWidget(components->editorView()); setWidget(splitter); + setXMLFile(KStandardDirs::locate("data", "zanshin/zanshin-tasks_part.rc")); + foreach (QAction *action, components->configureActions()) { actionCollection()->addAction(action->objectName(), action); } foreach (QAction *action, components->editorView()->actions()) { actionCollection()->addAction(action->objectName(), action); } + foreach (QAction *action, components->pageView()->actions()) { + actionCollection()->addAction(action->objectName(), action); + } - setXMLFile(KStandardDirs::locate("data", "zanshin/zanshin-tasks_part.rc")); } TasksPart::~TasksPart() diff --git a/src/kontact/zanshin-notes_part.rc b/src/kontact/zanshin-notes_part.rc --- a/src/kontact/zanshin-notes_part.rc +++ b/src/kontact/zanshin-notes_part.rc @@ -1,6 +1,10 @@ + + File + + Configure Zanshin diff --git a/src/kontact/zanshin-tasks_part.rc b/src/kontact/zanshin-tasks_part.rc --- a/src/kontact/zanshin-tasks_part.rc +++ b/src/kontact/zanshin-tasks_part.rc @@ -1,6 +1,10 @@ + + File + + Configure Zanshin diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -14,6 +14,7 @@ newpagedialog.cpp newpagedialoginterface.cpp pageview.cpp + printwidget.cpp ) qt4_wrap_ui(widgets_SRCS @@ -22,5 +23,14 @@ newpagedialog.ui ) +include_directories( + ${Grantlee_INCLUDE_DIRS} + ${CMAKE_CURRENT_BINARY_DIR} +) + +configure_file(grantlee_paths.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/grantlee_paths.h) + kde4_add_library(widgets STATIC ${widgets_SRCS}) -target_link_libraries(widgets ${QT4_QTGUI_LIBRARY} presentation ${KDEPIM_STATIC_LIBS}) +target_link_libraries(widgets ${QT4_QTGUI_LIBRARY} presentation ${KDEPIM_STATIC_LIBS} ${Grantlee_CORE_LIBRARIES}) + +add_subdirectory(templates) \ No newline at end of file diff --git a/src/widgets/grantlee_paths.h.cmake b/src/widgets/grantlee_paths.h.cmake new file mode 100644 --- /dev/null +++ b/src/widgets/grantlee_paths.h.cmake @@ -0,0 +1,2 @@ +#define GRANTLEE_TEMPLATE_PATH "@DATA_INSTALL_DIR@/zanshin/templates" + diff --git a/src/widgets/pageview.h b/src/widgets/pageview.h --- a/src/widgets/pageview.h +++ b/src/widgets/pageview.h @@ -70,6 +70,7 @@ void onEditingFinished(); void onRemoveItemRequested(); void onCurrentChanged(const QModelIndex ¤t); + void onPrint(); private: QObject *m_model; diff --git a/src/widgets/pageview.cpp b/src/widgets/pageview.cpp --- a/src/widgets/pageview.cpp +++ b/src/widgets/pageview.cpp @@ -23,17 +23,22 @@ #include "pageview.h" +#include "printwidget.h" #include #include #include +#include +#include #include #include #include #include #include #include +#include #include +#include #include "filterwidget.h" #include "itemdelegate.h" @@ -134,6 +139,13 @@ addAction(removeItemAction); m_messageBoxInterface = MessageBox::Ptr::create(); + + auto menuAction = new QAction(0); + menuAction->setText(tr("Print")); + menuAction->setShortcut(QKeySequence::Print); + menuAction->setObjectName("print"); + connect(menuAction, SIGNAL(triggered()), SLOT(onPrint())); + addAction(menuAction); } QObject *PageView::model() const @@ -144,7 +156,9 @@ void PageView::configurePopupMenu(QMenu *menu, const Domain::Artifact::Ptr &artifact) { for (auto action : actions()) { - if (action->objectName() == "removeAction") { + if (action->objectName() == "print") { // Don't show print in the context menu + continue; + } else if (action->objectName() == "removeAction") { if (artifact) { menu->addAction(action); } @@ -183,6 +197,33 @@ m_centralView->expandAll(); } +void PageView::onPrint() +{ + QString docName = tr("Tasks and notes"); + QString title = tr(" Print tasks and notes"); + if (m_mode == TasksOnly) { + docName = tr("Tasks"); + title = tr("Print Tasks"); + } else if (m_mode == NotesOnly) { + docName = tr("Notes"); + title = tr("Print Notes"); + } + QPrinter printer; + printer.setDocName(docName); + printer.setOutputFormat(QPrinter::PdfFormat); + printer.setCollateCopies(true); + + QPrintDialog printDialog(KdePrint::createPrintDialog(&printer)); + + printDialog.setWindowTitle(title); + if (printDialog.exec() != QDialog::Accepted) { + return; + } + + PrintWidget widget(this); + widget.print(m_filterWidget->proxyModel(), printDialog.printer()); +} + MessageBoxInterface::Ptr PageView::messageBoxInterface() const { return m_messageBoxInterface; diff --git a/src/widgets/pageview.h b/src/widgets/printwidget.h copy from src/widgets/pageview.h copy to src/widgets/printwidget.h --- a/src/widgets/pageview.h +++ b/src/widgets/printwidget.h @@ -1,6 +1,6 @@ /* This file is part of Zanshin - Copyright 2014 Kevin Ottens + Copyright 2015 Sandro Knauß This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as @@ -21,9 +21,8 @@ USA. */ - -#ifndef WIDGETS_PAGEVIEW_H -#define WIDGETS_PAGEVIEW_H +#ifndef WIDGETS_PRINTWIDGET_H +#define WIDGETS_PRINTWIDGET_H #include @@ -32,19 +31,30 @@ #include #include "domain/artifact.h" -#include "messageboxinterface.h" +#include "domain/task.h" class QLineEdit; class QModelIndex; class QTreeView; class QMessageBox; class QMenu; +class QTextDocument; +class QTextEdit; +class QTimer; +class QPrinter; + +namespace Akonadi { + class TaskRepository; + class TaskQueries; +} -namespace Widgets { +namespace Grantlee { + class Engine; +} -class FilterWidget; +namespace Widgets { -class PageView : public QWidget +class PrintWidget : public QWidget { Q_OBJECT public: @@ -53,33 +63,38 @@ NotesOnly, TasksAndNotes }; - explicit PageView(QWidget *parent = 0, ApplicationMode mode = TasksAndNotes); + explicit PrintWidget(QWidget *parent = 0, ApplicationMode mode = TasksAndNotes); QObject *model() const; - MessageBoxInterface::Ptr messageBoxInterface() const; -public slots: - void setModel(QObject *model); - void setMessageBoxInterface(const MessageBoxInterface::Ptr &interface); - void configurePopupMenu(QMenu *menu, const Domain::Artifact::Ptr &artifact); + void print(QPrinter *printer); signals: - void currentArtifactChanged(const Domain::Artifact::Ptr &artifact); + void init(); -private slots: - void onEditingFinished(); - void onRemoveItemRequested(); - void onCurrentChanged(const QModelIndex ¤t); +public slots: + void setModel(QObject *model); + void print(QObject *model, QPrinter *printer); private: QObject *m_model; - FilterWidget *m_filterWidget; - QTreeView *m_centralView; - QLineEdit *m_quickAddEdit; - MessageBoxInterface::Ptr m_messageBoxInterface; + QTextDocument *m_document; + QTextEdit *m_edit; + QTimer *m_timer; ApplicationMode m_mode; + bool m_init; + + Akonadi::TaskRepository *m_repository; + Akonadi::TaskQueries *m_queries; + Grantlee::Engine *m_engine; +private slots: + void render(); + void delayedInit(); + +private: + QString renderItem(const Domain::Task::Ptr &task); }; } -#endif // WIDGETS_PAGEVIEW_H +#endif // WIDGETS_PRINTWIDGET_H diff --git a/src/widgets/printwidget.cpp b/src/widgets/printwidget.cpp new file mode 100644 --- /dev/null +++ b/src/widgets/printwidget.cpp @@ -0,0 +1,156 @@ +/* This file is part of Zanshin + + Copyright 2015 Sandro Knauß + + 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) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + 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 "printwidget.h" + +#include +#include +#include +#include +#include + +#include + +#include "presentation/artifactfilterproxymodel.h" +#include "presentation/metatypes.h" + +#include "akonadi/akonaditaskqueries.h" +#include "akonadi/akonaditaskrepository.h" +#include "presentation/tasklistmodel.h" +#include + +#include "grantlee_paths.h" +#include "grantlee_core.h" + +using namespace Widgets; + +PrintWidget::PrintWidget(QWidget *parent, ApplicationMode mode) + : QWidget(parent), + m_model(0), + m_mode(mode), + m_document(new QTextDocument(this)), + m_edit(new QTextEdit()), + m_timer(new QTimer()), + m_repository(new Akonadi::TaskRepository), + m_queries(new Akonadi::TaskQueries), + m_engine(0), + m_init(false) +{ + m_edit->setDocument(m_document); + + connect(m_timer, SIGNAL(timeout()), SLOT(render())); + m_timer->setInterval(2000); + + auto layout = new QVBoxLayout; + layout->addWidget(m_edit); + setLayout(layout); + + QTimer::singleShot(0, this, SLOT(delayedInit())); +} + +void PrintWidget::delayedInit() +{ + m_engine = new Grantlee::Engine(this); + m_engine->addDefaultLibrary( "grantlee_i18n" ); + m_engine->addDefaultLibrary( "grantlee_scriptabletags" ); + + Grantlee::FileSystemTemplateLoader::Ptr loader = Grantlee::FileSystemTemplateLoader::Ptr(new Grantlee::FileSystemTemplateLoader()); + loader->setTemplateDirs(QStringList() << GRANTLEE_TEMPLATE_PATH); + m_engine->addTemplateLoader(loader); + + m_init = true; + emit init(); +} + +QObject *PrintWidget::model() const +{ + return m_model; +} + +void PrintWidget::setModel(QObject *model) +{ + if (model == m_model) + return; + + m_model = model; + + if (!m_model) { + m_timer->stop(); + return; + } + m_timer->start(); +} + +void PrintWidget::render() +{ + if (!m_model) { + return; + } + + auto model = static_cast(m_model); + + QString html; + + QVariantList artifacts; + + for(int i=0; irowCount(); i++) { + auto index = model->index(i,0); + auto data = index.data(Presentation::QueryTreeModelBase::ObjectRole); + if (!data.isValid()) + continue; + + auto artifact = data.value(); + if (!artifact) + continue; + artifacts << QVariant::fromValue(static_cast(artifact.data())); + } + + Grantlee::Template t = m_engine->loadByName("main.html"); + if (t->error()) { + qWarning() << t->errorString(); + return; + } + Grantlee::Context c; + c.insert("anz", artifacts.count()); + c.insert("tasks", artifacts); + + html += t->render(&c); + m_document->setHtml(html); +} + +void PrintWidget::print(QPrinter *printer) +{ + render(); + m_document->print(printer); +} + +void PrintWidget::print(QObject *model, QPrinter *printer) +{ + if (!m_init) { + QEventLoop loop; + connect(this, SIGNAL(init()), &loop, SLOT(quit())); + loop.exec(); + } + setModel(model); + print(printer); +} diff --git a/src/widgets/templates/CMakeLists.txt b/src/widgets/templates/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/widgets/templates/CMakeLists.txt @@ -0,0 +1,4 @@ +FILE(GLOB files "${CMAKE_CURRENT_SOURCE_DIR}/*.html") +install(FILES ${files} + DESTINATION ${DATA_INSTALL_DIR}/zanshin/templates +) \ No newline at end of file diff --git a/src/widgets/templates/main.html b/src/widgets/templates/main.html new file mode 100644 --- /dev/null +++ b/src/widgets/templates/main.html @@ -0,0 +1,16 @@ + +

{% i18n "You have %1 Tasks" anz %}

+ +{% for task in tasks %} +
+

{{ task.title }} {% if task.done %}---done---{% endif %}

+{% if task.dueDate %} +{% i18n "Due to:" %} {{ task.dueDate|date }} +{% endif %} +{% if task.text %} +

{{ task.text|safe }}

+{% endif %} +
+{% endfor %} + +