[tryton-debian-vcs] goocalendar branch upstream created. 1ec25723a1daecd5adf16c3352fd4ba789d79cc2
Mathias Behrle
tryton-debian-vcs at alioth.debian.org
Wed Nov 27 16:48:09 UTC 2013
The following commit has been merged in the upstream branch:
https://alioth.debian.org/plugins/scmgit/cgi-bin/gitweb.cgi/?p=tryton/goocalendar.git;a=commitdiff;h=1ec25723a1daecd5adf16c3352fd4ba789d79cc2
commit 1ec25723a1daecd5adf16c3352fd4ba789d79cc2
Author: Mathias Behrle <mathiasb at m9s.biz>
Date: Tue Oct 15 18:51:33 2013 +0200
Adding upstream version 0.1.
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644
index 0000000..416204d
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,2 @@
+Version 0.1 - 2013-02-18
+* Initial release
diff --git a/COPYRIGHT b/COPYRIGHT
new file mode 100644
index 0000000..2e334b4
--- /dev/null
+++ b/COPYRIGHT
@@ -0,0 +1,16 @@
+Copyright (C) 2012 Antoine Smolders
+Copyright (C) 2012-2013 Cédric Krier
+Copyright (C) 2007 Samuel Abels <http://debain.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2, as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
diff --git a/GooCalendar.egg-info/PKG-INFO b/GooCalendar.egg-info/PKG-INFO
new file mode 100644
index 0000000..c9fd974
--- /dev/null
+++ b/GooCalendar.egg-info/PKG-INFO
@@ -0,0 +1,28 @@
+Metadata-Version: 1.1
+Name: GooCalendar
+Version: 0.1
+Summary: A calendar widget for GTK using PyGoocanvas
+Home-page: http://code.google.com/p/goocalendar/
+Author: Cédric Krier
+Author-email: cedric.krier at b2ck.com
+License: GPL-2
+Download-URL: http://code.google.com/p/goocalendar/downloads/
+Description: GooCalendar
+ ===========
+
+ A calendar widget for GTK using PyGoocanvas
+
+ Nutshell
+ --------
+
+ .. TODO
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: X11 Applications :: GTK
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 2 :: Only
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Widget Sets
diff --git a/GooCalendar.egg-info/SOURCES.txt b/GooCalendar.egg-info/SOURCES.txt
new file mode 100644
index 0000000..cce47d8
--- /dev/null
+++ b/GooCalendar.egg-info/SOURCES.txt
@@ -0,0 +1,18 @@
+CHANGELOG
+COPYRIGHT
+INSTALL
+LICENSE
+MANIFEST.in
+README
+setup.py
+GooCalendar.egg-info/PKG-INFO
+GooCalendar.egg-info/SOURCES.txt
+GooCalendar.egg-info/dependency_links.txt
+GooCalendar.egg-info/top_level.txt
+doc/Makefile
+doc/conf.py
+doc/index.rst
+goocalendar/__init__.py
+goocalendar/_calendar.py
+goocalendar/_event.py
+goocalendar/util.py
\ No newline at end of file
diff --git a/GooCalendar.egg-info/dependency_links.txt b/GooCalendar.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/GooCalendar.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/GooCalendar.egg-info/top_level.txt b/GooCalendar.egg-info/top_level.txt
new file mode 100644
index 0000000..cdeff5e
--- /dev/null
+++ b/GooCalendar.egg-info/top_level.txt
@@ -0,0 +1 @@
+goocalendar
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..281d620
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,26 @@
+Installing GooCalendar
+======================
+
+Prerequisites
+-------------
+
+ * Python (http://www.python.org/)
+ * PyGoocanvas (https://live.gnome.org/PyGoocanvas)
+
+Installation
+------------
+
+Once you've downloaded and unpacked the GooCalendar source release, enter the
+directory where the archive was unpacked, and run:
+
+ python setup.py install
+
+Note that you may need administrator/root privileges for this step, as
+this command will by default attempt to install module to the Python
+site-packages directory on your system.
+
+For advanced options, please refer to the easy_install and/or the distutils
+documentation:
+
+ http://peak.telecommunity.com/DevCenter/EasyInstall
+ http://docs.python.org/inst/inst.html
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d159169
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..884bd41
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,6 @@
+include LICENSE
+include COPYRIGHT
+include README
+include CHANGELOG
+include INSTALL
+include doc/*
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..c9fd974
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,28 @@
+Metadata-Version: 1.1
+Name: GooCalendar
+Version: 0.1
+Summary: A calendar widget for GTK using PyGoocanvas
+Home-page: http://code.google.com/p/goocalendar/
+Author: Cédric Krier
+Author-email: cedric.krier at b2ck.com
+License: GPL-2
+Download-URL: http://code.google.com/p/goocalendar/downloads/
+Description: GooCalendar
+ ===========
+
+ A calendar widget for GTK using PyGoocanvas
+
+ Nutshell
+ --------
+
+ .. TODO
+
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: X11 Applications :: GTK
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 2 :: Only
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Widget Sets
diff --git a/README b/README
new file mode 100644
index 0000000..2eb50c8
--- /dev/null
+++ b/README
@@ -0,0 +1,9 @@
+GooCalendar
+===========
+
+A calendar widget for GTK using PyGoocanvas
+
+Nutshell
+--------
+
+.. TODO
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..7ebd347
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GooCalendar.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GooCalendar.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/GooCalendar"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GooCalendar"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..4d5ab0e
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,284 @@
+# -*- coding: utf-8 -*-
+#
+# GooCalendar documentation build configuration file, created by
+# sphinx-quickstart on Mon Aug 13 13:24:40 2012.
+#
+# This file is execfile()d with the current directory set to its containing dir
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration ----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+#source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'GooCalendar'
+copyright = u'2012, Samuel Abels, Cédric Krier, Antoine Smolders'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.1'
+# The full version, including alpha/beta/rc tags.
+release = '0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output --------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+#html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'GooCalendardoc'
+
+
+# -- Options for LaTeX output -------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual])
+latex_documents = [
+ ('index', 'GooCalendar.tex', u'GooCalendar Documentation',
+ u'Samuel Abels, Cédric Krier, Antoine Smolders', 'manual'),
+ ]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output -------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'goocalendar', u'GooCalendar Documentation',
+ [u'Samuel Abels, Cédric Krier, Antoine Smolders'], 1)
+ ]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output -----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'GooCalendar', u'GooCalendar Documentation',
+ u'Samuel Abels, Cédric Krier, Antoine Smolders', 'GooCalendar',
+ 'One line description of project.',
+ 'Miscellaneous'),
+ ]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+
+# -- Options for Epub output --------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = u'GooCalendar'
+epub_author = u'Samuel Abels, Cédric Krier, Antoine Smolders'
+epub_publisher = u'Samuel Abels, Cédric Krier, Antoine Smolders'
+epub_copyright = u'2012, Samuel Abels, Cédric Krier, Antoine Smolders'
+
+# The language of the text. It defaults to the language option
+# or en if the language is not set.
+#epub_language = ''
+
+# The scheme of the identifier. Typical schemes are ISBN or URL.
+#epub_scheme = ''
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#epub_identifier = ''
+
+# A unique identification for the text.
+#epub_uid = ''
+
+# A tuple containing the cover image and cover page html template filenames.
+#epub_cover = ()
+
+# HTML files that should be inserted before the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_pre_files = []
+
+# HTML files shat should be inserted after the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_post_files = []
+
+# A list of files that should not be packed into the epub file.
+#epub_exclude_files = []
+
+# The depth of the table of contents in toc.ncx.
+#epub_tocdepth = 3
+
+# Allow duplicate toc entries.
+#epub_tocdup = True
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..76b8c97
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,420 @@
+:mod:`goocalendar` --- Calendar widget using GooCanvas
+======================================================
+
+.. module:: goocalendar
+ :synopsis: Calendar widget using GooCanvas
+.. moduleauthor:: Samuel Abels <http://debain.org>
+.. moduleauthor:: Cedric Krier <cedric.krier at b2ck.com>
+.. moduleauthor:: Antoine Smolders <smoldersan at gmail.com>
+.. sectionauthor:: Antoine Smolders <smoldersan at gmail.com>
+
+The :mod:`goocalendar <goocalendar>` module supplies a calendar widget drawed
+with GooCanvas that can display a month view and a week view. It also supplies
+classes to manage events you can add to the calendar.
+
+
+.. _calendar:
+
+Calendar Objects
+------------------
+A :class:`Calendar <goocalendar.Calendar>` is a calendar widget using
+GooCanvas that can display a month view and a week view. It holds an
+:class:`EventStore<goocalendar.EventStore>` which contains events
+displayed in the calendar.
+
+.. class:: goocalendar.Calendar([event_store[, view[, time_format[, \
+ firstweekday]]]])
+
+ Creates a :class:`Calendar<goocalendar.Calendar>` object. All arguments are
+ optional. *event_store* should be an
+ :class:`EventStore<goocalendar.EventStore>`.
+ *view* should be either 'month' or 'week'. Default value is 'month'.
+ *time_format* determines the format of displayed time in the calendar.
+ Default value is '%H:%M'. *firstweekday* is a constant of module calendar
+ specifying the first day of the week. Default value is *calendar.SUNDAY*.
+
+Instance attributes:
+
+.. attribute:: event_store
+
+ :class:`EventStore <goocalendar.EventStore>` currently plugged.
+ Setting a new event store will automatically redraw the canvas to display
+ the events of the new event store.
+
+.. attribute:: view
+
+ The current view: 'month' or 'week'.
+
+.. attribute:: selected_date
+
+ `datetime.date
+ <http://docs.python.org/library/datetime.html#date-objects>`_
+ which determines the current selected day in the calendar.
+
+.. attribute:: firstweekday
+
+ Determines the first day of the week (0 is Monday).
+
+Instance methods:
+
+.. method:: select(date)
+
+ Select the given date in the calendar. Date should be a
+ `datetime.date
+ <http://docs.python.org/library/datetime.html#date-objects>`_.
+
+.. method:: previous_page()
+
+ Go to the previous page of the calendar.
+
+.. method:: next_page()
+
+ Go to the next page of the calendar.
+
+.. method:: set_view(view)
+
+ Change calendar's view. Possible values: 'month' or 'week'.
+
+.. method:: draw_events()
+
+ Redraws events.
+
+.. method:: update()
+
+ Redraws calendar and events.
+
+Instance signals:
+
+``event-pressed``
+
+ The ``event-pressed`` signal is emitted when an Event is pressed with the
+ button 1 of the mouse.
+
+ ``def callback(calendar, event, user_param1, ...)``
+
+ *calendar*
+ The :class:`Calendar <goocalendar.Calendar>` that received the signal.
+
+ *event*
+ The pressed :class:`Event <goocalendar.Event>` object.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+``event-activated``
+
+ The ``event-activated`` signal is emitted when an
+ :class:`Event <goocalendar.Event>` is double-clicked
+ with the button 1 of the mouse.
+
+ ``def callback(calendar, event, user_param1, ...)``
+
+ *calendar*
+ The :class:`Calendar <goocalendar.Calendar>` that received the signal.
+
+ *event*
+ The double-clicked :class:`Event <goocalendar.Event>` object.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+``event-released``
+
+ The ``event-released`` signal is emitted when the button 1 of the mouse is
+ released on an event.
+
+ ``def callback(calendar, event, user_param1, ...)``
+
+ *calendar*
+ The :class:`Calendar <goocalendar.Calendar>` that received the signal.
+
+ *event*
+ The double-clicked :class:`Event <goocalendar.Event>` object.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+``day-pressed``
+
+ The ``day-pressed`` signal is emitted when a day is pressed with the
+ mouse button 1.
+
+ ``def callback(calendar, date, user_param1, ...)``
+
+ *calendar*
+ The :class:`Calendar <goocalendar.Calendar>` that received the signal.
+
+ *date*
+ `datetime.date
+ <http://docs.python.org/library/datetime.html#date-objects>`_
+ corresponding to the day pressed.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+``day-activated``
+
+ The ``day-activated`` signal is emitted when the day is double-clicked with
+ the mouse button 1.
+
+ ``def callback(calendar, date, user_param1, ...)``
+
+ *calendar*
+ The :class:`Calendar <goocalendar.Calendar>` that received the signal.
+
+ *date*
+ `datetime.date
+ <http://docs.python.org/library/datetime.html#date-objects>`_
+ corresponding to the activated day.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method
+
+ *...*
+ additional user parameters (if any).
+
+``day-selected``
+
+ The ``day-selected`` signal is emitted when the selected day changes.
+
+ ``def callback(calendar, date, user_param1, ...)``
+
+ *calendar*
+ The :class:`Calendar <goocalendar.Calendar>` that received the signal.
+
+ *date*
+ `datetime.date
+ <http://docs.python.org/library/datetime.html#date-objects>`_
+ corresponding to the new selected day.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+``view-changed``
+
+ The ``view-changed`` signal is emitted when the view changes
+
+ ``def callback(calendar, view, user_param1, ...)``
+
+ *calendar*
+ The :class:`Calendar <goocalendar.Calendar>` that received the signal.
+
+ *view*
+ 'month' or 'week'
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method
+
+ *...*
+ additional user parameters (if any).
+
+``page-changed``
+
+ The ``page-changed`` signal is emitted when the page currently showed in
+ the calendar is changed.
+
+ ``def callback(calendar, date, user_param1, ...)``
+
+ *calendar*
+ The :class:`Calendar <goocalendar.Calendar>` that received the signal.
+
+ *date*
+ `datetime.date
+ <http://docs.python.org/library/datetime.html#date-objects>`_
+ corresponding to the selected day in the calendar.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+.. _eventstore:
+
+EventStore Objects
+--------------------
+
+An :class:`EventStore <goocalendar.EventStore>` is the store of
+:class:`Event <goocalendar.Event>` that can be plugged to a
+:class:`Calendar <goocalendar.Calendar>`.
+
+.. class:: goocalendar.EventStore()
+
+ There is no arguments for this class.
+
+Instance methods:
+
+.. method:: add(event)
+
+ Add the given event to the event store.
+
+.. method:: remove(event)
+
+ Remove the given event from the event store.
+
+.. method:: clear()
+
+ Remove all events from the event store and restore it to initial state.
+
+.. method:: get_events(start, end)
+
+ Returns a list of all events that intersect with the given start and end
+ datetime. If no start time nor end time are given, the method returns a
+ list containing all events.
+
+Instance signals:
+
+``event-added``
+
+ The ``event-added`` signal is emitted when an Event is added to the
+ event store.
+
+ ``def callback(event_store, event, user_param1, ...)``
+
+ *event_store*
+ The :class:`EventStore <goocalendar.EventStore>` that received the signal.
+
+ *event*
+ The added :class:`Event <goocalendar.Event>`.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+``event-removed``
+
+ The ``event-removed`` signal is emitted when an Event is removed from
+ the event store.
+
+ ``def callback(event_store, event, user_param1, ...)``
+
+ *event_store*
+ The :class:`EventStore <goocalendar.EventStore>` that received the signal.
+
+ *event*
+ The removed :class:`Event <goocalendar.Event>`.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+``events-cleared``
+
+ The ``events-cleared`` signal is emitted when the event store is cleared.
+
+ ``def callback(event_store, user_param1, ...)``
+
+ *event_store*
+ The :class:`EventStore <goocalendar.EventStore>` that received the signal.
+
+ *user_param1*
+ the first user parameter (if any) specified with the connect() method.
+
+ *...*
+ additional user parameters (if any).
+
+
+
+.. _event:
+
+Event Objects
+---------------
+
+An :class:`Event <goocalendar.Event>` represents an event in a
+:class:`Calendar <goocalendar.Calendar>`.
+
+.. class:: goocalendar.Event(caption, start[, end[, all_day[, text_color \
+ [, bg_color]]]])
+
+ *caption* argument is mandatory and will be the string displayed on the
+ event. *start* argument is mandatory and determines the starting time of
+ the event. It should be a
+ `datetime\
+ <http://docs.python.org/library/datetime.html#datetime-objects>`_.
+ All other arguments are optional. *end* argument may be a datetime,
+ all_day a boolean value. An event will be considered as all day
+ event if no *end* argument is supplied. *text_color* and *bg_color*
+ arguments are supposed to be color strings.
+
+Instance attributes:
+
+.. attribute:: id
+
+ Unique identification integer.
+
+.. attribute:: caption
+
+ Caption to display on the event in the calendar.
+
+.. attribute:: start
+
+ `datetime <http://docs.python.org/library/datetime.html#datetime-objects>`_
+ determining event start time.
+
+.. attribute:: end
+
+ `datetime <http://docs.python.org/library/datetime.html#datetime-objects>`_
+ determining event end time.
+
+.. attribute:: all_day
+
+ Boolean determining if the day is an all day event or a normal event.
+
+.. attribute:: text_color
+
+ String determining caption text color.
+
+.. attribute:: bg_color
+
+ String determining background color.
+
+.. attribute:: multidays
+
+ Boolean property determining if the event is longer than one day.
+
+Supported operations:
+
+All comparisons operations are supported.
+
+event1 is considered less than event2 if it starts before event2.
+If two events start at the same time, the event which ends the first
+one is considered smaller.
+
+Example usage::
+
+ >>> import datetime
+ >>> import goocalendar
+ >>> event_store = goocalendar.EventStore()
+ >>> calendar = goocalendar.Calendar(event_store)
+ >>> event = goocalendar.Event('Event number 1',
+ ... datetime.datetime(2012, 8, 21, 14),
+ ... datetime.datetime(2012, 8, 21, 17),
+ ... bg_color='lightgreen')
+ >>> event_store.add(event)
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/goocalendar/__init__.py b/goocalendar/__init__.py
new file mode 100644
index 0000000..615d42f
--- /dev/null
+++ b/goocalendar/__init__.py
@@ -0,0 +1,7 @@
+#This file is part of GooCalendar. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+from ._calendar import Calendar
+from ._event import Event, EventStore
+
+__all__ = ['Calendar', 'EventStore', 'Event']
+__version__ = '0.1'
diff --git a/goocalendar/_calendar.py b/goocalendar/_calendar.py
new file mode 100644
index 0000000..1d2f5b5
--- /dev/null
+++ b/goocalendar/_calendar.py
@@ -0,0 +1,1276 @@
+#This file is part of GooCalendar. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+import datetime
+import calendar
+import math
+from operator import add
+
+import gtk
+import gobject
+import goocanvas
+import pango
+
+import util
+from .util import left_click
+
+
+class Calendar(goocanvas.Canvas):
+ AVAILABLE_VIEWS = ["month", "week"]
+ MIN_PER_LEVEL = 15 # Number of minutes per graduation for drag and drop
+
+ def __init__(self, event_store=None, view="month", time_format="%H:%M",
+ firstweekday=calendar.SUNDAY):
+ super(Calendar, self).__init__()
+ self._selected_day = None
+ self._bg_rect = None
+ self._timeline = None
+ self._line_height = 0
+ self._realized = False
+ self._event_store = None
+ self._event_removed_sigid = None
+ self._event_added_sigid = None
+ self._events_cleared_sigid = None
+ self.event_store = event_store
+ self.firstweekday = firstweekday
+ self._drag_start_date = None
+ self._drag_date = None
+ self._drag_x = None
+ self._drag_y = None
+ self._drag_height = 0
+ self._last_click_x = None
+ self._last_click_y = None
+ self._last_click_time = 0
+ self._day_width = 0
+ self._day_height = 0
+ self._event_items = []
+ assert view in self.AVAILABLE_VIEWS
+ self.view = view
+ self.selected_date = datetime.date.today()
+ self.time_format = time_format
+ self.set_bounds(0, 0, 200, 200)
+ self.set_flags(gtk.CAN_FOCUS)
+ self.set_events(gtk.gdk.EXPOSURE_MASK
+ | gtk.gdk.BUTTON_PRESS_MASK
+ | gtk.gdk.BUTTON_RELEASE_MASK
+ | gtk.gdk.POINTER_MOTION_MASK
+ | gtk.gdk.POINTER_MOTION_HINT_MASK
+ | gtk.gdk.KEY_PRESS_MASK
+ | gtk.gdk.KEY_RELEASE_MASK
+ | gtk.gdk.ENTER_NOTIFY_MASK
+ | gtk.gdk.LEAVE_NOTIFY_MASK
+ | gtk.gdk.FOCUS_CHANGE_MASK)
+ self.connect_after('realize', self.on_realize)
+ self.connect('size-allocate', self.on_size_allocate)
+ self.connect('key-press-event', self.on_key_press_event)
+
+ # Initialize background, timeline and days and add them to canvas
+ root = self.get_root_item()
+ style = self.get_style()
+ color = util.color_to_string(style.bg[gtk.STATE_PRELIGHT])
+ self._bg_rect = goocanvas.Rect(parent=root, x=0, y=0,
+ stroke_color=color, fill_color=color)
+ self._timeline = TimelineItem(self, time_format=self.time_format)
+ root.add_child(self._timeline)
+ self.days = []
+ while len(self.days) < 42: # 6 rows of 7 days
+ box = DayItem(self)
+ root.add_child(box)
+ box.connect('button_press_event',
+ self.on_day_item_button_press_event)
+ self.days.append(box)
+
+ def select(self, new_date):
+ cal = calendar.Calendar(self.firstweekday)
+ if hasattr(new_date, 'date'):
+ new_date = new_date.date()
+ old_date = self.selected_date
+ old_day = self._selected_day
+ self.selected_date = new_date
+ page_changed = False
+ if self.view == "month":
+ page_changed = not util.same_month(old_date, new_date)
+ elif self.view == "week":
+ old_first_weekday = util.first_day_of_week(cal, old_date)
+ new_first_weekday = util.first_day_of_week(cal, new_date)
+ page_changed = old_first_weekday != new_first_weekday
+
+ # This is slow: When the month was changed we need to update
+ # the entire canvas.
+ if old_day is None or page_changed:
+ self.update()
+ self.emit('day-selected', self.selected_date)
+ self.emit('page-changed', self.selected_date)
+ return
+
+ # This is fast: Update only the old and newly selected days.
+ # Find the canvas item that corresponds to the new date.
+ weeks = cal.monthdayscalendar(new_date.year, new_date.month)
+ found = -1
+ for weekno, week in enumerate(weeks):
+ for dayno, day in enumerate(week):
+ if day == new_date.day:
+ found = weekno * 7 + dayno
+ break
+ if found != -1:
+ break
+
+ # Swap border colors.
+ new_day = self.days[found]
+ old_border_color = old_day.border_color
+ old_day.full_border = False
+ old_day.border_color = new_day.border_color
+ new_day.border_color = old_border_color
+ new_day.full_border = True
+
+ # Redraw.
+ old_day.update()
+ new_day.update()
+ self._selected_day = new_day
+ if old_day != new_day:
+ self.emit('day-selected', self.selected_date)
+
+ def previous_page(self):
+ cal = calendar.Calendar(self.firstweekday)
+ if self.view == "month":
+ new_date = util.previous_month(cal, self.selected_date)
+ elif self.view == "week":
+ new_date = util.previous_week(cal, self.selected_date)
+ self.select(new_date)
+
+ def next_page(self):
+ cal = calendar.Calendar(self.firstweekday)
+ if self.view == "month":
+ new_date = util.next_month(cal, self.selected_date)
+ elif self.view == "week":
+ new_date = util.next_month(cal, self.selected_date)
+ self.select(new_date)
+
+ def set_view(self, level):
+ if level == self.view:
+ return
+ assert level in self.AVAILABLE_VIEWS
+ self.view = level
+ self.update()
+ self.emit('view-changed', self.view)
+
+ @property
+ def event_store(self):
+ return self._event_store
+
+ @event_store.setter
+ def event_store(self, event_store):
+ # Disconnect previous event store if any
+ if self._event_store:
+ self._event_store.disconnect(self._event_removed_sigid)
+ self._event_store.disconnect(self._event_added_sigid)
+ self._event_store_disconnect(self._events_cleared_sigid)
+
+ # Set and connect new event_store
+ self._event_store = event_store
+ self.update()
+ if not event_store:
+ return
+ self._event_removed_sigid = self._event_store.connect('event-removed',
+ self.on_event_store_event_removed)
+ self._event_added_sigid = self._event_store.connect('event-added',
+ self.on_event_store_event_added)
+ self._events_cleared_sigid = \
+ self._event_store.connect('events-cleared',
+ self.on_event_store_events_cleared)
+
+ def on_realize(self, *args):
+ self._realized = True
+ self.grab_focus(self.get_root_item())
+ self.on_size_allocate(*args)
+
+ def on_size_allocate(self, *args):
+ alloc = self.get_allocation()
+ if not self._realized or alloc.width < 10 or alloc.height < 10:
+ return
+ self.set_bounds(0, 0, alloc.width, alloc.height)
+ self.update()
+
+ def update(self):
+ if not self._realized:
+ return
+ self.draw_background()
+ if self.view == "month":
+ self.draw_month()
+ elif self.view == "week":
+ self.draw_week()
+ self.draw_events()
+
+ def draw_background(self):
+ x, y, w, h = self.get_bounds()
+ self._bg_rect.set_property('width', w)
+ self._bg_rect.set_property('height', h)
+
+ def draw_week(self):
+ """
+ Draws the currently selected week.
+ """
+ style = self.get_style()
+ pango_size = style.font_desc.get_size()
+ text_color = util.color_to_string(style.fg[gtk.STATE_NORMAL])
+ border_color = util.color_to_string(style.mid[gtk.STATE_NORMAL])
+ body_color = util.color_to_string(style.light[gtk.STATE_ACTIVE])
+ selected_border_color = util.color_to_string(
+ style.mid[gtk.STATE_SELECTED])
+ today_body_color = 'ivory'
+ x, y, w, h = self.get_bounds()
+ timeline_w = self._timeline.width
+ caption_size = max(len(day_name) for day_name in calendar.day_name)
+ caption_size += 3 # The needed space for the date before the day_name
+ day_width_min = caption_size * pango_size / pango.SCALE
+ day_width_max = (w - timeline_w) / 7
+ self._day_width = max(day_width_min, day_width_max)
+ self._day_height = h
+ width, height = self.get_size_request()
+ new_width = int(timeline_w + 7 * self._day_width)
+ if (width != new_width and day_width_min >= day_width_max):
+ self.set_size_request(new_width, height) # Minimum widget size
+
+ # Redraw all days.
+ cal = calendar.Calendar(self.firstweekday)
+ weeks = util.my_monthdatescalendar(cal, self.selected_date)
+ for weekno, week in enumerate(weeks):
+ # Hide all days that are not part of the current week.
+ if self.selected_date not in week:
+ for dayno, date in enumerate(week):
+ box = self.days[weekno * 7 + dayno]
+ box.set_property('visibility', goocanvas.ITEM_INVISIBLE)
+ continue
+
+ # Draw the days that are part of the current week.
+ for dayno, current_date in enumerate(week):
+ # Highlight the day according to it's selection.
+ selected = current_date == self.selected_date
+ if selected:
+ the_border_color = selected_border_color
+ else:
+ the_border_color = border_color
+ if current_date == datetime.date.today():
+ the_body_color = today_body_color
+ else:
+ the_body_color = body_color
+
+ # Draw.
+ box = self.days[weekno * 7 + dayno]
+ box.x = self._day_width * dayno + timeline_w
+ box.y = 0
+ box.width = self._day_width - 2
+ box.height = self._day_height
+ box.type = 'week'
+ box.date = current_date
+ box.full_border = selected
+ box.border_color = the_border_color
+ box.body_color = the_body_color
+ box.title_text_color = text_color
+ box.event_text_color = text_color
+ box.set_property('visibility', goocanvas.ITEM_VISIBLE)
+ box.update()
+
+ if selected:
+ self._selected_day = box
+ self._line_height = self._selected_day.line_height
+
+ def draw_month(self):
+ """
+ Draws the currently selected month.
+ """
+ style = self.get_style()
+ x1, y1, w, h = self.get_bounds()
+ pango_size = style.font_desc.get_size()
+ caption_size = max(len(day_name) for day_name in calendar.day_name)
+ caption_size += 3 # The needed space for the date before the day_name
+ day_width_min = caption_size * pango_size / pango.SCALE
+ day_width_max = w / 7
+ self._day_width = max(day_width_min, day_width_max)
+ self._day_height = h / 6
+ text_color = util.color_to_string(style.fg[gtk.STATE_NORMAL])
+ inactive_text_color = util.color_to_string(
+ style.fg[gtk.STATE_INSENSITIVE])
+ border_color = util.color_to_string(style.mid[gtk.STATE_NORMAL])
+ selected_border_color = util.color_to_string(
+ style.mid[gtk.STATE_SELECTED])
+ inactive_border_color = util.color_to_string(
+ style.bg[gtk.STATE_PRELIGHT])
+ body_color = util.color_to_string(style.light[gtk.STATE_ACTIVE])
+ today_body_color = 'ivory'
+
+ # Hide the timeline.
+ if self._timeline is not None:
+ self._timeline.set_property('visibility', goocanvas.ITEM_INVISIBLE)
+
+ # Draw the grid.
+ y_pos = 0
+ cal = calendar.Calendar(self.firstweekday)
+ weeks = util.my_monthdatescalendar(cal, self.selected_date)
+ for weekno, week in enumerate(weeks):
+ for dayno, date in enumerate(week):
+ # The color depends on whether each day is part of the
+ # current month.
+ if (not util.same_month(date, self.selected_date)):
+ the_border_color = inactive_border_color
+ the_text_color = inactive_text_color
+ else:
+ the_border_color = border_color
+ the_text_color = text_color
+
+ # Highlight the day according to it's selection.
+ selected = date == self.selected_date
+ if selected:
+ the_border_color = selected_border_color
+ if date == datetime.date.today():
+ the_body_color = today_body_color
+ else:
+ the_body_color = body_color
+
+ # Draw a box for the day.
+ box = self.days[weekno * 7 + dayno]
+ box.x = self._day_width * dayno
+ box.y = y_pos
+ box.width = self._day_width - 2
+ box.height = self._day_height - 2
+ box.date = date
+ box.full_border = selected
+ box.border_color = the_border_color
+ box.body_color = the_body_color
+ box.title_text_color = the_text_color
+ box.event_text_color = the_text_color
+ box.type = 'month'
+ box.set_property('visibility', goocanvas.ITEM_VISIBLE)
+ box.update()
+
+ if selected:
+ self._selected_day = box
+ self._line_height = self._selected_day.line_height
+
+ y_pos += self._day_height
+
+ width, height = self.get_size_request()
+ new_width = int(7 * self._day_width)
+ new_height = int(14 * box.line_height)
+ if ((width != new_width and self._day_width == day_width_min)
+ or new_height != height):
+ self.set_size_request(new_width, new_height)
+
+ def _get_day_item(self, find_date):
+ cal = calendar.Calendar(self.firstweekday)
+ weeks = util.my_monthdatescalendar(cal, find_date)
+ for weekno, week in enumerate(weeks):
+ for dayno, date in enumerate(week):
+ if date == find_date:
+ return self.days[weekno * 7 + dayno]
+ raise Exception('Day not found: %s' % (find_date))
+
+ def _get_day_items(self, event):
+ """
+ Given an event, this method returns a list containing the
+ DayItem corresponding with each day on which the event takes
+ place.
+ Days that are currently not in the view are not returned.
+ """
+ cal = calendar.Calendar(self.firstweekday)
+ weeks = util.my_monthdatescalendar(cal, self.selected_date)
+ start = event.start.date()
+ end = event.end.date() if event.end else event.start.date()
+ assert start <= end
+ days = []
+ for weekno, week in enumerate(weeks):
+ if self.view == "week":
+ if self.selected_date not in week:
+ continue
+ for dayno, date in enumerate(week):
+ if date >= start and date <= end:
+ days.append(self.days[weekno * 7 + dayno])
+ if date == end:
+ return days
+ if len(days) > 0:
+ return days
+ raise Exception('Days not found: %s %s' % (event.start, end))
+
+ def _find_free_line(self, days):
+ for line in range(days[0].n_lines):
+ free = True
+ for day in days:
+ if line in day.lines:
+ free = False
+ break
+ if free:
+ return line
+ return None
+
+ def draw_events(self):
+ # Clear previous events.
+ for item in self._event_items:
+ self.get_root_item().remove_child(item)
+ self._event_items = []
+ for day in self.days:
+ day.lines.clear()
+ day.show_indic = False
+ day.update()
+
+ if not self._event_store:
+ return
+
+ cal = calendar.Calendar(self.firstweekday)
+ if self.view == "month":
+ weeks = util.my_monthdatescalendar(cal, self.selected_date)
+ dates = []
+ for week in weeks:
+ dates += week
+ else:
+ dates = util.my_weekdatescalendar(cal, self.selected_date)
+
+ # Retrieve a list of all events in the current time span,
+ # and sort them by event length.
+ onedaydelta = (datetime.timedelta(days=1)
+ - datetime.timedelta(microseconds=1))
+ start = datetime.datetime.combine(dates[0], datetime.time())
+ end = datetime.datetime.combine(dates[-1], datetime.time()) \
+ + onedaydelta
+ events = self._event_store.get_events(start, end)
+ events.sort(util.event_days, reverse=True)
+
+ # Draw all-day events, longest event first.
+ max_y = self._selected_day.line_height
+ non_all_day_events = []
+ for event in events:
+ event.event_items = []
+ # Handle non-all-day events differently in week mode.
+ if (self.view == "week" and not event.all_day
+ and not event.multidays):
+ non_all_day_events.append(event)
+ continue
+
+ # Find a line that is free in all of the days.
+ days = self._get_day_items(event)
+ free_line = self._find_free_line(days)
+ if free_line is None:
+ for day in days:
+ day.show_indic = True
+ day.update()
+ continue
+
+ max_line_height = max(x.line_height for x in days)
+ all_day_events_height = (free_line + 2) * max_line_height
+ all_day_events_height += (free_line + 1) * 2 # 2px margin per line
+ all_day_events_height += 1 # 1px padding-top
+ max_y = max(all_day_events_height, max_y)
+ for day in days:
+ day.lines[free_line] = 1
+
+ # Split days into weeks.
+ weeks = []
+ week_start = 0
+ week_end = 0
+ while week_end < len(days):
+ day = days[week_start]
+ weekday = (day.date.weekday() - self.firstweekday) % 7
+ week_end = week_start + (7 - weekday)
+ week = days[week_start:week_end]
+ weeks.append(week)
+ week_start = week_end
+
+ for week in weeks:
+ dayno = 0
+ day = week[dayno]
+ event_item = EventItem(self, event=event,
+ time_format=self.time_format)
+ if len(event.event_items):
+ event_item.no_caption = True
+ event.event_items.append(event_item)
+ event_item.connect('button_press_event',
+ self.on_event_item_button_press_event)
+ event_item.connect('button_release_event',
+ self.on_event_item_button_release)
+ event_item.connect('motion_notify_event',
+ self.on_event_item_motion_notified)
+ self._event_items.append(event_item)
+ self.get_root_item().add_child(event_item)
+ event_item.x = day.x
+ event_item.left_border = day.x + 2
+ event_item.y = day.y + (free_line + 1) * day.line_height
+ event_item.y += free_line * 2 # 2px of margin per line
+ event_item.y += 1 # 1px padding-top
+ event_item.width = (day.width + 2) * len(week)
+ event_item.height = day.line_height
+ week_start = week[0].date
+ week_end = week[-1].date
+ end = event.end if event.end else event.start
+ if (event.start.date() < week_start
+ and end.date() > week_end):
+ event_item.type = 'mid'
+ event_item.width -= 3
+ elif event.start.date() < week_start:
+ event_item.type = 'right'
+ event_item.width -= 4
+ elif end.date() > week_end:
+ event_item.type = 'left'
+ event_item.x += 2
+ event_item.width -= 4
+ else:
+ event_item.x += 2
+ event_item.width -= 6
+ event_item.type = 'leftright'
+ event_item.update()
+
+ if self.view != "week":
+ return
+
+ # Redraw the timeline.
+ style = self.get_style()
+ text_color = util.color_to_string(style.fg[gtk.STATE_NORMAL])
+ border_color = util.color_to_string(style.mid[gtk.STATE_NORMAL])
+ body_color = util.color_to_string(style.light[gtk.STATE_ACTIVE])
+ self._timeline.set_property('visibility', goocanvas.ITEM_VISIBLE)
+ x, y, w, h = self.get_bounds()
+ self._timeline.x = x
+ self._timeline.y = max_y
+ self._timeline.height = h - max_y - 2
+ self._timeline.line_color = body_color
+ self._timeline.bg_color = border_color
+ self._timeline.text_color = text_color
+ self._timeline.update()
+ width, height = self.get_size_request()
+ min_line_height = self._timeline.min_line_height
+ line_height = self._timeline.line_height
+ self.minute_height = line_height / 60.0
+ new_height = int(max_y + 24 * min_line_height)
+ if (height != new_height):
+ self.set_size_request(width, new_height)
+
+ # Draw non-all-day events.
+ for date in dates:
+ date_start = datetime.datetime.combine(date, datetime.time())
+ date_end = (datetime.datetime.combine(date_start, datetime.time())
+ + datetime.timedelta(days=1))
+ day = self._get_day_item(date)
+ day_events = util.get_intersection_list(non_all_day_events,
+ date_start, date_end)
+ day_events.sort()
+ columns = []
+ column = 0
+
+ # Sort events into columns.
+ remaining_events = day_events[:]
+ while len(remaining_events) > 0:
+ columns.append([remaining_events[0]])
+ for event in remaining_events:
+ intersections = util.count_intersections(columns[-1],
+ event.start, event.end)
+ if intersections == 0:
+ columns[-1].append(event)
+ for event in columns[-1]:
+ remaining_events.remove(event)
+
+ # Walk through all columns.
+ for columnno, column in enumerate(columns):
+ for event in column:
+ # Crop the event to the current day.
+ event1_start = max(event.start, date_start)
+ event1_end = min(event.end, date_end)
+
+ parallel = util.count_parallel_events(day_events,
+ event1_start, event1_end)
+
+ # Draw.
+ top_offset = event1_start - date_start
+ bottom_offset = event1_end - event1_start
+ top_offset_mins = top_offset.seconds / 60
+ bottom_offset_mins = ((bottom_offset.days * 24 * 60)
+ + bottom_offset.seconds / 60)
+
+ event_item = EventItem(self, event=event,
+ time_format=self.time_format)
+ if event.event_items:
+ event_item.no_caption = True
+ event.event_items.append(event_item)
+ event_item.connect('button_press_event',
+ self.on_event_item_button_press_event)
+ event_item.connect('button_release_event',
+ self.on_event_item_button_release)
+ event_item.connect('motion_notify_event',
+ self.on_event_item_motion_notified)
+ self._event_items.append(event_item)
+ self.get_root_item().add_child(event_item)
+ y_off1 = top_offset_mins * self.minute_height
+ y_off2 = bottom_offset_mins * self.minute_height
+ column_width = day.width / parallel
+ event_item.left_border = day.x + 2
+ event_item.x = day.x + (columnno * column_width) + 2
+ event_item.y = max_y + y_off1
+ event_item.width = column_width - 4
+ if columnno != (parallel - 1):
+ event_item.width += column_width / 1.2
+ event_item.height = y_off2
+ if event.start < event1_start and event.end > event1_end:
+ event_item.type = 'mid'
+ elif event.start < event1_start:
+ event_item.type = 'top'
+ elif event.end > event1_end:
+ event_item.type = 'bottom'
+ else:
+ event_item.type = 'topbottom'
+ event_item.update()
+
+ def on_event_store_event_removed(self, store, event):
+ self.update()
+
+ def on_event_store_event_added(self, store, event):
+ self.update()
+
+ def on_event_store_events_cleared(self, store):
+ self.update()
+
+ def on_key_press_event(self, widget, event):
+ date = self.selected_date
+ if event.keyval == gtk.gdk.keyval_from_name('Up'):
+ self.select(date - datetime.timedelta(7))
+ elif event.keyval == gtk.gdk.keyval_from_name('Down'):
+ self.select(date + datetime.timedelta(7))
+ elif event.keyval == gtk.gdk.keyval_from_name('Left'):
+ self.select(date - datetime.timedelta(1))
+ elif event.keyval == gtk.gdk.keyval_from_name('Right'):
+ self.select(date + datetime.timedelta(1))
+
+ @left_click
+ def on_day_item_button_press_event(self, day, widget2, event):
+ self.emit('day-pressed', day.date)
+ self.select(day.date)
+
+ if self._is_double_click(event):
+ self.emit('day-activated', day.date)
+
+ def _is_double_click(self, event):
+ gtk_settings = gtk.settings_get_default()
+ double_click_distance = gtk_settings.props.gtk_double_click_distance
+ double_click_time = gtk_settings.props.gtk_double_click_time
+ if (self._last_click_x is not None and
+ event.time < (self._last_click_time + double_click_time) and
+ abs(event.x - self._last_click_x) <= double_click_distance and
+ abs(event.y - self._last_click_y) <= double_click_distance):
+ self._last_click_x = None
+ self._last_click_y = None
+ self._last_click_time = None
+ return True
+ else:
+ self._last_click_x = event.x
+ self._last_click_y = event.y
+ self._last_click_time = event.time
+ return False
+
+ def get_cur_pointed_date(self, x, y):
+ """
+ Return the date of the day_item pointed by two coordinates [x,y]
+ """
+ # Get current week
+ cal = calendar.Calendar(self.firstweekday)
+ weeks = util.my_monthdatescalendar(cal, self.selected_date)
+ if self.view == 'week':
+ cur_week, = (week for week in weeks for date in week
+ if self.selected_date == date)
+ elif self.view == 'month':
+ max_height = 6 * self._day_height
+ if y < 0:
+ weekno = 0
+ elif y > max_height:
+ weekno = 5
+ else:
+ weekno = int(y / self._day_height)
+ cur_week = weeks[weekno]
+
+ # Get Current pointed date
+ max_width = 7 * self._day_width
+ if x < 0:
+ day_no = 0
+ elif x > max_width:
+ day_no = 6
+ else:
+ offset_x = self._timeline.width if self.view == 'week' else 0
+ day_no = int((x - offset_x) / self._day_width)
+ return cur_week[day_no]
+
+ @left_click
+ def on_event_item_button_press_event(self, event_item, rect, event):
+
+ # Drag and drop starting coordinates
+ self._drag_x = event.x
+ self._drag_y = event.y
+ self._drag_height = 0
+ self._drag_start_date = self.get_cur_pointed_date(event.x, event.y)
+ self._drag_date = self._drag_start_date
+ self.set_has_tooltip(False)
+ event_item.raise_(None)
+ event_item.transparent = True
+
+ event_item.width = self._day_width - 6 # Biggest event width
+ event_date = event_item.event.start.date()
+ daysdelta = self._drag_start_date - event_date
+ if self.view == 'week':
+ event_item.x = event_item.left_border
+ if ((event_item.event.all_day or event_item.event.multidays)
+ and self._drag_start_date != event_date):
+ event_item.x += daysdelta.days * self._day_width
+ event_item.event.start += daysdelta
+ if event_item.event.end:
+ event_item.event.end += daysdelta
+ else:
+ for item in event_item.event.event_items:
+ if item != event_item:
+ self.get_root_item().remove_child(item)
+ self._event_items.remove(item)
+
+ event_item.height = 2 * self._line_height
+ day_no = (int((event.x - self._timeline.width)
+ / self._day_width))
+ day_off = day_no * self._day_width + 2
+ event_item.x = self._timeline.width + day_off
+ if (event_item.no_caption or event.y < event_item.y
+ or event.y > (event_item.y + event_item.height)):
+ # click was not performed inside the new day item
+ level_height = self.minute_height * self.MIN_PER_LEVEL
+ cur_level = int((event.y - self._timeline.y)
+ / level_height)
+ nb_levels_per_hour = 60 / self.MIN_PER_LEVEL
+ cur_level -= nb_levels_per_hour # click is in the middle
+ if cur_level < 0:
+ cur_level = 0
+ event_item.y = self._timeline.y + cur_level * level_height
+ nb_minutes = cur_level * self.MIN_PER_LEVEL
+ minutes = nb_minutes % 60
+ hours = nb_minutes / 60
+ old_start = event_item.event.start
+ new_start = \
+ datetime.datetime.combine(self._drag_start_date,
+ datetime.time(hours, minutes))
+ event_item.event.start = new_start
+ delta = new_start - old_start
+ if event_item.event.end:
+ event_item.event.end += delta
+ event_item.no_caption = False
+ elif self.view == 'month':
+ for item in event_item.event.event_items:
+ if item != event_item:
+ self.get_root_item().remove_child(item)
+ self._event_items.remove(item)
+ else:
+ event_item.event.start += daysdelta
+ if event_item.event.end:
+ event_item.event.end += daysdelta
+ weekno = int(event.y / self._day_height)
+ day_no = int(event.x / self._day_width)
+ event_item.y = weekno * self._day_height
+ event_item.y += int(self._line_height) + 1 # padding-top
+ event_item.x = day_no * self._day_width + 2 # padding-left
+ item_height = self._line_height + 2 # 2px between items
+ while event_item.y < event.y:
+ event_item.y += item_height
+ event_item.y -= item_height
+ event_item.no_caption = False
+ event_item.update()
+ self.emit('event-pressed', event_item.event)
+
+ if self._is_double_click(event):
+ self._stop_drag_and_drop()
+ self.emit('event-activated', event_item.event)
+
+ def on_event_item_button_release(self, event_item, rect, event):
+ event_item.transparent = False
+ self._stop_drag_and_drop()
+ self.draw_events()
+ self.emit('event-released', event_item.event)
+
+ def _stop_drag_and_drop(self):
+ self._drag_x = None
+ self._drag_y = None
+ self._drag_height = 0
+ self._drag_start_date = None
+ self._drag_date = None
+ self.set_has_tooltip(True)
+
+ def on_event_item_motion_notified(self, event_item, rect, event):
+ if self._drag_x and self._drag_y:
+ # We are currently drag and dropping this event item
+ diff_y = event.y - self._drag_y
+ self._drag_x = event.x
+ self._drag_y = event.y
+ self._drag_height += diff_y
+
+ cur_pointed_date = self.get_cur_pointed_date(event.x, event.y)
+ daysdelta = cur_pointed_date - self._drag_date
+ if self.view == 'month':
+ if cur_pointed_date != self._drag_date:
+ event_item.event.start += daysdelta
+ if event_item.event.end:
+ event_item.event.end += daysdelta
+ nb_lines = int(round(float(daysdelta.days) / 7))
+ nb_columns = daysdelta.days - nb_lines * 7
+ event_item.x += nb_columns * self._day_width
+ self._drag_date = cur_pointed_date
+ event_item.y += diff_y
+ event_item.update()
+ return
+
+ # Handle horizontal translation
+ if cur_pointed_date != self._drag_date:
+ self._drag_date = cur_pointed_date
+ event_item.event.start += daysdelta
+ if event_item.event.end:
+ event_item.event.end += daysdelta
+ event_item.x += daysdelta.days * self._day_width
+
+ if event_item.event.multidays or event_item.event.all_day:
+ event_item.update()
+ return
+
+ # Compute vertical translation
+ diff_minutes = int(round(self._drag_height / self.minute_height))
+ diff_time = datetime.timedelta(minutes=diff_minutes)
+ old_start = event_item.event.start
+ new_start = old_start + diff_time
+ next_level = util.next_level(old_start, self.MIN_PER_LEVEL)
+ prev_level = util.prev_level(old_start, self.MIN_PER_LEVEL)
+ if diff_time >= datetime.timedelta(0) and new_start >= next_level:
+ new_start = util.prev_level(new_start, self.MIN_PER_LEVEL)
+ elif diff_time < datetime.timedelta(0) and new_start <= prev_level:
+ new_start = util.next_level(new_start, self.MIN_PER_LEVEL)
+ else:
+ # We stay at the same level
+ event_item.update()
+ return
+
+ # Apply vertical translation
+ midnight = datetime.time()
+ old_start_midnight = datetime.datetime.combine(old_start, midnight)
+ onedaydelta = datetime.timedelta(days=1)
+ next_day_midnight = old_start_midnight + onedaydelta
+ if new_start.day < old_start.day:
+ new_start = old_start_midnight
+ elif new_start >= next_day_midnight:
+ seconds_per_level = 60 * self.MIN_PER_LEVEL
+ level_delta = datetime.timedelta(seconds=seconds_per_level)
+ last_level = next_day_midnight - level_delta
+ new_start = last_level
+ event_item.event.start = new_start
+ if event_item.event.end:
+ timedelta = new_start - old_start
+ event_item.event.end += timedelta
+ pxdelta = (timedelta.total_seconds() / 60 * self.minute_height)
+ event_item.y += pxdelta
+ event_item.update()
+ self._drag_height -= pxdelta
+
+gobject.signal_new('event-pressed',
+ Calendar,
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))
+gobject.signal_new('event-activated',
+ Calendar,
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))
+gobject.signal_new('event-released',
+ Calendar,
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))
+gobject.signal_new('day-pressed',
+ Calendar,
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))
+gobject.signal_new('day-activated',
+ Calendar,
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))
+gobject.signal_new('day-selected',
+ Calendar,
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))
+gobject.signal_new('view-changed',
+ Calendar,
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))
+gobject.signal_new('page-changed',
+ Calendar,
+ gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,))
+
+
+class DayItem(goocanvas.Group):
+ """
+ A canvas item representing a day.
+ """
+
+ def __init__(self, cal, **kwargs):
+ super(DayItem, self).__init__()
+
+ self._cal = cal
+ self.x = kwargs.get('x', 0)
+ self.y = kwargs.get('y', 0)
+ self.width = kwargs.get('width', 0)
+ self.height = kwargs.get('height', 0)
+ self.border_color = kwargs.get('border_color')
+ self.body_color = kwargs.get('body_color')
+ self.full_border = kwargs.get('full_border')
+ self.date = kwargs.get('date')
+ self.type = kwargs.get('type', 'month')
+ self.show_indic = False
+ self.lines = {}
+ self.n_lines = 0
+ self.title_text_color = ""
+ self.line_height = 0
+
+ # Create canvas items.
+ self.border = goocanvas.Rect(parent=self)
+ self.text = goocanvas.Text(parent=self)
+ self.box = goocanvas.Rect(parent=self)
+ self.indic = goocanvas.Rect(parent=self)
+
+ def update(self):
+ if not self.date:
+ return
+
+ week_day = self.date.weekday()
+ day_name = calendar.day_name[week_day]
+ caption = '%s %s' % (self.date.day, day_name)
+ style = self._cal.get_style()
+ font_descr = style.font_desc.copy()
+ font = font_descr.to_string()
+ self.text.set_property('font', font)
+ self.text.set_property('text', caption)
+ logical_height = self.text.get_natural_extents()[1][3]
+ line_height = int(math.ceil(float(logical_height) / pango.SCALE))
+ self.line_height = line_height
+
+ # Draw the border.
+ self.border.set_property('x', self.x)
+ self.border.set_property('y', self.y)
+ self.border.set_property('width', self.width)
+ self.border.set_property('height', self.height)
+ self.border.set_property('stroke_color', self.border_color)
+ self.border.set_property('fill_color', self.border_color)
+
+ # Draw the title text.
+ padding_left = 2
+ self.text.set_property('x', self.x + padding_left)
+ self.text.set_property('y', self.y)
+ self.text.set_property('fill_color', self.title_text_color)
+
+ # Print the "body" of the day.
+ if self.full_border:
+ box_x = self.x + 2
+ box_y = self.y + line_height
+ box_width = max(self.width - 4, 0)
+ box_height = max(self.height - line_height - 3, 0)
+ else:
+ box_x = self.x + 1
+ box_y = self.y + line_height
+ box_width = max(self.width - 2, 0)
+ box_height = max(self.height - line_height, 0)
+ self.box.set_property('x', box_x)
+ self.box.set_property('y', box_y)
+ self.box.set_property('width', box_width)
+ self.box.set_property('height', box_height)
+ self.box.set_property('stroke_color', self.body_color)
+ self.box.set_property('fill_color', self.body_color)
+
+ line_height_and_margin = line_height + 2 # 2px of margin per line
+ self.n_lines = int(box_height / line_height_and_margin)
+
+ # Show an indicator in the title, if requested.
+ if not self.show_indic:
+ self.indic.set_property('visibility', goocanvas.ITEM_INVISIBLE)
+ return
+
+ self.indic.set_property('visibility', goocanvas.ITEM_VISIBLE)
+ self.indic.set_property('x',
+ self.x + self.width - line_height / 1.5)
+ self.indic.set_property('y', self.y + line_height / 3)
+ self.indic.set_property('width', line_height / 3)
+ self.indic.set_property('height', line_height / 3)
+ self.indic.set_property('stroke_color', self.title_text_color)
+ self.indic.set_property('fill_color', self.title_text_color)
+
+ # Draw a triangle.
+ x1 = self.x + self.width - line_height / 1.5
+ y1 = self.y + line_height / 3
+ x2 = x1 + line_height / 6
+ y2 = y1 + line_height / 3
+ x3 = x1 + line_height / 3
+ y3 = y1
+ path = 'M%s,%s L%s,%s L%s,%s Z' % (x1, y1, x2, y2, x3, y3)
+ self.indic.set_property('clip_path', path)
+
+
+class EventItem(goocanvas.Group):
+ """
+ A canvas item representing an event.
+ """
+
+ def __init__(self, cal, **kwargs):
+ super(EventItem, self).__init__()
+
+ self._cal = cal
+ self.x = kwargs.get('x')
+ self.y = kwargs.get('y')
+ self.width = kwargs.get('width')
+ self.height = kwargs.get('height')
+ self.bg_color = kwargs.get('bg_color')
+ self.text_color = kwargs.get('text_color')
+ self.event = kwargs.get('event')
+ self.type = kwargs.get('type', 'leftright')
+ self.time_format = kwargs.get('time_format')
+ self.transparent = False
+ self.no_caption = False
+
+ # Create canvas items.
+ self.box = goocanvas.Rect(parent=self)
+ self.text = goocanvas.Text(parent=self)
+ style = self._cal.get_style()
+ font_descr = style.font_desc.copy()
+ self.font = font_descr.to_string()
+ self.text.set_property('font', self.font)
+ logical_height = self.text.get_natural_extents()[1][3]
+ self.line_height = logical_height / pango.SCALE
+
+ if self.x is not None:
+ self.update()
+
+ def update(self):
+ if (self.event.all_day or self._cal.view == "month"
+ or self.event.multidays):
+ self.update_all_day_event()
+ else:
+ self.update_event()
+
+ def update_event(self):
+ self.width = max(self.width, 0)
+ starttime = self.event.start.strftime(self.time_format)
+ endtime = self.event.end.strftime(self.time_format)
+ tooltip = '%s - %s\n%s' % (starttime, endtime, self.event.caption)
+
+ # Do we have enough width for caption
+ first_line = starttime + ' - ' + endtime
+ self.text.set_property('text', first_line)
+ logical_width = self.text.get_natural_extents()[1][2] / pango.SCALE
+ if self.width < logical_width:
+ first_line = starttime + ' - '
+
+ second_line = self.event.caption
+ self.text.set_property('text', second_line)
+ logical_width = self.text.get_natural_extents()[1][2] / pango.SCALE
+ if self.width < logical_width:
+ second_line = None
+
+ # Do we have enough height for whole caption
+ if self.height >= (2 * self.line_height):
+ caption = first_line
+ if second_line:
+ caption += '\n' + second_line
+ elif self.height >= self.line_height:
+ caption = first_line
+ else:
+ caption = ''
+ caption = '' if self.no_caption else caption
+ the_event_bg_color = self.event.bg_color
+
+ # Choose text color.
+ if self.event.text_color is None:
+ the_event_text_color = self.text_color
+ else:
+ the_event_text_color = self.event.text_color
+
+ if the_event_bg_color is not None:
+ self.box.set_property('x', self.x)
+ self.box.set_property('y', self.y)
+ self.box.set_property('width', self.width)
+ self.box.set_property('height', self.height)
+ self.box.set_property('stroke_color', the_event_bg_color)
+ bg_rgba_color = self.box.get_property('stroke_color_rgba')
+ bg_colors = list(util.rgba_to_colors(bg_rgba_color))
+ # We make background color 1/4 brighter (+64 over 255)
+ bg_colors[:3] = map(min,
+ map(add, bg_colors[:3], (64,) * 3), (255,) * 3)
+ bg_colors = util.colors_to_rgba(*bg_colors)
+ self.box.set_property('fill_color_rgba', bg_colors)
+
+ # Alpha color is set to half of 255, i.e an opacity of 5O percents
+ transparent_color = self.box.get_property('fill_color_rgba') - 128
+ if self.transparent:
+ self.box.set_property('stroke_color_rgba', transparent_color)
+ self.box.set_property('fill_color_rgba', transparent_color)
+ self.box.set_property('tooltip', tooltip)
+
+ # Print the event name into the title box.
+ self.text.set_property('x', self.x + 2)
+ self.text.set_property('y', self.y)
+ self.text.set_property('text', caption)
+ self.text.set_property('fill_color', the_event_text_color)
+ self.text.set_property('tooltip', tooltip)
+
+ # Clip the text.
+ x2, y2 = self.x + self.width, self.y + self.height,
+ path = 'M%s,%s L%s,%s L%s,%s L%s,%s Z' % (self.x, self.y, self.x, y2,
+ x2, y2, x2, self.y)
+ self.text.set_property('clip_path', path)
+
+ def update_all_day_event(self):
+ self.width = max(self.width, 0)
+ startdate = self.event.start.strftime('%x')
+ starttime = self.event.start.strftime(self.time_format)
+ if self.event.end:
+ enddate = self.event.end.strftime('%x')
+ endtime = self.event.end.strftime(self.time_format)
+
+ if self.event.all_day:
+ caption = self.event.caption
+ if not self.event.end:
+ tooltip = '%s\n%s' % (startdate, caption)
+ else:
+ tooltip = '%s - %s\n%s' % (startdate, enddate, caption)
+ elif self.event.multidays:
+ caption = '%s %s' % (starttime, self.event.caption)
+ tooltip = '%s %s - %s %s\n%s' % (startdate, starttime, enddate,
+ endtime, self.event.caption)
+ else:
+ caption = '%s %s' % (starttime, self.event.caption)
+ tooltip = '%s - %s\n%s' % (starttime, endtime, self.event.caption)
+ caption = '' if self.no_caption else caption
+ the_event_bg_color = self.event.bg_color
+ self.text.set_property('text', caption)
+ logical_height = self.text.get_natural_extents()[1][3]
+ self.height = logical_height / pango.SCALE
+
+ # Choose text color.
+ if self.event.text_color is None:
+ the_event_text_color = self.text_color
+ else:
+ the_event_text_color = self.event.text_color
+
+ if the_event_bg_color is not None:
+ self.box.set_property('x', self.x)
+ self.box.set_property('y', self.y)
+ self.box.set_property('width', self.width)
+ self.box.set_property('height', self.height)
+ self.box.set_property('stroke_color', the_event_bg_color)
+ bg_rgba_color = self.box.get_property('stroke_color_rgba')
+ bg_colors = list(util.rgba_to_colors(bg_rgba_color))
+ bg_colors[:3] = map(min,
+ map(add, bg_colors[:3], (64,) * 3), (255,) * 3)
+ bg_colors = util.colors_to_rgba(*bg_colors)
+ self.box.set_property('fill_color_rgba', bg_colors)
+ transparent_color = self.box.get_property('fill_color_rgba') - 128
+ if self.transparent:
+ self.box.set_property('stroke_color_rgba', transparent_color)
+ self.box.set_property('fill_color_rgba', transparent_color)
+ self.box.set_property('tooltip', tooltip)
+
+ # Print the event name into the title box.
+ self.text.set_property('x', self.x + 2)
+ self.text.set_property('y', self.y)
+ self.text.set_property('fill_color', the_event_text_color)
+ self.text.set_property('tooltip', tooltip)
+
+ # Clip the text.
+ x2, y2 = self.x + self.width, self.y + self.height,
+ path = 'M%s,%s L%s,%s L%s,%s L%s,%s Z' % (
+ self.x, self.y, self.x, y2, x2, y2, x2, self.y)
+ self.text.set_property('clip_path', path)
+
+
+class TimelineItem(goocanvas.Group):
+ """
+ A canvas item representing a timeline.
+ """
+
+ def __init__(self, cal, **kwargs):
+ super(TimelineItem, self).__init__()
+
+ self._cal = cal
+ self.x = kwargs.get('x')
+ self.y = kwargs.get('y')
+ self.line_color = kwargs.get('line_color')
+ self.bg_color = kwargs.get('bg_color')
+ self.text_color = kwargs.get('text_color')
+ self.time_format = kwargs.get('time_format')
+ self.width = 0
+
+ # Create canvas items.
+ self._timeline_rect = {}
+ self._timeline_text = {}
+ for n in range(24):
+ caption = datetime.time(n).strftime(self.time_format)
+ self._timeline_rect[n] = goocanvas.Rect(parent=self)
+ self._timeline_text[n] = goocanvas.Text(parent=self, text=caption)
+
+ if self.x is not None:
+ self.update()
+
+ @property
+ def min_line_height(self):
+ logical_height = 0
+ self.ink_padding_top = 0
+ for n in range(24):
+ natural_extents = self._timeline_text[n].get_natural_extents()
+ logical_rect = natural_extents[1]
+ logical_height = max(logical_height, logical_rect[3])
+ ink_rect = natural_extents[0]
+ self.ink_padding_top = max(self.ink_padding_top, ink_rect[0])
+ line_height = int(math.ceil(float(logical_height) / pango.SCALE))
+ return line_height
+
+ @property
+ def line_height(self):
+ self.padding_top = 0
+ line_height = self.min_line_height
+ if line_height < self.height / 24:
+ line_height = self.height / 24
+ pango_size = self._cal.get_style().font_desc.get_size()
+ padding_top = (line_height - pango_size / pango.SCALE) / 2
+ padding_top -= int(math.ceil(float(self.ink_padding_top) /
+ pango.SCALE))
+ self.padding_top = padding_top
+ return line_height
+
+ def _compute_width(self):
+ style = self._cal.get_style()
+ font = style.font_desc
+ ink_padding_left = 0
+ ink_max_width = 0
+ for n in range(24):
+ self._timeline_text[n].set_property('font', font)
+ natural_extents = self._timeline_text[n].get_natural_extents()
+ ink_rect = natural_extents[0]
+ ink_padding_left = max(ink_padding_left, ink_rect[0])
+ ink_max_width = max(ink_max_width, ink_rect[2])
+ self.width = int(math.ceil(float(ink_padding_left + ink_max_width)
+ / pango.SCALE))
+
+ def update(self):
+ self._compute_width()
+ line_height = self.line_height
+
+ # Draw the timeline.
+ for n in range(24):
+ rect = self._timeline_rect[n]
+ text = self._timeline_text[n]
+ y = self.y + n * line_height
+
+ rect.set_property('x', self.x)
+ rect.set_property('y', y)
+ rect.set_property('width', self.width)
+ rect.set_property('height', line_height)
+ rect.set_property('stroke_color', self.line_color)
+ rect.set_property('fill_color', self.bg_color)
+
+ text.set_property('x', self.x)
+ text.set_property('y', y + self.padding_top)
+ text.set_property('fill_color', self.text_color)
diff --git a/goocalendar/_event.py b/goocalendar/_event.py
new file mode 100644
index 0000000..7d8f59b
--- /dev/null
+++ b/goocalendar/_event.py
@@ -0,0 +1,97 @@
+#This file is part of GooCalendar. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+import gobject
+
+import util
+from .util import total_ordering
+
+
+ at total_ordering
+class Event(object):
+ """
+ This class represents an event that can be displayed in the calendar.
+ """
+
+ def __init__(self, caption, start, end=None, **kwargs):
+ assert caption is not None
+ assert start is not None
+ self.id = None
+ self.caption = caption
+ self.start = start
+ self.end = end
+ self.all_day = kwargs.get('all_day', False)
+ self.text_color = kwargs.get('text_color', None)
+ self.bg_color = kwargs.get('bg_color', 'orangered')
+ self.event_items = []
+ if end is None:
+ self.all_day = True
+
+ @property
+ def multidays(self):
+ if not self.end:
+ return False
+ return (self.end - self.start).days > 0
+
+ def __eq__(self, other_event):
+ if not isinstance(other_event, Event):
+ raise NotImplemented
+ return (self.start, self.end) == (other_event.start, other_event.start)
+
+ def __lt__(self, other_event):
+ if not isinstance(other_event, Event):
+ raise NotImplemented
+ return (self.start, self.end) < (other_event.start, other_event.start)
+
+
+class EventStore(gobject.GObject):
+ __gsignals__ = {
+ 'event-removed': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,)),
+ 'event-added': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (gobject.TYPE_PYOBJECT,)),
+ 'events-cleared': (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE, ())}
+
+ def __init__(self):
+ super(EventStore, self).__init__()
+ self._next_event_id = 0
+ self._events = {}
+
+ def remove(self, event):
+ assert event is not None
+ if event.id is None:
+ return
+ del self._events[event.id]
+ self.emit('event-removed', event)
+
+ def add(self, event):
+ assert event is not None
+ self.add_events([event])
+
+ def add_events(self, events):
+ for event in events:
+ assert event.id is None
+ self._events[self._next_event_id] = event
+ event.id = self._next_event_id
+ self._next_event_id += 1
+ self.emit('event-added', events)
+
+ def clear(self):
+ self._events.clear()
+ self._next_event_id = 0
+ self.emit('events-cleared')
+
+ def get_events(self, start=None, end=None):
+ """
+ Returns a list of all events that intersect with the given start
+ and end times.
+ """
+ if not start and not end:
+ return self._events.values()
+ events = []
+ for event in self._events.values():
+ if util.event_intersects(event, start, end):
+ events.append(event)
+ return events
diff --git a/goocalendar/util.py b/goocalendar/util.py
new file mode 100644
index 0000000..f332f17
--- /dev/null
+++ b/goocalendar/util.py
@@ -0,0 +1,241 @@
+#This file is part of GooCalendar. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+import sys
+import datetime
+
+
+def my_weekdatescalendar(cal, date):
+ weeks = cal.monthdatescalendar(date.year, date.month)
+ for weekno, week in enumerate(weeks):
+ # Hide all days that are not part of the current week.
+ if date in week:
+ return week
+ raise Exception('No such week')
+
+
+def my_monthdatescalendar(cal, date):
+ # Months that have only five weeks are filled with another week from
+ # the following month.
+ weeks = cal.monthdatescalendar(date.year, date.month)
+ if len(weeks) < 6:
+ last_day = weeks[-1][-1]
+ offset = datetime.timedelta(1)
+ week = []
+ for i in range(7):
+ last_day += offset
+ week.append(last_day)
+ weeks.append(week)
+ return weeks
+
+
+def first_day_of_week(cal, date):
+ firstweekday = cal.firstweekday
+ day_shift = (date.weekday() + 7 - firstweekday) % 7
+ return date - datetime.timedelta(day_shift)
+
+
+def previous_month(cal, date):
+ year, month, day = date.timetuple()[:3]
+ if month == 1:
+ year -= 1
+ month = 12
+ else:
+ month -= 1
+ prev_month_days = [d for d in cal.itermonthdays(year, month)]
+ if day not in prev_month_days:
+ day = max(prev_month_days)
+ return datetime.datetime(year, month, day)
+
+
+def next_month(cal, date):
+ year, month, day = date.timetuple()[:3]
+ if month == 12:
+ year += 1
+ month = 1
+ else:
+ month += 1
+ next_month_days = [d for d in cal.itermonthdays(year, month)]
+ if day not in next_month_days:
+ day = max(next_month_days)
+ return datetime.datetime(year, month, day)
+
+
+def same_month(date1, date2):
+ return (date1.year == date2.year and date1.month == date2.month)
+
+
+def previous_week(cal, date):
+ return date - datetime.timedelta(7)
+
+
+def next_week(cal, date):
+ return date + datetime.timedelta(7)
+
+
+def time_delta(datetime1, datetime2):
+ delta = datetime1 - datetime2
+ if delta < datetime.timedelta():
+ return -delta
+ return delta
+
+
+def event_days(event1, event2):
+ end1 = event1.end if event1.end else event1.start
+ end2 = event2.end if event2.end else event2.start
+ return (time_delta(event1.start, end1).days
+ - time_delta(event2.start, end2).days)
+
+
+def event_intersects(event, start, end=None):
+ end = end if end else start
+ event_end = event.end if event.end else event.start
+ return ((event.start >= start and event.start < end)
+ or (event_end > start and event_end <= end)
+ or (event.start < start and event_end > end))
+
+
+def get_intersection_list(list, start, end):
+ intersections = []
+ for event in list:
+ if event_intersects(event, start, end):
+ intersections.append(event)
+ return intersections
+
+
+def count_intersections(list, start, end):
+ intersections = 0
+ for event in list:
+ if event_intersects(event, start, end):
+ intersections += 1
+ return intersections
+
+
+def count_parallel_events(list, start, end):
+ """
+ Given a list of events, this function returns the maximum number of
+ parallel events in the given timeframe.
+ """
+ parallel = 0
+ i = 0
+ for i, event1 in enumerate(list):
+ if not event_intersects(event1, start, end):
+ continue
+ parallel = max(parallel, 1)
+ for f in range(i + 1, len(list)):
+ event2 = list[f]
+ new_start = max(event1.start, event2.start)
+ new_end = min(event1.end, event2.end)
+ if (event_intersects(event2, start, end)
+ and event_intersects(event2, new_start, new_end)):
+ n = count_parallel_events(list[f:], new_start, new_end)
+ parallel = max(parallel, n + 1)
+ return parallel
+
+
+def next_level(cur_time, min_per_level):
+ """
+ Given a datetime and the duration (in minutes) of time levels,
+ return the datetime of the next level
+ """
+ delta_per_level = datetime.timedelta(minutes=min_per_level)
+ delta_min = cur_time.minute % min_per_level
+ delta_sec = cur_time.second
+ cur_delta = datetime.timedelta(minutes=delta_min, seconds=delta_sec)
+ next_level = cur_time - cur_delta + delta_per_level
+ return next_level
+
+
+def prev_level(cur_time, min_per_level):
+ """
+ Given a datetime and the duration (in minutes) of time levels,
+ return the datetime of the previous level
+ """
+ delta_per_level = datetime.timedelta(minutes=min_per_level)
+ delta_min = cur_time.minute % min_per_level
+ cur_delta = datetime.timedelta(minutes=delta_min)
+ prev_level = cur_time - cur_delta
+ if prev_level == cur_time:
+ prev_level -= delta_per_level
+ return prev_level
+
+
+def color_to_string(color):
+ hexstring = "#%02X%02X%02X" % (
+ color.red / 256, color.blue / 256, color.green / 256)
+ return hexstring
+
+
+def colors_to_rgba(red, green, blue, alpha):
+ values = [alpha, blue, green, red]
+ rgba_color = 0
+ base = 1
+ for value in values:
+ rgba_color += value * base
+ base *= 256
+ return rgba_color
+
+
+def rgba_to_colors(rgba):
+ i = 0
+ colors = []
+ base = 256
+ prev_base = 1
+ while i < 4:
+ value = (rgba % base) / prev_base
+ colors.append(value)
+ rgba -= value
+ prev_base = base
+ base *= 256
+ i += 1
+ return colors[3], colors[2], colors[1], colors[0]
+
+
+def left_click(func):
+ def wrapper(*args):
+ if args[-1].button != 1: # left mouse button is required
+ return
+ return func(*args)
+ return wrapper
+
+
+if sys.version_info >= (2, 7):
+ from functools import total_ordering
+else:
+ # This code comes from python standard library
+ def total_ordering(cls):
+ """Class decorator that fills in missing ordering methods"""
+ convert = {
+ '__lt__': [
+ ('__gt__', lambda self, other:
+ not (self < other or self == other)),
+ ('__le__', lambda self, other: self < other or self == other),
+ ('__ge__', lambda self, other: not self < other)],
+ '__le__': [
+ ('__ge__', lambda self, other:
+ not self <= other or self == other),
+ ('__lt__', lambda self, other:
+ self <= other and not self == other),
+ ('__gt__', lambda self, other: not self <= other)],
+ '__gt__': [
+ ('__lt__', lambda self, other:
+ not (self > other or self == other)),
+ ('__ge__', lambda self, other: self > other or self == other),
+ ('__le__', lambda self, other: not self > other)],
+ '__ge__': [
+ ('__le__', lambda self, other:
+ (not self >= other) or self == other),
+ ('__gt__', lambda self, other:
+ self >= other and not self == other),
+ ('__lt__', lambda self, other: not self >= other)]
+ }
+ roots = set(dir(cls)) & set(convert)
+ if not roots:
+ raise ValueError(
+ 'must define at least one ordering operation: < > <= >=')
+ root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__
+ for opname, opfunc in convert[root]:
+ if opname not in roots:
+ opfunc.__name__ = opname
+ opfunc.__doc__ = getattr(int, opname).__doc__
+ setattr(cls, opname, opfunc)
+ return cls
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..861a9f5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build =
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..f51b0f5
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#This file is part of GooCalendar. The COPYRIGHT file at the top level of
+#this repository contains the full copyright notices and license terms.
+
+import os
+from setuptools import setup, find_packages
+
+
+def read(fname):
+ return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+setup(name='GooCalendar',
+ version='0.1',
+ author='Cédric Krier',
+ author_email='cedric.krier at b2ck.com',
+ url='http://code.google.com/p/goocalendar/',
+ description='A calendar widget for GTK using PyGoocanvas',
+ long_description=read('README'),
+ download_url='http://code.google.com/p/goocalendar/downloads/',
+ packages=find_packages(),
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: X11 Applications :: GTK',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python :: 2 :: Only',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+ 'Topic :: Software Development :: Widget Sets',
+ ],
+ license='GPL-2',
+ )
--
goocalendar
More information about the tryton-debian-vcs
mailing list