From a6161308da868451f2250df8e6b11823953122ff Mon Sep 17 00:00:00 2001 From: Alan Aguiar Date: Sun, 21 Oct 2012 08:55:07 +0000 Subject: add new timeline 0.18 --- diff --git a/CHANGES b/CHANGES index 6bfe05a..e02eb9f 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,20 @@ +Timeline 0.18.0, released on 30 September 2012 +============================================== + +New features, enhancements: + + * Zooming with scroll wheel zooms at cursor position instead of center. + +Bug fixes: + + * Adding multiple events without closing event dialog, works again. + * Alert time comparision problem solved + * Fixed problem with ends-today property + * Fit millennium now works close to edges + * Fit century now works close to edges + + + Timeline 0.17.0, released on 15 June 2012 ========================================= @@ -10,7 +27,7 @@ New features, enhancements: Bug fixes: - * No Error when fitting month, december, when using extended timetype. + * No Error when fitting month, december, when using extended timetype. Timeline 0.16.0, released on 31 January 2012 ============================================ diff --git a/HACKING b/HACKING index 86b721c..ce9049b 100644 --- a/HACKING +++ b/HACKING @@ -53,6 +53,7 @@ Features for the next version (x.y+1) can continue to be developed in main. 1. version.py 2. CHANGES 3. Run `python execute-specs.py` to find where else you need to modify + 4. Commit and push Work on stable -------------- @@ -62,6 +63,9 @@ Work on stable 1. Request download from here (login required) http://translations.launchpad.net/thetimelineproj/trunk/+translations 2. Run `python import-po-from-launchpad-export.py /path/to/launchpad-export.tar.gz` + 3. Upload new pot-file + Create new po-file with the command + scons pot 3. Check that information and version numbers are correct in 1. version.py 2. CHANGES diff --git a/README b/README index 29ff32b..8d7ac6c 100644 --- a/README +++ b/README @@ -1,4 +1,4 @@ -This directory contains the 0.17.0 release of Timeline. +This directory contains the 0.18.0 release of Timeline. Timeline is a cross-platform application for displaying and navigating events on a timeline. diff --git a/execute-specs.py b/execute-specs.py index d863ec8..9fd9fcf 100755 --- a/execute-specs.py +++ b/execute-specs.py @@ -32,6 +32,7 @@ def execute_all_specs(): def setup_paths(): root_dir = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, os.path.join(root_dir, "libs", "dev", "mock-0.7.2")) + sys.path.insert(0, os.path.join(root_dir, "libs", "dependencies", "icalendar-2.1")) def install_gettext_in_builtin_namespace(): def _(message): @@ -57,7 +58,7 @@ def add_specs(suite): load_test_cases_from_module_name(suite, abs_module_name) def add_doctests(suite): - load_doc_test_from_module_name(suite, "timelinelib.db.backends.xmlparser") + load_doc_test_from_module_name(suite, "timelinelib.xml.parser") load_doc_test_from_module_name(suite, "timelinelib.utils") def load_test_cases_from_module_name(suite, module_name): diff --git a/libs/dependencies/icalendar-2.1/icalendar/prop.py b/libs/dependencies/icalendar-2.1/icalendar/prop.py index 9722bf0..50840d3 100644 --- a/libs/dependencies/icalendar-2.1/icalendar/prop.py +++ b/libs/dependencies/icalendar-2.1/icalendar/prop.py @@ -73,7 +73,7 @@ class vBinary: 'This is gibberish' The roundtrip test - >>> x = 'Binary data æ ø Ã¥ \x13 \x56' + >>> x = 'Binary data æ ø å \x13 \x56' >>> vBinary(x).ical() 'QmluYXJ5IGRhdGEg5iD4IOUgEyBW' >>> vBinary.from_ical('QmluYXJ5IGRhdGEg5iD4IOUgEyBW') @@ -1055,12 +1055,12 @@ class vText(unicode): If you pass a unicode object, it will be utf-8 encoded. As this is the (only) standard that RFC 2445 support. - >>> t = vText(u'international chars æøå ÆØÅ ü') + >>> t = vText(u'international chars æøå ÆØÅ ü') >>> t.ical() 'international chars \\xc3\\xa6\\xc3\\xb8\\xc3\\xa5 \\xc3\\x86\\xc3\\x98\\xc3\\x85 \\xc3\\xbc' Unicode is converted to utf-8 - >>> t = vText(u'international æ ø Ã¥') + >>> t = vText(u'international æ ø å') >>> str(t) 'international \\xc3\\xa6 \\xc3\\xb8 \\xc3\\xa5' @@ -1386,12 +1386,12 @@ class TypesFactory(CaselessDict): datetime.datetime(2005, 1, 1, 12, 30) It can also be used to directly encode property and parameter values - >>> comment = factory.ical('comment', u'by Rasmussen, Max Møller') + >>> comment = factory.ical('comment', u'by Rasmussen, Max Møller') >>> str(comment) 'by Rasmussen\\\\, Max M\\xc3\\xb8ller' >>> factory.ical('priority', 1) '1' - >>> factory.ical('cn', u'Rasmussen, Max Møller') + >>> factory.ical('cn', u'Rasmussen, Max Møller') 'Rasmussen\\\\, Max M\\xc3\\xb8ller' >>> factory.from_ical('cn', 'Rasmussen\\\\, Max M\\xc3\\xb8ller') diff --git a/libs/dependencies/pysvg-0.2.1/pysvg/__init__.py b/libs/dependencies/pysvg-0.2.1/pysvg/__init__.py index ed916d5..0a81d9f 100644 --- a/libs/dependencies/pysvg-0.2.1/pysvg/__init__.py +++ b/libs/dependencies/pysvg-0.2.1/pysvg/__init__.py @@ -11,4 +11,4 @@ - + \ No newline at end of file diff --git a/libs/dependencies/pysvg-0.2.1/pysvg/linking.py b/libs/dependencies/pysvg-0.2.1/pysvg/linking.py index ed003b1..3f4fc5a 100644 --- a/libs/dependencies/pysvg-0.2.1/pysvg/linking.py +++ b/libs/dependencies/pysvg-0.2.1/pysvg/linking.py @@ -64,4 +64,4 @@ class view(BaseElement, CoreAttrib, ExternalAttrib): def set_viewTarget(self,viewTarget): self._attributes['viewTarget']=viewTarget def get_viewTarget(self): - return self._attributes['viewTarget'] + return self._attributes['viewTarget'] \ No newline at end of file diff --git a/libs/dependencies/pysvg-0.2.1/pysvg/shape.py b/libs/dependencies/pysvg-0.2.1/pysvg/shape.py index 5b87251..c61058d 100644 --- a/libs/dependencies/pysvg-0.2.1/pysvg/shape.py +++ b/libs/dependencies/pysvg-0.2.1/pysvg/shape.py @@ -478,4 +478,4 @@ class polygon(polyline): def __init__(self, points=None, **kwargs): BaseElement.__init__(self,'polygon') self.set_points(points) - self.setKWARGS(**kwargs) + self.setKWARGS(**kwargs) \ No newline at end of file diff --git a/libs/dependencies/pysvg-0.2.1/pysvg/turtle.py b/libs/dependencies/pysvg-0.2.1/pysvg/turtle.py index febcada..a15a16c 100644 --- a/libs/dependencies/pysvg-0.2.1/pysvg/turtle.py +++ b/libs/dependencies/pysvg-0.2.1/pysvg/turtle.py @@ -203,4 +203,4 @@ class Turtle(object): for element in self.getSVGElements(): svgContainer.addElement(element) return svgContainer - + \ No newline at end of file diff --git a/specs/AlertController.py b/specs/AlertController.py index 2e57c68..4b916d6 100644 --- a/specs/AlertController.py +++ b/specs/AlertController.py @@ -37,27 +37,24 @@ class AlertControllerSpec(unittest.TestCase): self.given_early_pytimes() self.given_controller_time_type(PyTimeType()) time_as_text = "%s" % self.tm - expired = self.controller._time_has_expired(time_as_text) + expired = self.controller._time_has_expired(self.tm) self.assertTrue(expired) - + def test_pytime_has_not_expired(self): self.given_late_pytimes() - time_as_text = "%s" % self.tm - expired = self.controller._time_has_expired(time_as_text) + expired = self.controller._time_has_expired(self.tm) self.assertFalse(expired) - + def test_wxtime_has_expired(self): self.given_early_wxtimes() self.given_controller_time_type(WxTimeType()) - time_as_text = "%s" % self.tm - expired = self.controller._time_has_expired(time_as_text) + expired = self.controller._time_has_expired(self.tm) self.assertTrue(expired) - + def test_wxtime_has_not_expired(self): self.given_late_wxtimes() self.given_controller_time_type(WxTimeType()) - time_as_text = "%s" % self.tm - expired = self.controller._time_has_expired(time_as_text) + expired = self.controller._time_has_expired(self.tm) self.assertFalse(expired) def given_early_pytimes(self): @@ -89,24 +86,24 @@ class AlertControllerSpec(unittest.TestCase): def given_wxtime_later(self): self.tm = self.now + wx.TimeSpan(hours=12) - + def given_wxtime_earlier(self): self.tm = self.now - wx.TimeSpan(hours=12) - + def given_pytime_now(self): self.now = PyTimeType().now() - + def given_pytime_later(self): self.tm = self.now + datetime.timedelta(days=1) - + def given_pytime_earlier(self): self.tm = self.now + datetime.timedelta(days=-1) def given_controller_time_type(self, time_type): self.controller.time_type = time_type - + def setUp(self): self.now = PyTimeType().now() self.alert = (self.now, "Time to go") - self.event = an_event() + self.event = an_event() self.controller = AlertController() diff --git a/specs/Backend.py b/specs/Backend.py new file mode 100644 index 0000000..3bac5fd --- /dev/null +++ b/specs/Backend.py @@ -0,0 +1,72 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +import unittest + +from specs.utils import TmpDirTestCase +from timelinelib.db.backends.ics import IcsTimeline +from timelinelib.db.backends.memory import MemoryDB + + +class BackendTest(object): + + # These tests should work for any backend. They are tested with different + # backends below. + + def test_get_all_events_returns_a_list(self): + all_events = self.backend.get_all_events() + self.assertTrue(isinstance(all_events, list)) + + def test_has_time_type_method(self): + self.backend.get_time_type() + + +class MemoryBackendTest(unittest.TestCase, BackendTest): + + def setUp(self): + self.backend = MemoryDB() + + +class IcsBackendTest(TmpDirTestCase, BackendTest): + + def setUp(self): + TmpDirTestCase.setUp(self) + self.backend = IcsTimeline(self.write_ics_content()) + + def write_ics_content(self): + tmp_path = self.get_tmp_path("test.ics") + f = open(tmp_path, "w") + f.write(ICS_EXAMPLE) + f.close() + return tmp_path + + +ICS_EXAMPLE = """ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//hacksw/handcal//NONSGML v1.0//EN +BEGIN:VEVENT +UID:uid1@example.com +DTSTAMP:19970714T170000Z +ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com +DTSTART:19970714T170000Z +DTEND:19970715T035959Z +SUMMARY:Bastille Day Party +END:VEVENT +END:VCALENDAR +""" diff --git a/specs/CategoriesTree.py b/specs/CategoriesTree.py index 9c76bdf..62a08cc 100644 --- a/specs/CategoriesTree.py +++ b/specs/CategoriesTree.py @@ -20,7 +20,7 @@ import unittest from mock import Mock -from timelinelib.db.interface import TimelineDB +from timelinelib.db.backends.memory import MemoryDB from timelinelib.db.objects import Category from timelinelib.wxgui.components.cattree import CategoriesTree from timelinelib.wxgui.components.cattree import CategoriesTreeController @@ -51,7 +51,7 @@ class describe_categories_tree_control(unittest.TestCase): self.controller.initialize_from_timeline_view(None) def setUp(self): - self.db = Mock(TimelineDB) + self.db = Mock(MemoryDB) self.foo = Category("foo", (255, 0, 0), None, True, parent=None) self.foofoo = Category("foofoo", (255, 0, 0), None, True, parent=self.foo) self.bar = Category("bar", (255, 0, 0), None, True, parent=None) diff --git a/specs/CategoryEditor.py b/specs/CategoryEditor.py index 3941026..30e47a5 100644 --- a/specs/CategoryEditor.py +++ b/specs/CategoryEditor.py @@ -20,7 +20,7 @@ import unittest from mock import Mock -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Category from timelinelib.editors.category import CategoryEditor from timelinelib.repositories.interface import CategoryRepository diff --git a/specs/CategorySorter.py b/specs/CategorySorter.py index 37c4021..88eedaa 100644 --- a/specs/CategorySorter.py +++ b/specs/CategorySorter.py @@ -18,8 +18,8 @@ import unittest +from timelinelib.db.objects.category import sort_categories from timelinelib.db.objects import Category -from timelinelib.domain.category import sort_categories class CategorySorter(unittest.TestCase): diff --git a/specs/ContainerEditor.py b/specs/ContainerEditor.py index bec39b6..d01b34e 100644 --- a/specs/ContainerEditor.py +++ b/specs/ContainerEditor.py @@ -21,11 +21,11 @@ import unittest from mock import Mock, sentinel from specs.utils import an_event_with, human_time_to_py, ObjectWithTruthValue +from timelinelib.db.backends.memory import MemoryDB +from timelinelib.db.objects import Container from timelinelib.editors.container import ContainerEditor from timelinelib.repositories.interface import EventRepository from timelinelib.wxgui.dialogs.eventeditor import ContainerEditorDialog -from timelinelib.db.backends.memory import MemoryDB -from timelinelib.db.container import Container class ContainerEditorTestCase(unittest.TestCase): @@ -42,25 +42,24 @@ class ContainerEditorTestCase(unittest.TestCase): self.given_editor_without_container() self.view.set_name.assert_called_with("") self.view.set_category.assert_called_with(None) - + def testConstructionWithContainer(self): self.given_editor_with_container() self.view.set_name.assert_called_with("Container1") self.view.set_category.assert_called_with(None) - + def testContainerCreated(self): self.given_editor_without_container() self.editor.save() self.view.get_name.assert_called() self.view.get_category.assert_called() self.assertFalse(self.editor.container == None) - + def given_editor_without_container(self): self.editor = ContainerEditor(self.view, self.db, None) def given_editor_with_container(self): self.editor = ContainerEditor(self.view, self.db, self.container) - + def time(self, tm): return self.db.get_time_type().parse_time(tm) - diff --git a/specs/ContainerObject.py b/specs/ContainerObject.py index bca851a..c1bbf9d 100644 --- a/specs/ContainerObject.py +++ b/specs/ContainerObject.py @@ -18,10 +18,10 @@ import unittest -from timelinelib.db.objects import Category -from timelinelib.db.container import Container -from timelinelib.db.subevent import Subevent from timelinelib.db.backends.memory import MemoryDB +from timelinelib.db.objects import Category +from timelinelib.db.objects import Container +from timelinelib.db.objects import Subevent class ContainerSpec(unittest.TestCase): @@ -49,7 +49,7 @@ class ContainerSpec(unittest.TestCase): def testNameAndCategoryCanBeUpdated(self): self.given_default_container() new_name = "new text" - new_category = Category("cat", (255,0,0), (255,0,0), True) + new_category = Category("cat", (255,0,0), (255,0,0), True) self.container.update_properties(new_name, new_category) self.assertEqual(new_category, self.container.category) @@ -62,12 +62,12 @@ class ContainerSpec(unittest.TestCase): self.container = Container(self.db.get_time_type(), self.now, self.now, "container") def given_period_subevent(self): - self.event = Subevent(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), + self.event = Subevent(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), self.time("2000-01-03 10:01:01"), "evt") def time(self, tm): return self.db.get_time_type().parse_time(tm) - + def setUp(self): self.db = MemoryDB() self.now = self.db.get_time_type().now() @@ -98,4 +98,3 @@ class ContainerConstructorSpec(unittest.TestCase): def setUp(self): self.db = MemoryDB() self.now = self.db.get_time_type().now() - diff --git a/specs/ContainerStrategyInterface.py b/specs/ContainerStrategyInterface.py index 1d6da79..755616a 100644 --- a/specs/ContainerStrategyInterface.py +++ b/specs/ContainerStrategyInterface.py @@ -22,11 +22,11 @@ from timelinelib.db.interface import ContainerStrategy class ContainerStrategyInterfaceSpec(unittest.TestCase): - + def testConstruction(self): self.given_strategy_with_none_container() self.assertEqual(None, self.strategy.container) - + def testRegisterSubeventNotImplemented(self): self.given_strategy_with_none_container() self.assertRaises(NotImplementedError, self.strategy.register_subevent, None) @@ -34,14 +34,13 @@ class ContainerStrategyInterfaceSpec(unittest.TestCase): def testUnregisterSubeventNotImplemented(self): self.given_strategy_with_none_container() self.assertRaises(NotImplementedError, self.strategy.unregister_subevent, None) - + def testUpdateSubeventNotImplemented(self): self.given_strategy_with_none_container() self.assertRaises(NotImplementedError, self.strategy.update, None) - + def given_strategy_with_none_container(self): self.strategy = ContainerStrategy(None) def setUp(self): pass - diff --git a/specs/DbOpen.py b/specs/DbOpen.py index 67a58c7..85a8d76 100644 --- a/specs/DbOpen.py +++ b/specs/DbOpen.py @@ -18,12 +18,8 @@ from datetime import datetime import codecs -import os -import os.path -import shutil -import tempfile -import unittest +from specs.utils import TmpDirTestCase from timelinelib.db.backends.xmlfile import XmlTimeline from timelinelib.db import db_open from timelinelib.drawing.viewproperties import ViewProperties @@ -93,7 +89,7 @@ CONTENT_0100 = u""" """.strip() -class DbOpenSpec(unittest.TestCase): +class DbOpenSpec(TmpDirTestCase): IO = True @@ -245,11 +241,8 @@ class DbOpenSpec(unittest.TestCase): self.fail("Unknown category.") def setUp(self): - self.tmp_dir = tempfile.mkdtemp(prefix="timeline-test") - self.tmp_path = os.path.join(self.tmp_dir, "test.timeline") - - def tearDown(self): - shutil.rmtree(self.tmp_dir) + TmpDirTestCase.setUp(self) + self.tmp_path = self.get_tmp_path("test.timeline") def writeContentToTmpFile(self, content): f = codecs.open(self.tmp_path, "w", "utf-8") diff --git a/specs/DefaultContainerStrategy.py b/specs/DefaultContainerStrategy.py index b3946ef..668aca7 100644 --- a/specs/DefaultContainerStrategy.py +++ b/specs/DefaultContainerStrategy.py @@ -19,13 +19,13 @@ import unittest from timelinelib.db.backends.memory import MemoryDB -from timelinelib.db.container import Container -from timelinelib.db.subevent import Subevent +from timelinelib.db.objects import Container +from timelinelib.db.objects import Subevent from timelinelib.db.strategies import DefaultContainerStrategy class DefaultContainerStartegySpec(unittest.TestCase): - + def test_construction(self): self.given_strategy_with_container() self.assertEqual(self.container, self.strategy.container) @@ -60,7 +60,7 @@ class DefaultContainerStartegySpec(unittest.TestCase): self.strategy.update(self.subevent2) self.assert_equal_start(self.container, self.subevent1) self.assert_equal_end(self.container, self.subevent2) - + def test_adding_partial_overlapping_event_moves_overlapped_event_backwards(self): # Container event: +-------+ # New sub-event: +-------+ @@ -84,27 +84,27 @@ class DefaultContainerStartegySpec(unittest.TestCase): # New sub-event: +---+ self.given_container_with_two_events_with_same_start_time() self.assert_start_equals_end(self.subevent1, self.subevent2) - + def test_overlapping_nonperiod_event_at_begining_moves_nonperiod_event_backwards(self): # Container event: + # New sub-event: +----------+ self.given_strategy_with_container() self.given_event_overlapping_point_event() self.assert_start_equals_start(self.subevent1, self.subevent2) - + def test_overlapping_nonperiod_event_at_end_moves_nonperiod_event_forward(self): # Container event: + # New sub-event: +----------+ self.given_strategy_with_container() self.given_event_overlapping_point_event2() self.assert_start_equals_end(self.subevent1, self.subevent2) - + def given_container_with_two_events_with_nonoverlapping_periods(self): self.given_strategy_with_container() self.given_two_events_with_nonoverlapping_periods() self.strategy.register_subevent(self.subevent1) self.strategy.register_subevent(self.subevent2) - + def given_container_with_two_events_with_overlapping_periods(self): self.given_strategy_with_container() self.given_two_overlapping_events() @@ -130,85 +130,84 @@ class DefaultContainerStartegySpec(unittest.TestCase): self.strategy.register_subevent(self.subevent2) def given_strategy_with_container(self): - self.container = Container(self.db.get_time_type(), - self.time("2000-01-01 10:01:01"), + self.container = Container(self.db.get_time_type(), + self.time("2000-01-01 10:01:01"), self.time("2000-01-01 10:01:01"), "Container1") self.strategy = DefaultContainerStrategy(self.container) def given_event_overlapping_point_event(self): - self.subevent1 = Subevent(self.db.get_time_type(), - self.time("2000-05-01 10:02:01"), + self.subevent1 = Subevent(self.db.get_time_type(), + self.time("2000-05-01 10:02:01"), self.time("2000-05-01 10:02:01"), "Container1") - self.subevent2 = Subevent(self.db.get_time_type(), - self.time("2000-05-01 10:01:01"), + self.subevent2 = Subevent(self.db.get_time_type(), + self.time("2000-05-01 10:01:01"), self.time("2000-07-01 10:01:01"), "Container1") self.strategy.register_subevent(self.subevent1) self.strategy.register_subevent(self.subevent2) def given_event_overlapping_point_event2(self): - self.subevent1 = Subevent(self.db.get_time_type(), - self.time("2000-07-01 10:00:01"), + self.subevent1 = Subevent(self.db.get_time_type(), + self.time("2000-07-01 10:00:01"), self.time("2000-07-01 10:00:01"), "Container1") - self.subevent2 = Subevent(self.db.get_time_type(), - self.time("2000-05-01 10:01:01"), + self.subevent2 = Subevent(self.db.get_time_type(), + self.time("2000-05-01 10:01:01"), self.time("2000-07-01 10:01:01"), "Container1") self.strategy.register_subevent(self.subevent1) self.strategy.register_subevent(self.subevent2) - + def given_two_overlapping_events(self): - self.subevent1 = Subevent(self.db.get_time_type(), - self.time("2000-03-01 10:01:01"), + self.subevent1 = Subevent(self.db.get_time_type(), + self.time("2000-03-01 10:01:01"), self.time("2000-06-01 10:01:01"), "Container1") - self.subevent2 = Subevent(self.db.get_time_type(), - self.time("2000-05-01 10:01:01"), + self.subevent2 = Subevent(self.db.get_time_type(), + self.time("2000-05-01 10:01:01"), self.time("2000-07-01 10:01:01"), "Container1") def given_two_events_with_same_period(self): - self.subevent1 = Subevent(self.db.get_time_type(), - self.time("2000-03-01 10:01:01"), + self.subevent1 = Subevent(self.db.get_time_type(), + self.time("2000-03-01 10:01:01"), self.time("2000-06-01 10:01:01"), "Container1") - self.subevent2 = Subevent(self.db.get_time_type(), - self.time("2000-03-01 10:01:01"), + self.subevent2 = Subevent(self.db.get_time_type(), + self.time("2000-03-01 10:01:01"), self.time("2000-06-01 10:01:01"), "Container1") def given_two_events_with_same_start_time(self): - self.subevent1 = Subevent(self.db.get_time_type(), - self.time("2000-03-01 10:01:01"), + self.subevent1 = Subevent(self.db.get_time_type(), + self.time("2000-03-01 10:01:01"), self.time("2000-06-01 10:01:01"), "Container1") - self.subevent2 = Subevent(self.db.get_time_type(), - self.time("2000-03-01 10:01:01"), + self.subevent2 = Subevent(self.db.get_time_type(), + self.time("2000-03-01 10:01:01"), self.time("2000-04-01 10:01:01"), "Container1") def given_two_events_with_nonoverlapping_periods(self): - self.subevent1 = Subevent(self.db.get_time_type(), - self.time("2000-01-01 10:01:01"), + self.subevent1 = Subevent(self.db.get_time_type(), + self.time("2000-01-01 10:01:01"), self.time("2000-02-01 10:01:01"), "Container1") - self.subevent2 = Subevent(self.db.get_time_type(), - self.time("2000-03-01 10:01:01"), + self.subevent2 = Subevent(self.db.get_time_type(), + self.time("2000-03-01 10:01:01"), self.time("2000-04-01 10:01:01"), "Container1") - + def given_subevent1(self): - self.subevent1 = Subevent(self.db.get_time_type(), - self.time("2000-01-01 10:01:01"), + self.subevent1 = Subevent(self.db.get_time_type(), + self.time("2000-01-01 10:01:01"), self.time("2000-02-01 10:01:01"), "Container1") def assert_equal_start(self, obj1, obj2): self.assertEqual(obj1.time_period.start_time, obj2.time_period.start_time) - + def assert_equal_end(self, obj1, obj2): self.assertEqual(obj1.time_period.end_time, obj2.time_period.end_time) def assert_start_equals_end(self, obj1, obj2): self.assertEqual(obj1.time_period.start_time, obj2.time_period.end_time) - + def assert_start_equals_start(self, obj1, obj2): self.assertEqual(obj1.time_period.start_time, obj2.time_period.start_time) - + def time(self, tm): return self.db.get_time_type().parse_time(tm) - + def setUp(self): self.db = MemoryDB() self.now = self.db.get_time_type().now() self.time_type = self.db.get_time_type() - diff --git a/specs/DuplicateEventDialog.py b/specs/DuplicateEventDialog.py index 1d64cc1..74ee882 100644 --- a/specs/DuplicateEventDialog.py +++ b/specs/DuplicateEventDialog.py @@ -16,22 +16,21 @@ # along with Timeline. If not, see . -import unittest import datetime +import unittest from mock import Mock -from timelinelib.db.interface import TimelineDB -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.backends.memory import MemoryDB +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Event from timelinelib.db.objects import TimePeriod -from timelinelib.wxgui.dialogs.duplicateevent import DuplicateEventDialog -from timelinelib.editors.duplicateevent import DuplicateEventEditor -from timelinelib.time import PyTimeType - -from timelinelib.editors.duplicateevent import FORWARD from timelinelib.editors.duplicateevent import BACKWARD from timelinelib.editors.duplicateevent import BOTH +from timelinelib.editors.duplicateevent import DuplicateEventEditor +from timelinelib.editors.duplicateevent import FORWARD +from timelinelib.time import PyTimeType +from timelinelib.wxgui.dialogs.duplicateevent import DuplicateEventDialog class duplicate_event_dialog_spec_base(unittest.TestCase): @@ -56,13 +55,13 @@ class duplicate_event_dialog_spec_base(unittest.TestCase): return self.move_period_fn def _create_db_mock(self): - self.db = Mock(TimelineDB) + self.db = Mock(MemoryDB) self.db.get_time_type.return_value = PyTimeType() return self.db def _create_event(self): self.event = Event( - self.db.get_time_type(), + self.db.get_time_type(), datetime.datetime(2010, 1, 1), datetime.datetime(2010, 1, 1), "foo", diff --git a/specs/EventEditor.py b/specs/EventEditor.py index d25bbbf..895c6e5 100644 --- a/specs/EventEditor.py +++ b/specs/EventEditor.py @@ -51,7 +51,7 @@ class EventEditorTestCase(unittest.TestCase): def when_editor_opened_with(self, start, end, event): self.editor = EventEditor(self.view) - self.editor.edit(PyTimeType(), self.event_repository, self.timeline, + self.editor.edit(PyTimeType(), self.event_repository, self.timeline, start, end, event) def simulate_user_enters_start_time(self, time): @@ -153,6 +153,26 @@ class describe_event_editor__locked_checkbox(EventEditorTestCase): self.when_editor_opened_with_event(event) self.view.set_locked.assert_called_with(sentinel.LOCKED) + def test_new_event_starting_in_history(self): + self.when_editor_opened_with_time("1 Jan 3010") + self.assertFalse(self.editor.start_is_in_history()) + + def test_new_event_not_starting_in_history(self): + self.when_editor_opened_with_time("1 Jan 2010") + self.assertTrue(self.editor.start_is_in_history()) + + def test_event_not_starting_in_history(self): + event = Mock() + event.time_period.start_time = human_time_to_py("1 Jan 3010") + self.when_editor_opened_with_event(event) + self.assertFalse(self.editor.start_is_in_history()) + + def test_event_starting_in_history(self): + event = Mock() + event.time_period.start_time = human_time_to_py("1 Jan 2010") + self.when_editor_opened_with_event(event) + self.assertTrue(self.editor.start_is_in_history()) + class describe_event_editor__ends_today_checkbox(EventEditorTestCase): diff --git a/specs/EventObject.py b/specs/EventObject.py index 1d70e75..812272d 100644 --- a/specs/EventObject.py +++ b/specs/EventObject.py @@ -26,30 +26,30 @@ class EventSpec(unittest.TestCase): def testEventPropertyEndsTodayCanBeUpdated(self): self.given_default_point_event() - self.event.update(self.now, self.now, "evt", ends_today=True) + self.event.update(self.now, self.now, "evt", ends_today=True) self.assertEqual(True, self.event.ends_today) def testEventPropertyFuzzyCanBeUpdated(self): self.given_default_point_event() - self.event.update(self.now, self.now, "evt", fuzzy=True) + self.event.update(self.now, self.now, "evt", fuzzy=True) self.assertEqual(True, self.event.fuzzy) def testEventPropertyLockedCanBeUpdated(self): self.given_default_point_event() - self.event.update(self.now, self.now, "evt", locked=True) + self.event.update(self.now, self.now, "evt", locked=True) self.assertEqual(True, self.event.locked) def testEventPropertyEndsTodayCantBeSetOnLockedEvent(self): self.given_default_point_event() - self.event.update(self.now, self.now, "evt", locked=True) - self.event.update(self.now, self.now, "evt", ends_today=True) + self.event.update(self.now, self.now, "evt", locked=True) + self.event.update(self.now, self.now, "evt", ends_today=True) self.assertEqual(False, self.event.ends_today) def testEventPropertyEndsTodayCantBeUnsetOnLockedEvent(self): self.given_default_point_event() - self.event.update(self.now, self.now, "evt", locked=True, ends_today=True) + self.event.update(self.now, self.now, "evt", locked=True, ends_today=True) self.assertEqual(True, self.event.ends_today) - self.event.update(self.now, self.now, "evt", ends_today=False) + self.event.update(self.now, self.now, "evt", ends_today=False) self.assertEqual(True, self.event.ends_today) def setUp(self): @@ -63,7 +63,7 @@ class EventSpec(unittest.TestCase): self.event = Event(self.db.get_time_type(), self.now, self.now, "evt") def given_point_event(self): - self.event = Event(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), + self.event = Event(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), self.time("2000-01-01 10:01:01"), "evt") @@ -76,7 +76,7 @@ class EventCosntructorSpec(unittest.TestCase): self.assertEqual(False, self.event.ends_today) self.assertEqual(False, self.event.is_container()) self.assertEqual(False, self.event.is_subevent()) - + def testEventPropertyFuzzyCanBeSetAtConstruction(self): self.given_fuzzy_point_event() self.assertEqual(True, self.event.fuzzy) @@ -118,4 +118,3 @@ class EventFunctionsSpec(unittest.TestCase): def setUp(self): self.db = MemoryDB() self.now = self.db.get_time_type().now() - diff --git a/specs/FileTimeline.py b/specs/FileTimeline.py index 74dee1a..19eb02d 100644 --- a/specs/FileTimeline.py +++ b/specs/FileTimeline.py @@ -16,25 +16,21 @@ # along with Timeline. If not, see . -import tempfile -import shutil -import os.path -import unittest -import os -import stat import datetime +import unittest -from timelinelib.db.interface import TimelineIOError -from timelinelib.db.objects import TimePeriod +from specs.utils import TmpDirTestCase +from timelinelib.db.backends.file import dequote from timelinelib.db.backends.file import FileTimeline from timelinelib.db.backends.file import quote -from timelinelib.db.backends.file import dequote from timelinelib.db.backends.file import split_on_semicolon -from timelinelib.time import PyTimeType +from timelinelib.db.exceptions import TimelineIOError +from timelinelib.db.objects import TimePeriod from timelinelib.drawing.viewproperties import ViewProperties +from timelinelib.time import PyTimeType -class FileTimelineSpec(unittest.TestCase): +class FileTimelineSpec(TmpDirTestCase): IO = True @@ -89,13 +85,13 @@ class FileTimelineSpec(unittest.TestCase): self.assertRaises(TimelineIOError, timeline.save_view_properties, vp) def setUp(self): + TmpDirTestCase.setUp(self) # Create temporary dir and names - self.tmp_dir = tempfile.mkdtemp(prefix="timeline-test") - self.corrupt_file = os.path.join(self.tmp_dir, "corrupt.timeline") - self.missingeof_file = os.path.join(self.tmp_dir, "missingeof.timeline") - self._021_file = os.path.join(self.tmp_dir, "021.timeline") - self.invalid_time_period_file = os.path.join(self.tmp_dir, "invalid_time_period.timeline") - self.valid_file = os.path.join(self.tmp_dir, "valid.timeline") + self.corrupt_file = self.get_tmp_path("corrupt.timeline") + self.missingeof_file = self.get_tmp_path("missingeof.timeline") + self._021_file = self.get_tmp_path("021.timeline") + self.invalid_time_period_file = self.get_tmp_path("invalid_time_period.timeline") + self.valid_file = self.get_tmp_path("valid.timeline") # Write content to files HEADER_030 = "# Written by Timeline 0.3.0 on 2009-7-23 9:40:33" HEADER_030_DEV = "# Written by Timeline 0.3.0dev on 2009-7-23 9:40:33" @@ -120,9 +116,6 @@ class FileTimelineSpec(unittest.TestCase): ] self.write_timeline(self.valid_file, valid) - def tearDown(self): - shutil.rmtree(self.tmp_dir) - def write_timeline(self, path, lines): f = file(path, "w") f.write("\n".join(lines)) diff --git a/specs/MemoryDB.py b/specs/MemoryDB.py index bed6a62..2d9cf29 100644 --- a/specs/MemoryDB.py +++ b/specs/MemoryDB.py @@ -21,13 +21,13 @@ import unittest from mock import Mock -from timelinelib.time import PyTimeType -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.backends.memory import MemoryDB +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Category from timelinelib.db.objects import Event from timelinelib.db.objects import TimePeriod -from timelinelib.db.backends.memory import MemoryDB from timelinelib.drawing.viewproperties import ViewProperties +from timelinelib.time import PyTimeType class MemoryDBSpec(unittest.TestCase): @@ -84,7 +84,7 @@ class MemoryDBSpec(unittest.TestCase): self.assertRaises(TimelineIOError, self.db.save_view_properties, vp) def testGetSetDisplayedPeriod(self): - tp = TimePeriod(self.db.get_time_type(), datetime(2010, 3, 23), + tp = TimePeriod(self.db.get_time_type(), datetime(2010, 3, 23), datetime(2010, 3, 24)) self.db._set_displayed_period(tp) # Assert that we get back the same period @@ -268,7 +268,7 @@ class MemoryDBSpec(unittest.TestCase): def testSaveNewEvent(self): self.db.save_event(self.e1) - tp = TimePeriod(self.db.get_time_type(), datetime(2010, 2, 12), + tp = TimePeriod(self.db.get_time_type(), datetime(2010, 2, 12), datetime(2010, 2, 14)) self.assertTrue(self.e1.has_id()) self.assertEqual(self.db.get_events(tp), [self.e1]) @@ -282,7 +282,7 @@ class MemoryDBSpec(unittest.TestCase): id_before = self.e1.id self.e1.text = "new text" self.db.save_event(self.e1) - tp = TimePeriod(self.db.get_time_type(), datetime(2010, 2, 12), + tp = TimePeriod(self.db.get_time_type(), datetime(2010, 2, 12), datetime(2010, 2, 14)) self.assertEqual(id_before, self.e1.id) self.assertEqual(self.db.get_events(tp), [self.e1]) @@ -300,7 +300,7 @@ class MemoryDBSpec(unittest.TestCase): self.assertEquals(self.db._save.call_count, 0) def testDeleteExistingEvent(self): - tp = TimePeriod(self.db.get_time_type(), datetime(2010, 2, 12), + tp = TimePeriod(self.db.get_time_type(), datetime(2010, 2, 12), datetime(2010, 2, 15)) self.db.save_event(self.e1) self.db.save_event(self.e2) @@ -406,10 +406,10 @@ class MemoryDBSpec(unittest.TestCase): self.db_listener = Mock() self.c1 = Category("work", (255, 0, 0), None, True) self.c2 = Category("private", (0, 255, 0), None, True) - self.e1 = Event(self.db.get_time_type(), datetime(2010, 2, 13), datetime(2010, 2, 13), + self.e1 = Event(self.db.get_time_type(), datetime(2010, 2, 13), datetime(2010, 2, 13), "holiday") - self.e2 = Event(self.db.get_time_type(), datetime(2010, 2, 14), datetime(2010, 2, 14), + self.e2 = Event(self.db.get_time_type(), datetime(2010, 2, 14), datetime(2010, 2, 14), "work starts") - self.e3 = Event(self.db.get_time_type(), datetime(2010, 2, 15), datetime(2010, 2, 16), + self.e3 = Event(self.db.get_time_type(), datetime(2010, 2, 15), datetime(2010, 2, 16), "period") self.db.register(self.db_listener) diff --git a/specs/NumTimePicker.py b/specs/NumTimePicker.py index f90c176..f5a2226 100644 --- a/specs/NumTimePicker.py +++ b/specs/NumTimePicker.py @@ -36,5 +36,5 @@ class ANumTimePicker(unittest.TestCase): def testTimeControlIsAssignedZeroIfSetWithValueNone(self): self.controller.set_value(None) - self.time_picker.set_value.assert_called_with(0) + self.time_picker.set_value.assert_called_with(0) diff --git a/specs/NumTimeType.py b/specs/NumTimeType.py index 63de73d..41b4561 100644 --- a/specs/NumTimeType.py +++ b/specs/NumTimeType.py @@ -32,17 +32,17 @@ class NumTimeTypeSpec(unittest.TestCase): def test_returns_half_delta(self): delta = 126 half_delta = self.time_type.half_delta(delta) - self.assertEquals(63, half_delta) + self.assertEquals(63, half_delta) def test_returns_margin_delta(self): delta = 24 * 12345 margin_delta = self.time_type.margin_delta(delta) - self.assertEquals(12345, margin_delta) + self.assertEquals(12345, margin_delta) def test_format_delta_1(self): delta = 1 - self.assertEquals("1", self.time_type.format_delta(delta)) + self.assertEquals("1", self.time_type.format_delta(delta)) def test_format_delta_2(self): delta = 2 - self.assertEquals("2", self.time_type.format_delta(delta)) + self.assertEquals("2", self.time_type.format_delta(delta)) diff --git a/specs/PlayController.py b/specs/PlayController.py index 7e49002..5f02694 100644 --- a/specs/PlayController.py +++ b/specs/PlayController.py @@ -21,11 +21,9 @@ import unittest from mock import Mock -from specs.utils import an_event -from timelinelib.wxgui.dialogs.playframe import PlayFrame -from timelinelib.db.interface import TimelineDB +from timelinelib.db.backends.memory import MemoryDB from timelinelib.play.playcontroller import PlayController -from timelinelib.time.pytime import PyTimeType +from timelinelib.wxgui.dialogs.playframe import PlayFrame class PlayControllerSpec(unittest.TestCase): @@ -33,7 +31,7 @@ class PlayControllerSpec(unittest.TestCase): def setUp(self): self.play_frame = Mock(PlayFrame) self.play_frame.get_view_period_length.return_value = datetime.timedelta(1) - self.timeline = Mock(TimelineDB) + self.timeline = Mock(MemoryDB) self.drawing_algorithm = Mock() self.config = Mock() self.controller = PlayController(self.play_frame, self.timeline, diff --git a/specs/PyDateTimePicker.py b/specs/PyDateTimePicker.py index 8f95cd0..83b7758 100644 --- a/specs/PyDateTimePicker.py +++ b/specs/PyDateTimePicker.py @@ -93,7 +93,7 @@ class PyDatePickerBaseFixture(unittest.TestCase): self.controller.on_text_changed() def simulate_change_insertion_point(self, new_insertion_point): - self.py_date_picker.GetSelection.return_value = (new_insertion_point, new_insertion_point) + self.py_date_picker.GetSelection.return_value = (new_insertion_point, new_insertion_point) self.py_date_picker.GetInsertionPoint.return_value = new_insertion_point def _update_insertion_point_and_selection(self, from_pos, to_pos): @@ -359,12 +359,12 @@ class PyTimePickerBaseFixture(unittest.TestCase): self.controller.on_text_changed() def simulate_change_insertion_point(self, new_insertion_point): - self.py_time_picker.GetSelection.return_value = (new_insertion_point, new_insertion_point) + self.py_time_picker.GetSelection.return_value = (new_insertion_point, new_insertion_point) self.py_time_picker.GetInsertionPoint.return_value = new_insertion_point def _update_insertion_point_and_selection(self, from_pos, to_pos): self.py_time_picker.GetInsertionPoint.return_value = from_pos - self.py_time_picker.GetSelection.return_value = (from_pos, to_pos) + self.py_time_picker.GetSelection.return_value = (from_pos, to_pos) class APyTimePicker(PyTimePickerBaseFixture): diff --git a/specs/PyTimeNavigationFunctions.py b/specs/PyTimeNavigationFunctions.py index 4b75a52..7c14b7b 100644 --- a/specs/PyTimeNavigationFunctions.py +++ b/specs/PyTimeNavigationFunctions.py @@ -37,6 +37,14 @@ class PyTimeNavigationFunctionsSpec(unittest.TestCase): self.when_navigating(fit_day_fn, "1 Jan 2010", "4 Jan 2010") self.then_period_becomes("2 Jan 2010", "3 Jan 2010") + def test_fit_first_day_should_display_the_day_that_is_in_the_center(self): + self.when_navigating(fit_day_fn, "1 Jan 10", "2 Jan 10") + self.then_period_becomes("1 Jan 10", "2 Jan 10") + + def test_fit_last_day_should_display_the_day_that_is_in_the_center(self): + self.when_navigating(fit_day_fn, "31 Dec 9989", "1 Jan 9990") + self.then_period_becomes("31 Dec 9989", "1 Jan 9990") + def test_fit_month_should_display_the_month_that_is_in_the_center(self): self.when_navigating(fit_month_fn, "1 Jan 2010", "2 Jan 2010") self.then_period_becomes("1 Jan 2010", "1 Feb 2010") @@ -45,22 +53,62 @@ class PyTimeNavigationFunctionsSpec(unittest.TestCase): self.when_navigating(fit_month_fn, "1 Dec 2010", "2 Dec 2010") self.then_period_becomes("1 Dec 2010", "1 Jan 2011") + def test_fit_first_month_december_should_display_the_month_that_is_in_the_center(self): + self.when_navigating(fit_month_fn, "1 Jan 10", "2 Jan 10") + self.then_period_becomes("1 Jan 10", "1 Feb 10") + + def test_fit_last_month_december_should_display_the_month_that_is_in_the_center(self): + self.when_navigating(fit_month_fn, "1 Dec 9989", "1 Jan 9990") + self.then_period_becomes("1 Dec 9989", "1 Jan 9990") + def test_fit_year_should_display_the_year_that_is_in_the_center(self): self.when_navigating(fit_year_fn, "1 Jan 2010", "2 Jan 2010") self.then_period_becomes("1 Jan 2010", "1 Jan 2011") + def test_fit_first_year_should_display_the_year_that_is_in_the_center(self): + self.when_navigating(fit_year_fn, "1 Jan 10", "2 Jan 10") + self.then_period_becomes("1 Jan 10", "1 Jan 11") + + def test_fit_last_year_should_display_the_year_that_is_in_the_center(self): + self.when_navigating(fit_year_fn, "1 Jan 9989", "1 Jan 9990") + self.then_period_becomes("1 Jan 9989", "1 Jan 9990") + def test_fit_decade_should_display_the_decade_that_is_in_the_center(self): self.when_navigating(fit_decade_fn, "1 Jan 2010", "2 Jan 2010") self.then_period_becomes("1 Jan 2010", "1 Jan 2020") + def test_fit_first_decade_should_display_the_decade_that_is_in_the_center(self): + self.when_navigating(fit_decade_fn, "1 Jan 10", "2 Jan 10") + self.then_period_becomes("1 Jan 10", "1 Jan 20") + + def test_fit_last_decade_should_display_the_decade_that_is_in_the_center(self): + self.when_navigating(fit_decade_fn, "1 Jan 9989", "1 Jan 9990") + self.then_period_becomes("1 Jan 9980", "1 Jan 9990") + def test_fit_century_should_display_the_century_that_is_in_the_center(self): self.when_navigating(fit_century_fn, "1 Jan 2010", "2 Jan 2010") self.then_period_becomes("1 Jan 2000", "1 Jan 2100") + def test_fit_first_century_should_display_the_century_that_is_in_the_center(self): + self.when_navigating(fit_century_fn, "1 Jan 10", "1 Jan 11") + self.then_period_becomes("1 Jan 10", "1 Jan 110") + + def test_fit_last_century_should_display_the_century_that_is_in_the_center(self): + self.when_navigating(fit_century_fn, "1 Jan 9989", "1 Jan 9990") + self.then_period_becomes("1 Jan 9890", "1 Jan 9990") + def test_fit_millennium_should_display_the_millennium_that_is_in_the_center(self): self.when_navigating(fit_millennium_fn, "1 Jan 2010", "2 Jan 2010") self.then_period_becomes("1 Jan 2000", "1 Jan 3000") + def test_fit_first_millennium_should_display_the_millennium_that_is_in_the_center(self): + self.when_navigating(fit_millennium_fn, "1 Jan 10", "2 Jan 10") + self.then_period_becomes("1 Jan 10", "1 Jan 1010") + + def test_fit_last_millennium_should_display_the_millennium_that_is_in_the_center(self): + self.when_navigating(fit_millennium_fn, "1 Jan 9989", "1 Jan 9990") + self.then_period_becomes("1 Jan 8990", "1 Jan 9990") + def test_move_page_smart_not_smart_forward(self): self.when_navigating(forward_fn, "1 Jan 2010", "5 Jan 2010") self.then_period_becomes("5 Jan 2010", "9 Jan 2010") diff --git a/specs/PyTimeType.py b/specs/PyTimeType.py index 3090371..315a81d 100644 --- a/specs/PyTimeType.py +++ b/specs/PyTimeType.py @@ -49,7 +49,7 @@ class PyTimeTypeSpec(unittest.TestCase): self.time_type.parse_time, "2010-31-hello 0:0:0") def test_formats_period_to_string(self): - time_period = TimePeriod(self.time_type, + time_period = TimePeriod(self.time_type, datetime.datetime(2010, 8, 01, 13, 44), datetime.datetime(2010, 8, 02, 13, 30)) self.assertEquals( @@ -57,22 +57,22 @@ class PyTimeTypeSpec(unittest.TestCase): self.time_type.format_period(time_period)) def test_returns_min_time(self): - self.assertEquals(datetime.datetime(10, 1, 1), - self.time_type.get_min_time()[0]) + self.assertEquals(datetime.datetime(10, 1, 1), + self.time_type.get_min_time()[0]) def test_returns_max_time(self): - self.assertEquals(datetime.datetime(9990, 1, 1), + self.assertEquals(datetime.datetime(9990, 1, 1), self.time_type.get_max_time()[0]) def test_returns_half_delta(self): delta = datetime.timedelta(days=4) half_delta = self.time_type.half_delta(delta) - self.assertEquals(datetime.timedelta(days=2), half_delta) + self.assertEquals(datetime.timedelta(days=2), half_delta) def test_returns_margin_delta(self): delta = datetime.timedelta(days=48) margin_delta = self.time_type.margin_delta(delta) - self.assertEquals(datetime.timedelta(days=2), margin_delta) + self.assertEquals(datetime.timedelta(days=2), margin_delta) def test_event_date_string_method(self): self.assertEquals( @@ -100,10 +100,10 @@ class PyTimeTypeDeltaFormattingSpec(unittest.TestCase): self.time_type = PyTimeType() def test_format_delta_method(self): - time_period1 = TimePeriod(self.time_type, + time_period1 = TimePeriod(self.time_type, datetime.datetime(2010, 8, 01, 13, 44), datetime.datetime(2010, 8, 01, 13, 44)) - time_period2 = TimePeriod(self.time_type, + time_period2 = TimePeriod(self.time_type, datetime.datetime(2010, 8, 02, 13, 44), datetime.datetime(2010, 8, 02, 13, 44)) delta = time_period2.start_time - time_period1.start_time @@ -172,10 +172,10 @@ class PyTimeTypeDeltaFormattingSpec(unittest.TestCase): self.assertEquals(u"790 %s" % _("days"), self.time_type.format_delta(delta)) def test_format_overlapping_events(self): - time_period1 = TimePeriod(self.time_type, + time_period1 = TimePeriod(self.time_type, datetime.datetime(2010, 8, 01, 13, 44), datetime.datetime(2010, 8, 03, 13, 44)) - time_period2 = TimePeriod(self.time_type, + time_period2 = TimePeriod(self.time_type, datetime.datetime(2010, 8, 01, 13, 44), datetime.datetime(2010, 8, 03, 13, 44)) delta = time_period2.start_time - time_period1.end_time diff --git a/specs/Scene.py b/specs/Scene.py index fb84b48..9aa16ed 100644 --- a/specs/Scene.py +++ b/specs/Scene.py @@ -56,7 +56,7 @@ class SceneSpec(unittest.TestCase): self.given_visible_event_at("5 Jan 2010") self.given_visible_event_at("5 Jan 2010") self.when_scene_is_created() - self.assertTrue(self.scene.event_data[0][1].Y > + self.assertTrue(self.scene.event_data[0][1].Y > self.scene.event_data[1][1].Y) def test_point_events_on_different_dates_has_same_y_positions(self): @@ -64,7 +64,7 @@ class SceneSpec(unittest.TestCase): self.given_visible_event_at("2 Jan 2010") self.given_visible_event_at("9 Jan 2010") self.when_scene_is_created() - self.assertEqual(self.scene.event_data[0][1].Y, + self.assertEqual(self.scene.event_data[0][1].Y, self.scene.event_data[1][1].Y) def test_period_events_with_same_period_has_different_y_positions(self): @@ -72,7 +72,7 @@ class SceneSpec(unittest.TestCase): self.given_visible_event_at("2 Jan 2010", "10 Jan 2010") self.given_visible_event_at("2 Jan 2010", "10 Jan 2010") self.when_scene_is_created() - self.assertTrue(self.scene.event_data[0][1].Y < + self.assertTrue(self.scene.event_data[0][1].Y < self.scene.event_data[1][1].Y) def test_period_events_with_different_periods_has_same_y_positions(self): @@ -80,9 +80,17 @@ class SceneSpec(unittest.TestCase): self.given_visible_event_at("2 Jan 2010", "3 Jan 2010") self.given_visible_event_at("8 Jan 2010", "10 Jan 2010") self.when_scene_is_created() - self.assertEqual(self.scene.event_data[0][1].Y, + self.assertEqual(self.scene.event_data[0][1].Y, self.scene.event_data[1][1].Y) + def test_scene_must_be_created_at_last_century(self): + self.given_displayed_period("1 Jan 9890", "1 Jan 9990") + try: + self.when_scene_is_created() + self.assertTrue(self.scene != None) + except: + self.assertTrue(False) + def setUp(self): self.db = MemoryDB() self.view_properties = ViewProperties() @@ -112,7 +120,7 @@ class SceneSpec(unittest.TestCase): category = Category("category", (0, 0, 0), None, visible) if end_time is None: end_time = start_time - event = Event(self.db.get_time_type(), human_time_to_py(start_time), + event = Event(self.db.get_time_type(), human_time_to_py(start_time), human_time_to_py(end_time), "event-text", category) self.db.save_category(category) self.db.save_event(event) diff --git a/specs/SubeventObject.py b/specs/SubeventObject.py index 9b87de3..9ba999f 100644 --- a/specs/SubeventObject.py +++ b/specs/SubeventObject.py @@ -18,9 +18,9 @@ import unittest -from timelinelib.db.subevent import Subevent -from timelinelib.db.container import Container from timelinelib.db.backends.memory import MemoryDB +from timelinelib.db.objects import Container +from timelinelib.db.objects import Subevent class SubeventSpec(unittest.TestCase): @@ -33,7 +33,7 @@ class SubeventSpec(unittest.TestCase): self.assertEqual(self.container, self.subevent.container) def given_default_subevent(self): - self.subevent = Subevent(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), + self.subevent = Subevent(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), self.time("2000-01-03 10:01:01"), "evt") def given_container_with_cid(self): @@ -42,7 +42,7 @@ class SubeventSpec(unittest.TestCase): def time(self, tm): return self.db.get_time_type().parse_time(tm) - + def setUp(self): self.db = MemoryDB() self.now = self.db.get_time_type().now() @@ -64,11 +64,11 @@ class ContainerSubeventSpec(unittest.TestCase): self.assertEqual(99, self.subevent.cid()) def given_default_subevent(self): - self.subevent = Subevent(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), + self.subevent = Subevent(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), self.time("2000-01-03 10:01:01"), "evt") def given_subevent_with_cid(self): - self.subevent = Subevent(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), + self.subevent = Subevent(self.db.get_time_type(), self.time("2000-01-01 10:01:01"), self.time("2000-01-03 10:01:01"), "evt", cid=99) def time(self, tm): diff --git a/specs/TextDisplayEditor.py b/specs/TextDisplayEditor.py new file mode 100644 index 0000000..b3fec55 --- /dev/null +++ b/specs/TextDisplayEditor.py @@ -0,0 +1,57 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +import unittest +from mock import Mock + +from timelinelib.wxgui.dialogs.textdisplay import TextDisplayDialog +from timelinelib.editors.textdisplay import TextDisplayEditor + +class TextDisplayEditorSpec(unittest.TestCase): + + def test_set_text_sets_dialog_text(self): + text = "aha" + self.editor.set_text(text) + self.view.set_text.assert_called_with(text) + + def test_initialization_sets_dialog_text(self): + self.editor.initialize() + self.view.set_text.assert_called_with(self.text) + + def test_get_text_returns_dialog_text(self): + self.assertTrue(WhenDialogTextIs("foo2", self.view, self.editor).controller_returns("foo2")) + self.assertTrue(WhenDialogTextIs("foo3", self.view, self.editor).controller_returns("foo3")) + + def setUp(self): + self.text = "buu" + self.view = Mock(TextDisplayDialog) + self.editor = TextDisplayEditor(self.view, self.text) + + +class WhenDialogTextIs(object): + + def __init__(self, text, view, editor): + self.text = text + view.get_text.return_value = text + self.editor = editor + self.editor.initialize() + + def controller_returns(self, text): + text = self.editor.get_text() + return self.text == text + diff --git a/specs/TimePeriod.py b/specs/TimePeriod.py index cc2fded..b2fae17 100644 --- a/specs/TimePeriod.py +++ b/specs/TimePeriod.py @@ -34,7 +34,7 @@ class ATime(object): def __eq__(self, other): return isinstance(other, ATime) and self.num == other.num - def __ne__(self, ohter): + def __ne__(self, other): return not (self == other) def __add__(self, other): @@ -72,7 +72,7 @@ class ADelta(object): return isinstance(other, ADelta) and self.num == other.num # Exists only only to simplify testing - def __ne__(self, ohter): + def __ne__(self, other): return not (self == other) # Exists only only to simplify testing diff --git a/specs/TimelineApplication.py b/specs/TimelineApplication.py index d24093a..697f925 100644 --- a/specs/TimelineApplication.py +++ b/specs/TimelineApplication.py @@ -22,7 +22,7 @@ from mock import Mock from timelinelib.application import TimelineApplication from timelinelib.config.dotfile import Config -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError from timelinelib.wxgui.dialogs.mainframe import MainFrame @@ -65,7 +65,7 @@ class MainFrameSpec(unittest.TestCase): self.main_frame = Mock(MainFrame) self.db_open = Mock() self.config = Mock(Config) - self.config.get_use_wide_date_range.return_value = self.USE_WIDE_DATE_RANGE + self.config.get_use_wide_date_range.return_value = self.USE_WIDE_DATE_RANGE self.controller = TimelineApplication( self.main_frame, self.db_open, self.config) diff --git a/specs/TimelineView.py b/specs/TimelineView.py index 49cf948..bf2de11 100644 --- a/specs/TimelineView.py +++ b/specs/TimelineView.py @@ -138,7 +138,8 @@ class TimelineViewSpec(unittest.TestCase): def test_zooms_timeline_by_10_percent_on_each_side_when_scrolling_while_holding_down_ctrl(self): self.init_view_with_db_with_period("1 Aug 2010", "21 Aug 2010") - self.controller.mouse_wheel_moved(1, ctrl_down=True, shift_down=False) + self.controller.mouse_wheel_moved( + 1, ctrl_down=True, shift_down=False, x=self.middle_x) self.assert_displays_period("3 Aug 2010", "19 Aug 2010") def test_displays_balloon_for_event_with_description(self): @@ -207,7 +208,7 @@ class TimelineViewSpec(unittest.TestCase): event = self.given_event_with(pos=(40, 60), size=(20, 10)) self.init_view_with_db() self.simulate_mouse_double_click(50, 65) - self.view.edit_event.assert_called_with(event) + self.view.open_event_editor_for.assert_called_with(event) self.assert_timeline_redrawn() def test_selects_and_deselects_event_when_clicking_on_it(self): @@ -318,10 +319,12 @@ class TimelineViewSpec(unittest.TestCase): def test_scrolls_with_10_percent_when_using_mouse_wheel(self): self.init_view_with_db_with_period("1 Aug 2010", "21 Aug 2010") - self.controller.mouse_wheel_moved(-1, ctrl_down=False, shift_down=False) + self.controller.mouse_wheel_moved( + -1, ctrl_down=False, shift_down=False, x=self.middle_x) self.assert_displays_period("3 Aug 2010", "23 Aug 2010") self.assert_timeline_redrawn() - self.controller.mouse_wheel_moved(1, ctrl_down=False, shift_down=False) + self.controller.mouse_wheel_moved( + 1, ctrl_down=False, shift_down=False, x=self.middle_x) self.assert_displays_period("1 Aug 2010", "21 Aug 2010") self.assert_timeline_redrawn() @@ -346,7 +349,8 @@ class TimelineViewSpec(unittest.TestCase): def test_shift_scroll_changes_divider_line_value_and_redraws(self): self.init_view_with_db() - self.controller.mouse_wheel_moved(1, ctrl_down=False, shift_down=True) + self.controller.mouse_wheel_moved( + 1, ctrl_down=False, shift_down=True, x=self.middle_x) self.assertTrue(self.divider_line_slider.SetValue.called) self.assert_timeline_redrawn() @@ -357,7 +361,9 @@ class TimelineViewSpec(unittest.TestCase): def setUp(self): self.db = MemoryDB() self.view = Mock(DrawingAreaPanel) - self.view.GetSizeTuple.return_value = (10, 10) + self.width = 10 + self.middle_x = self.width / 2 + self.view.GetSizeTuple.return_value = (self.width, 10) self.status_bar_adapter = Mock(StatusBarAdapter) self.config = Mock(Config) self.mock_drawer = MockDrawer() @@ -462,7 +468,7 @@ class TimelineViewSpec(unittest.TestCase): self.assertTrue(self.view.redraw_surface.called) def assert_created_event_with_period(self, start, end): - self.view.create_new_event.assert_called_with( + self.view.open_create_event_editor.assert_called_with( human_time_to_py(start), human_time_to_py(end)) def assert_is_selected(self, event): diff --git a/specs/WxDateTimePicker.py b/specs/WxDateTimePicker.py index 3080f9c..0b66b85 100644 --- a/specs/WxDateTimePicker.py +++ b/specs/WxDateTimePicker.py @@ -93,7 +93,7 @@ class WxDatePickerBaseFixture(unittest.TestCase): self.controller.on_text_changed() def simulate_change_insertion_point(self, new_insertion_point): - self.py_date_picker.GetSelection.return_value = (new_insertion_point, new_insertion_point) + self.py_date_picker.GetSelection.return_value = (new_insertion_point, new_insertion_point) self.py_date_picker.GetInsertionPoint.return_value = new_insertion_point def _update_insertion_point_and_selection(self, from_pos, to_pos): @@ -363,12 +363,12 @@ class WxTimePickerBaseFixture(unittest.TestCase): self.controller.on_text_changed() def simulate_change_insertion_point(self, new_insertion_point): - self.py_time_picker.GetSelection.return_value = (new_insertion_point, new_insertion_point) + self.py_time_picker.GetSelection.return_value = (new_insertion_point, new_insertion_point) self.py_time_picker.GetInsertionPoint.return_value = new_insertion_point def _update_insertion_point_and_selection(self, from_pos, to_pos): self.py_time_picker.GetInsertionPoint.return_value = from_pos - self.py_time_picker.GetSelection.return_value = (from_pos, to_pos) + self.py_time_picker.GetSelection.return_value = (from_pos, to_pos) class AWxTimePicker(WxTimePickerBaseFixture): diff --git a/specs/WxTimeType.py b/specs/WxTimeType.py index 8abf309..50cad31 100644 --- a/specs/WxTimeType.py +++ b/specs/WxTimeType.py @@ -75,7 +75,7 @@ class WxTimeTypeSpec(unittest.TestCase): self.time_type.parse_time, "2010-31-hello 0:0:0") def test_format_period_method(self): - time_period = TimePeriod(self.time_type, + time_period = TimePeriod(self.time_type, wx.DateTimeFromDMY(1, 7, 2010, 13, 44), wx.DateTimeFromDMY(2, 7, 2010, 13, 30)) self.assertEquals( @@ -95,13 +95,13 @@ class WxTimeTypeSpec(unittest.TestCase): def test_returns_margin_delta(self): delta = wx.TimeSpan.Days(days=48) margin_delta = self.time_type.margin_delta(delta) - self.assertEquals(wx.TimeSpan.Days(2), margin_delta) + self.assertEquals(wx.TimeSpan.Days(2), margin_delta) def test_returns_half_delta(self): delta = wx.TimeSpan.Days(100 * 365) half_delta = self.time_type.half_delta(delta) - self.assertEquals(wx.TimeSpan.Days(50 * 365).GetMilliseconds(), - half_delta.GetMilliseconds()) + self.assertEquals(wx.TimeSpan.Days(50 * 365).GetMilliseconds(), + half_delta.GetMilliseconds()) class WxDateTimeConstructorSpec(unittest.TestCase): @@ -192,10 +192,10 @@ class WxTimeTypeDeltaFormattingSpec(unittest.TestCase): self.assertEquals(u"790 %s" % _("days"), self.time_type.format_delta(delta)) def test_format_overlapping_events(self): - time_period1 = TimePeriod(self.time_type, + time_period1 = TimePeriod(self.time_type, wx.DateTimeFromDMY(1, 7, 2010, 13, 44), wx.DateTimeFromDMY(2, 7, 2010, 13, 30)) - time_period2 = TimePeriod(self.time_type, + time_period2 = TimePeriod(self.time_type, wx.DateTimeFromDMY(1, 7, 2010, 13, 44), wx.DateTimeFromDMY(2, 7, 2010, 13, 30)) delta = time_period2.start_time - time_period1.end_time diff --git a/specs/XmlParser.py b/specs/XmlParser.py index ddb9582..7de4d55 100644 --- a/specs/XmlParser.py +++ b/specs/XmlParser.py @@ -16,11 +16,11 @@ # along with Timeline. If not, see . -import unittest from StringIO import StringIO +import unittest import xml.sax -import timelinelib.db.backends.xmlparser as xmlparser +import timelinelib.xml.parser as xmlparser class TestXmlParser(unittest.TestCase): diff --git a/specs/XmlTimeline.py b/specs/XmlTimeline.py index 9e14d47..ce4ea2b 100644 --- a/specs/XmlTimeline.py +++ b/specs/XmlTimeline.py @@ -17,14 +17,12 @@ # along with Timeline. If not, see . -import codecs -import tempfile -import os.path -import shutil from datetime import datetime -import unittest +import codecs + import wx +from specs.utils import TmpDirTestCase from timelinelib.db.backends.xmlfile import XmlTimeline from timelinelib.db import db_open from timelinelib.db.objects import Category @@ -35,32 +33,32 @@ from timelinelib.meta.version import get_version from timelinelib.time import WxTimeType -class XmlTimelineSpec(unittest.TestCase): +class XmlTimelineSpec(TmpDirTestCase): IO = True def testUseWxTimeTypeWhenUseWideDateRangeIsTrue(self): timeline = XmlTimeline(None, load=False, use_wide_date_range=True) - self.assertTrue(isinstance(timeline.time_type, WxTimeType)) + self.assertTrue(isinstance(timeline.get_time_type(), WxTimeType)) def testAlertStringParsingGivesAlertData(self): timeline = XmlTimeline(None, load=False, use_wide_date_range=True) time, text = timeline._parse_alert_string("2012-11-11 00:00:00;Now is the time") self.assertEqual("Now is the time", text) - self.assertEqual("2012-11-11 00:00:00", "%s" % timeline.time_type.time_string(time)) + self.assertEqual("2012-11-11 00:00:00", "%s" % timeline.get_time_type().time_string(time)) def testAlertDataConversionGivesAlertString(self): timeline = XmlTimeline(None, load=False, use_wide_date_range=False) alert = (datetime(2010, 8, 31, 0, 0, 0), "Hoho") alert_text = timeline.alert_string(alert) self.assertEqual("2010-8-31 0:0:0;Hoho", alert_text) - + def testWxTimeAlertDataConversionGivesAlertString(self): timeline = XmlTimeline(None, load=False, use_wide_date_range=True) alert = (wx.DateTimeFromDMY(30, 8, 2010, 0, 0, 0), "Hoho") alert_text = timeline.alert_string(alert) self.assertEqual("2010-09-30 00:00:00;Hoho", alert_text) - + def testDisplayedPeriodTagNotWrittenIfNotSet(self): # Create a new db and add one event db = db_open(self.tmp_path) @@ -162,8 +160,5 @@ class XmlTimelineSpec(unittest.TestCase): self.fail("Unknown category.") def setUp(self): - self.tmp_dir = tempfile.mkdtemp(prefix="timeline-test") - self.tmp_path = os.path.join(self.tmp_dir, "test.timeline") - - def tearDown(self): - shutil.rmtree(self.tmp_dir) + TmpDirTestCase.setUp(self) + self.tmp_path = self.get_tmp_path("test.timeline") diff --git a/specs/utils.py b/specs/utils.py index 5ddfdb0..cc5d8c5 100644 --- a/specs/utils.py +++ b/specs/utils.py @@ -24,17 +24,16 @@ import tempfile import traceback import unittest -from mock import Mock import wx import wx.lib.inspection +from timelinelib.calendar.monthnames import ABBREVIATED_ENGLISH_MONTH_NAMES from timelinelib.config.arguments import ApplicationArguments from timelinelib.config.dotfile import read_config from timelinelib.db import db_open from timelinelib.db.objects import Category from timelinelib.db.objects import Event from timelinelib.db.objects import TimePeriod -from timelinelib.calendar.monthnames import ABBREVIATED_ENGLISH_MONTH_NAMES from timelinelib.time.pytime import PyTimeType from timelinelib.time.wxtime import WxTimeType from timelinelib.wxgui.setup import start_wx_application @@ -101,18 +100,30 @@ def an_event_with(start=None, end=None, time=ANY_TIME, text="foo", fuzzy=False, fuzzy=fuzzy, locked=locked, ends_today=ends_today) -class WxEndToEndTestCase(unittest.TestCase): +class TmpDirTestCase(unittest.TestCase): def setUp(self): self.tmp_dir = tempfile.mkdtemp(prefix="timeline-test") - self.timeline_path = os.path.join(self.tmp_dir, "test.timeline") - self.config_file_path = os.path.join(self.tmp_dir, "thetimelineproj.cfg") + + def tearDown(self): + shutil.rmtree(self.tmp_dir) + + def get_tmp_path(self, name): + return os.path.join(self.tmp_dir, name) + + +class WxEndToEndTestCase(TmpDirTestCase): + + def setUp(self): + TmpDirTestCase.setUp(self) + self.timeline_path = self.get_tmp_path("test.timeline") + self.config_file_path = self.get_tmp_path("thetimelineproj.cfg") self.config = read_config(self.config_file_path) self.standard_excepthook = sys.excepthook self.error_in_gui_thread = None def tearDown(self): - shutil.rmtree(self.tmp_dir) + TmpDirTestCase.tearDown(self) sys.excepthook = self.standard_excepthook def start_timeline_and(self, steps_to_perform_in_gui): diff --git a/timelinelib/application.py b/timelinelib/application.py index afa6c91..024698e 100644 --- a/timelinelib/application.py +++ b/timelinelib/application.py @@ -16,7 +16,7 @@ # along with Timeline. If not, see . -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError class TimelineApplication(object): @@ -46,7 +46,7 @@ class TimelineApplication(object): self.config.append_recently_opened(path) self.main_frame._update_open_recent_submenu() self.main_frame._display_timeline(self.timeline) - + def set_no_timeline(self): self.timeline = None self.main_frame._display_timeline(None) diff --git a/timelinelib/db/__init__.py b/timelinelib/db/__init__.py index 79b8abe..e4dddef 100644 --- a/timelinelib/db/__init__.py +++ b/timelinelib/db/__init__.py @@ -16,20 +16,14 @@ # along with Timeline. If not, see . -""" -Functionality for reading and writing timeline data from and to persistent -storage. -""" - - import os.path -from timelinelib.db.tutorial import create_in_memory_tutorial_db +from timelinelib.db.backends.memory import MemoryDB +from timelinelib.db.backends.tutorial import create_in_memory_tutorial_db +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Category from timelinelib.db.objects import Event from timelinelib.db.objects import TimePeriod -from timelinelib.db.backends.memory import MemoryDB -from timelinelib.db.interface import TimelineIOError from timelinelib.drawing.viewproperties import ViewProperties @@ -59,7 +53,7 @@ def db_open(path, use_wide_date_range=False): from timelinelib.db.backends.file import FileTimeline from timelinelib.db.backends.xmlfile import XmlTimeline file_db = FileTimeline(path) - xml_db = XmlTimeline(path, load=False, + xml_db = XmlTimeline(path, load=False, use_wide_date_range=use_wide_date_range) copy_db(file_db, xml_db) return xml_db diff --git a/timelinelib/db/backends/__init__.py b/timelinelib/db/backends/__init__.py index 3093a3d..e69de29 100644 --- a/timelinelib/db/backends/__init__.py +++ b/timelinelib/db/backends/__init__.py @@ -1,21 +0,0 @@ -# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg -# -# This file is part of Timeline. -# -# Timeline 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 3 of the License, or -# (at your option) any later version. -# -# Timeline 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 Timeline. If not, see . - - -""" -Implementations of different timeline databases. -""" diff --git a/timelinelib/db/backends/dir.py b/timelinelib/db/backends/dir.py index ec679a6..708fca3 100644 --- a/timelinelib/db/backends/dir.py +++ b/timelinelib/db/backends/dir.py @@ -31,10 +31,10 @@ import time import wx -from timelinelib.db.interface import TimelineIOError from timelinelib.db.backends.memory import MemoryDB -from timelinelib.db.objects import Event +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Category +from timelinelib.db.objects import Event class DirTimeline(MemoryDB): @@ -58,10 +58,10 @@ class DirTimeline(MemoryDB): For each sub-directory a category is created and all events (files) belong the category (directory) in which they are. """ - if not os.path.exists(dir_path): + if not os.path.exists(dir_path): # Nothing to load return - if not os.path.isdir(dir_path): + if not os.path.isdir(dir_path): # Nothing to load return try: @@ -110,8 +110,8 @@ class DirTimeline(MemoryDB): def _event_from_path(self, file_path): stat = os.stat(file_path) - # st_atime (time of most recent access), - # st_mtime (time of most recent content modification), + # st_atime (time of most recent access), + # st_mtime (time of most recent content modification), # st_ctime (platform dependent; time of most recent metadata change on # Unix, or the time of creation on Windows): start_time = datetime.fromtimestamp(int(stat.st_mtime)) diff --git a/timelinelib/db/backends/file.py b/timelinelib/db/backends/file.py index e59eb35..13d4577 100644 --- a/timelinelib/db/backends/file.py +++ b/timelinelib/db/backends/file.py @@ -16,26 +16,22 @@ # along with Timeline. If not, see . -""" -Implementation of timeline database with flat file storage using our own custom -format. +# This database was only used in version 0.1.0 - 0.9.0. +# We plan to remove this in version 1.0.0. -This database was only used for version 0.1.0 - 0.9.0. -""" - -import re -import codecs -import os.path -from os.path import abspath from datetime import datetime +from os.path import abspath import base64 +import codecs +import os.path +import re import StringIO import wx from timelinelib.db.backends.memory import MemoryDB -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Category from timelinelib.db.objects import Event from timelinelib.db.objects import TimePeriod @@ -54,13 +50,6 @@ class ParseException(Exception): class FileTimeline(MemoryDB): """ - Implements the timeline database interface. - - The comments in the TimelineDB class describe what the public methods do. - - Every public method (including the constructor) can raise a TimelineIOError - if there was a problem reading or writing from file. - The general format of the file looks like this for version >= 0.3.0: # Written by Timeline 0.3.0 on 2009-7-23 9:40:33 @@ -102,7 +91,7 @@ class FileTimeline(MemoryDB): If a read error occurs a TimelineIOError will be raised. """ - if not os.path.exists(self.path): + if not os.path.exists(self.path): # Nothing to load. Will create a new timeline on save. return try: @@ -179,7 +168,7 @@ class FileTimeline(MemoryDB): try: if len(times) != 2: raise ParseException("Unexpected number of components.") - tp = TimePeriod(self.get_time_type(), self._parse_time(times[0]), + tp = TimePeriod(self.get_time_type(), self._parse_time(times[0]), self._parse_time(times[1])) self._set_displayed_period(tp) if not tp.is_period(): diff --git a/timelinelib/db/backends/ics.py b/timelinelib/db/backends/ics.py index e10c77f..5d4610c 100644 --- a/timelinelib/db/backends/ics.py +++ b/timelinelib/db/backends/ics.py @@ -16,42 +16,27 @@ # along with Timeline. If not, see . -""" -Implementation of timeline database that reads and writes (todo) ICS files. -""" - - -import re -import codecs -import shutil -import os.path -from os.path import abspath from datetime import date from datetime import datetime -from datetime import timedelta +from os.path import abspath +import os.path from icalendar import Calendar -from timelinelib.db.interface import STATE_CHANGE_ANY -from timelinelib.db.interface import STATE_CHANGE_CATEGORY -from timelinelib.db.interface import TimelineDB -from timelinelib.db.interface import TimelineIOError -from timelinelib.db.objects import Category +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Event -from timelinelib.db.objects import TimePeriod -from timelinelib.db.objects import time_period_center -from timelinelib.db.utils import generic_event_search +from timelinelib.db.observer import Observable +from timelinelib.db.search import generic_event_search from timelinelib.db.utils import IdCounter -from timelinelib.db.utils import safe_write -from timelinelib.meta.version import get_version from timelinelib.time import PyTimeType from timelinelib.utils import ex_msg -class IcsTimeline(TimelineDB): +class IcsTimeline(Observable): def __init__(self, path): - TimelineDB.__init__(self, path) + Observable.__init__(self) + self.path = path self.event_id_counter = IdCounter() self._load_data() @@ -134,9 +119,6 @@ class IcsTimeline(TimelineDB): def _load_data(self): self.cal = Calendar() - if not os.path.exists(self.path): - # Nothing to load. Will create a new timeline on save. - return try: file = open(self.path, "rb") try: @@ -156,26 +138,19 @@ class IcsTimeline(TimelineDB): whole_msg = (msg + "\n\n%s") % (abspath(self.path), e) raise TimelineIOError(whole_msg) - def _save_data(self): - #def save(file): - # file.write(self.cal.as_string()) - #safe_write(self.path, None, save) - pass - def extract_start_end(vevent): - """Return (start_time, end_time).""" - start = ensure_datetime(vevent.decoded("dtstart")) + start = convert_to_datetime(vevent.decoded("dtstart")) if vevent.has_key("dtend"): - end = ensure_datetime(vevent.decoded("dtend")) + end = convert_to_datetime(vevent.decoded("dtend")) elif vevent.has_key("duration"): end = start + vevent.decoded("duration") else: - end = ensure_datetime(vevent.decoded("dtstart")) + end = convert_to_datetime(vevent.decoded("dtstart")) return (start, end) -def ensure_datetime(d): +def convert_to_datetime(d): if isinstance(d, datetime): return datetime(d.year, d.month, d.day, d.hour, d.minute, d.second) elif isinstance(d, date): diff --git a/timelinelib/db/backends/memory.py b/timelinelib/db/backends/memory.py index b413f50..0c54c0d 100644 --- a/timelinelib/db/backends/memory.py +++ b/timelinelib/db/backends/memory.py @@ -29,21 +29,22 @@ query persistent storage to retrieve data. """ -from timelinelib.db.interface import TimelineIOError -from timelinelib.db.interface import TimelineDB -from timelinelib.db.interface import STATE_CHANGE_ANY -from timelinelib.db.interface import STATE_CHANGE_CATEGORY -from timelinelib.db.objects import Event -from timelinelib.db.container import Container +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Category +from timelinelib.db.objects import Container +from timelinelib.db.objects import Event +from timelinelib.db.observer import Observable +from timelinelib.db.observer import STATE_CHANGE_ANY +from timelinelib.db.observer import STATE_CHANGE_CATEGORY +from timelinelib.db.search import generic_event_search from timelinelib.db.utils import IdCounter -from timelinelib.db.utils import generic_event_search -class MemoryDB(TimelineDB): +class MemoryDB(Observable): def __init__(self): - TimelineDB.__init__(self, "") + Observable.__init__(self) + self.path = "" self.categories = [] self.category_id_counter = IdCounter() self.events = [] @@ -117,10 +118,10 @@ class MemoryDB(TimelineDB): id = subevent.cid() if id == 0: id = self._get_max_container_id(container_events) + 1 - subevent.set_cid(id) + subevent.set_cid(id) name = "[%d]Container" % id - container = Container(subevent.time_type, - subevent.time_period.start_time, + container = Container(subevent.time_type, + subevent.time_period.start_time, subevent.time_period.end_time, name) self.save_event(container) self._register_subevent(subevent) @@ -132,7 +133,7 @@ class MemoryDB(TimelineDB): if id < event.cid(): id = event.cid() return id - + def _unregister_subevent(self, subevent): container_events = [event for event in self.events if event.is_container()] @@ -146,7 +147,7 @@ class MemoryDB(TimelineDB): self.events.remove(container) except: pass - + def delete_event(self, event_or_id): if isinstance(event_or_id, Event): event = event_or_id @@ -169,7 +170,7 @@ class MemoryDB(TimelineDB): return list(self.categories) def get_containers(self): - containers = [event for event in self.events + containers = [event for event in self.events if event.is_container()] return containers diff --git a/timelinelib/db/backends/tutorial.py b/timelinelib/db/backends/tutorial.py new file mode 100644 index 0000000..00bd49b --- /dev/null +++ b/timelinelib/db/backends/tutorial.py @@ -0,0 +1,128 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +from datetime import datetime +from datetime import timedelta + +from timelinelib.db.objects import Category +from timelinelib.db.objects import Event +from timelinelib.db.objects import TimePeriod +from timelinelib.db.backends.memory import MemoryDB + + +def create_in_memory_tutorial_db(): + tutcreator = TutorialTimelineCreator() + tutcreator.add_category(_("Welcome"), (255, 80, 80), (0, 0, 0)) + tutcreator.add_event( + _("Welcome to Timeline"), + "", + timedelta(days=4)) + tutcreator.add_category(_("Intro"), (250, 250, 20), (0, 0, 0)) + tutcreator.add_event( + _("Hover me!"), + _("Hovering events with a triangle shows the event description."), + timedelta(days=5)) + tutcreator.add_category(_("Features"), (100, 100, 250), (250, 250, 20)) + tutcreator.add_event( + _("Scroll"), + _("Left click somewhere on the timeline and start dragging." + "\n\n" + "You can also use the mouse wheel." + "\n\n" + "You can also middle click with the mouse to center around that point."), + timedelta(days=5), + timedelta(days=10)) + tutcreator.add_event( + _("Zoom"), + _("Hold down Ctrl while scrolling the mouse wheel." + "\n\n" + "Hold down Shift while dragging with the mouse."), + timedelta(days=6), + timedelta(days=11)) + tutcreator.add_event( + _("Create event"), + _("Double click somewhere on the timeline." + "\n\n" + "Hold down Ctrl while dragging the mouse to select a period."), + timedelta(days=12), + timedelta(days=18)) + tutcreator.add_event( + _("Edit event"), + _("Double click on an event."), + timedelta(days=12), + timedelta(days=18)) + tutcreator.add_event( + _("Select event"), + _("Click on it." + "\n\n" + "Hold down Ctrl while clicking events to select multiple."), + timedelta(days=20), + timedelta(days=25)) + tutcreator.add_event( + _("Delete event"), + _("Select events to be deleted and press the Del key."), + timedelta(days=19), + timedelta(days=24)) + tutcreator.add_event( + _("Resize and move me!"), + _("First select me and then drag the handles."), + timedelta(days=11), + timedelta(days=19)) + tutcreator.add_category(_("Saving"), (50, 200, 50), (0, 0, 0)) + tutcreator.add_event( + _("Saving"), + _("This timeline is stored in memory and modifications to it will not " + "be persisted between sessions." + "\n\n" + "Choose File/New/File Timeline to create a timeline that is saved on " + "disk."), + timedelta(days=23)) + return tutcreator.get_db() + + +class TutorialTimelineCreator(object): + + def __init__(self): + self.db = MemoryDB() + now = datetime.now() + self.start = datetime(now.year, now.month, 1, 0, 0, 0) + self.end = self.start + timedelta(days=30) + self.db._set_displayed_period(TimePeriod(self.db.get_time_type(), + self.start, self.end)) + self.last_cat = None + + def add_category(self, name, color, font_color, make_last_added_parent=False): + if make_last_added_parent: + parent = self.last_cat + else: + parent = None + self.last_cat = Category(name, color, font_color, True, parent) + self.db.save_category(self.last_cat) + + def add_event(self, text, description, start_add, end_add=None): + start = self.start + start_add + end = start + if end_add is not None: + end = self.start + end_add + evt = Event(self.db.get_time_type(), start, end, text, self.last_cat) + if description: + evt.set_data("description", description) + self.db.save_event(evt) + + def get_db(self): + return self.db diff --git a/timelinelib/db/backends/xmlfile.py b/timelinelib/db/backends/xmlfile.py index 604f242..f53dbe6 100644 --- a/timelinelib/db/backends/xmlfile.py +++ b/timelinelib/db/backends/xmlfile.py @@ -31,22 +31,22 @@ from xml.sax.saxutils import escape as xmlescape import wx from timelinelib.db.backends.memory import MemoryDB -from timelinelib.db.backends.xmlparser import ANY -from timelinelib.db.backends.xmlparser import OPTIONAL -from timelinelib.db.backends.xmlparser import parse -from timelinelib.db.backends.xmlparser import parse_fn_store -from timelinelib.db.backends.xmlparser import SINGLE -from timelinelib.db.backends.xmlparser import Tag -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Category +from timelinelib.db.objects import Container from timelinelib.db.objects import Event -from timelinelib.db.container import Container -from timelinelib.db.subevent import Subevent +from timelinelib.db.objects import Subevent from timelinelib.db.objects import TimePeriod from timelinelib.db.utils import safe_write from timelinelib.meta.version import get_version from timelinelib.time import WxTimeType from timelinelib.utils import ex_msg +from timelinelib.xml.parser import ANY +from timelinelib.xml.parser import OPTIONAL +from timelinelib.xml.parser import parse +from timelinelib.xml.parser import parse_fn_store +from timelinelib.xml.parser import SINGLE +from timelinelib.xml.parser import Tag ENCODING = "utf-8" @@ -111,7 +111,7 @@ class XmlTimeline(MemoryDB): except: #TODO: Create container pass - + def _load(self): """ Load timeline data from the file that this timeline points to. @@ -122,7 +122,7 @@ class XmlTimeline(MemoryDB): If a read error occurs a TimelineIOError will be raised. """ - if not os.path.exists(self.path): + if not os.path.exists(self.path): # Nothing to load. Will create a new timeline on save. return try: @@ -255,7 +255,7 @@ class XmlTimeline(MemoryDB): time, text = alert time_string = self._time_string(time) return "%s;%s" % (time_string, text) - + def _parse_alert_string(self, alert_string): if alert_string is not None: try: @@ -267,13 +267,13 @@ class XmlTimeline(MemoryDB): else: alert = None return alert - + def _is_container_event(self, text): return text.startswith("[") def _is_subevent(self, text): return text.startswith("(") - + def _extract_container_id(self, text): str_id, text = text.split("]", 1) try: @@ -282,7 +282,7 @@ class XmlTimeline(MemoryDB): except: id = -1 return id, text - + def _extract_subid(self, text): id, text = text.split(")", 1) try: @@ -290,7 +290,7 @@ class XmlTimeline(MemoryDB): except: id = -1 return id, text - + def _parse_optional_bool(self, tmp_dict, id): if tmp_dict.has_key(id): return tmp_dict.pop(id) == "True" @@ -326,7 +326,7 @@ class XmlTimeline(MemoryDB): events = [event for event in self.events if not event.is_subevent()] events.extend(subevents) self.events = events - + def _write_xml_doc(self, file): file.write("\n") self._write_timeline(file) @@ -379,7 +379,7 @@ class XmlTimeline(MemoryDB): if evt.get_data("description") is not None: write_simple_tag(file, "description", evt.get_data("description"), INDENT3) - alert = evt.get_data("alert") + alert = evt.get_data("alert") if alert is not None: write_simple_tag(file, "alert", self.alert_string(alert), INDENT3) diff --git a/timelinelib/db/exceptions.py b/timelinelib/db/exceptions.py new file mode 100644 index 0000000..8485b6d --- /dev/null +++ b/timelinelib/db/exceptions.py @@ -0,0 +1,27 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +class TimelineIOError(Exception): + """ + Raised from a backend if a read/write error occurs. + + The constructor and any of the public methods can raise this exception. + + Also raised by the get_timeline method if loading of a timeline failed. + """ + pass diff --git a/timelinelib/db/interface.py b/timelinelib/db/interface.py index b266ac2..f758b09 100644 --- a/timelinelib/db/interface.py +++ b/timelinelib/db/interface.py @@ -16,198 +16,19 @@ # along with Timeline. If not, see . -# A category was added, edited, or deleted -STATE_CHANGE_CATEGORY = 1 -# Something happened that changed the state of the timeline -STATE_CHANGE_ANY = 2 - - -class Observable(object): - - def __init__(self): - self.observers = [] - - def register(self, fn): - self.observers.append(fn) - - def unregister(self, fn): - if fn in self.observers: - self.observers.remove(fn) - - def _notify(self, state_change): - for fn in self.observers: - fn(state_change) - - -class TimelineDB(Observable): - """ - Read (and write) timeline data from persistent storage. - - All methods that modify timeline data should automatically write it to - persistent storage. - - A TimelineIOError should be raised if reading or writing fails. After such - a failure the database it not guarantied to return correct data. (Read and - write errors are however very rare.) - - A timeline database is observable so that GUI components can update - themselves when data changes. The two types of state changes are given as - constants above. - - Future considerations: If databases get large it might be inefficient to - save to persistent storage every time we modify the database. A solution is - to add an explicit save method and have all the other methods just modify - the database in memory. - """ - - def __init__(self, path): - Observable.__init__(self) - self.path = path - - def get_time_type(self): - raise NotImplementedError() - - def is_read_only(self): - """ - Return True if you can only read from this database and False if you - can both read and write. - """ - raise NotImplementedError() - - def supported_event_data(self): - """ - Return a list of event data that we can write. - - Event data is represented by a string id. See Event.set_data for - information what string id map to what data. - - Not required if is_read_only returns True. - """ - raise NotImplementedError() - - def search(self, search_string): - """ - Return a list of events matching the search string. - """ - raise NotImplementedError() - - def get_events(self, time_period): - """ - Return a list of events within the time period. - """ - raise NotImplementedError() - - def get_all_events(self, time_period): - """ - Return a list of all events in the database. - """ - raise NotImplementedError() - - def get_first_event(self): - """Return the event with the earliest start time.""" - raise NotImplementedError() - - def get_last_event(self): - """Return the event with the latest end time.""" - raise NotImplementedError() - - def save_event(self, event): - """ - Make sure that the given event is saved to persistent storage. - - If the event is new it is given a new unique id. Otherwise the - information in the database is just updated. - - Not required if is_read_only returns True. - """ - raise NotImplementedError() - - def delete_event(self, event_or_id): - """ - Delete the event (or the event with the given id) from the database. - - Not required if is_read_only returns True. - """ - raise NotImplementedError() - - def get_categories(self): - """ - Return a list of all available categories. - """ - raise NotImplementedError() - - def save_category(self, category): - """ - Make sure that the given category is saved to persistent storage. - - If the category is new it is given a new unique id. Otherwise the - information in the database is just updated. - - Not required if is_read_only returns True. - """ - raise NotImplementedError() - - def delete_category(self, category_or_id): - """ - Delete the category (or the category with the given id) from the - database. - - Not required if is_read_only returns True. - """ - raise NotImplementedError() - - def load_view_properties(self, view_properties): - """ - Load saved view properties from persistent storage into view_properties - object. - """ - raise NotImplementedError() - - def save_view_properties(self, view_properties): - """ - Save subset of view properties to persistent storage. - - Not required if is_read_only returns True. - """ - raise NotImplementedError() - - def find_event_with_id(self, id): - """ - Return the event associated with the given event id. - """ - raise NotImplementedError() - - def place_event_after_event(self, event_to_place, target_event): - raise NotImplementedError() - - def place_event_before_event(self, event_to_place, target_event): - raise NotImplementedError() - - -class TimelineIOError(Exception): - """ - Raised from a TimelineDB if a read/write error occurs. - - The constructor and any of the public methods can raise this exception. - - Also raised by the get_timeline method if loading of a timeline failed. - """ - pass - - class ContainerStrategy(object): - + def __init__(self, container): self.container = container - + def register_subevent(self, subevent): """Return the event with the latest end time.""" - raise NotImplementedError() + raise NotImplementedError() def unregister_subevent(self, subevent): """Return the event with the latest end time.""" - raise NotImplementedError() + raise NotImplementedError() def update(self, subevent): """Update container properties when adding a new sub-event.""" - raise NotImplementedError() + raise NotImplementedError() diff --git a/timelinelib/db/objects/__init__.py b/timelinelib/db/objects/__init__.py new file mode 100644 index 0000000..8fd4d4d --- /dev/null +++ b/timelinelib/db/objects/__init__.py @@ -0,0 +1,27 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +from timelinelib.db.objects.category import Category +from timelinelib.db.objects.container import Container +from timelinelib.db.objects.event import Event +from timelinelib.db.objects.subevent import Subevent +from timelinelib.db.objects.timeperiod import PeriodTooLongError +from timelinelib.db.objects.timeperiod import TimeOutOfRangeLeftError +from timelinelib.db.objects.timeperiod import TimeOutOfRangeRightError +from timelinelib.db.objects.timeperiod import TimePeriod +from timelinelib.db.objects.timeperiod import time_period_center diff --git a/timelinelib/db/objects/category.py b/timelinelib/db/objects/category.py new file mode 100644 index 0000000..779e3b0 --- /dev/null +++ b/timelinelib/db/objects/category.py @@ -0,0 +1,47 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +class Category(object): + + # NOTE: The visible flag of categories should not be used any longer. + # Visibility of categories are now managed in ViewProperties. However some + # timeline databases still use this flag to manage the saving. This flag + # should be removed when we can. + + def __init__(self, name, color, font_color, visible, parent=None): + self.id = None + self.name = name + self.color = color + if font_color is None: + self.font_color = (0, 0, 0) + else: + self.font_color = font_color + self.visible = visible + self.parent = parent + + def has_id(self): + return self.id is not None + + def set_id(self, id): + self.id = id + + +def sort_categories(categories): + sorted_categories = list(categories) + sorted_categories.sort(cmp, lambda x: x.name.lower()) + return sorted_categories diff --git a/timelinelib/db/objects/container.py b/timelinelib/db/objects/container.py new file mode 100644 index 0000000..903ce36 --- /dev/null +++ b/timelinelib/db/objects/container.py @@ -0,0 +1,56 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +from timelinelib.db.objects.event import Event +from timelinelib.db.strategies import DefaultContainerStrategy + + +class Container(Event): + + def __init__(self, time_type, start_time, end_time, text, category=None, + cid=-1): + Event.__init__(self, time_type, start_time, end_time, text, category, + False, False, False) + self.container_id = cid + self.events = [] + self.strategy = DefaultContainerStrategy(self) + + def is_container(self): + return True + + def is_subevent(self): + return False + + def cid(self): + return self.container_id + + def set_cid(self, cid): + self.container_id = cid + + def register_subevent(self, subevent): + self.strategy.register_subevent(subevent) + + def unregister_subevent(self, subevent): + self.strategy.unregister_subevent(subevent) + + def update_container(self, subevent): + self.strategy.update(subevent) + + def update_properties(self, text, category=None): + self.text = text + self.category = category diff --git a/timelinelib/db/objects/event.py b/timelinelib/db/objects/event.py new file mode 100644 index 0000000..e5fa94e --- /dev/null +++ b/timelinelib/db/objects/event.py @@ -0,0 +1,139 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +from timelinelib.db.objects.timeperiod import TimePeriod + + +class Event(object): + + def __init__(self, time_type, start_time, end_time, text, category=None, + fuzzy=False, locked=False, ends_today=False): + self.time_type = time_type + self.fuzzy = fuzzy + self.locked = locked + self.ends_today = ends_today + self.id = None + self.selected = False + self.draw_ballon = False + self.update(start_time, end_time, text, category) + self.data = {} + + def has_id(self): + return self.id is not None + + def set_id(self, id): + self.id = id + + def update(self, start_time, end_time, text, category=None, fuzzy=None, + locked=None, ends_today=None): + """Change the event data.""" + self.time_period = TimePeriod(self.time_type, start_time, end_time) + self.text = text + self.category = category + if ends_today is not None: + if not self.locked: + self.ends_today = ends_today + if fuzzy is not None: + self.fuzzy = fuzzy + if locked is not None: + self.locked = locked + + def update_period(self, start_time, end_time): + """Change the event period.""" + self.time_period = TimePeriod(self.time_type, start_time, end_time) + + def update_period_o(self, new_period): + self.update_period(new_period.start_time, new_period.end_time) + + def update_start(self, start_time): + """Change the event data.""" + if start_time <= self.time_period.end_time: + self.time_period = TimePeriod( + self.time_type, start_time, self.time_period.end_time) + return True + return False + + def update_end(self, end_time): + """Change the event data.""" + if end_time >= self.time_period.start_time: + self.time_period = TimePeriod( + self.time_type, self.time_period.start_time, end_time) + return True + return False + + def inside_period(self, time_period): + """Wrapper for time period method.""" + return self.time_period.overlap(time_period) + + def is_period(self): + """Wrapper for time period method.""" + return self.time_period.is_period() + + def mean_time(self): + """Wrapper for time period method.""" + return self.time_period.mean_time() + + def get_data(self, id): + """ + Return data with the given id or None if no data with that id exists. + + See set_data for information how ids map to data. + """ + return self.data.get(id, None) + + def set_data(self, id, data): + """ + Set data with the given id. + + Here is how ids map to data: + + description - string + icon - wx.Bitmap + """ + self.data[id] = data + + def has_data(self): + """Return True if the event has associated data, or False if not.""" + for id in self.data: + if self.data[id] != None: + return True + return False + + def get_label(self): + """Returns a unicode label describing the event.""" + return u"%s (%s)" % (self.text, self.time_period.get_label()) + + def clone(self): + # Objects of type datetime are immutable. + new_event = Event(self.time_type, self.time_period.start_time, + self.time_period.end_time, self.text, self.category) + # Description is immutable + new_event.set_data("description", self.get_data("description") ) + # Icon is immutable in the sense that it is never changed by our + # application. + new_event.set_data("icon", self.get_data("icon")) + return new_event + + def is_container(self): + return False + + def is_subevent(self): + return False + + def time_span(self): + return self.time_period.end_time - self.time_period.start_time diff --git a/timelinelib/db/objects/subevent.py b/timelinelib/db/objects/subevent.py new file mode 100644 index 0000000..c481485 --- /dev/null +++ b/timelinelib/db/objects/subevent.py @@ -0,0 +1,57 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +from timelinelib.db.objects.event import Event + + +class Subevent(Event): + + def __init__(self, time_type, start_time, end_time, text, category=None, + container=None, cid=-1): + Event.__init__(self, time_type, start_time, end_time, text, category, + False, False, False) + self.container = container + if self.container != None: + self.container_id = self.container.cid() + else: + self.container_id = cid + + def is_container(self): + """Overrides parent method.""" + return False + + def is_subevent(self): + """Overrides parent method.""" + return True + + def update_period(self, start_time, end_time): + """Overrides parent method.""" + Event.update_period(self, start_time, end_time) + self.container.update_container(self) + + def update_period_o(self, new_period): + """Overrides parent method.""" + Event.update_period(self, new_period.start_time, new_period.end_time) + self.container.update_container(self) + + def cid(self): + return self.container_id + + def register_container(self, container): + self.container = container + self.container_id = container.cid() diff --git a/timelinelib/db/objects/timeperiod.py b/timelinelib/db/objects/timeperiod.py new file mode 100644 index 0000000..51a574d --- /dev/null +++ b/timelinelib/db/objects/timeperiod.py @@ -0,0 +1,230 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +class TimePeriod(object): + """ + Represents a period in time using a start and end time. + + This is used both to store the time period for an event and for storing the + currently displayed time period in the GUI. + """ + + def __init__(self, time_type, start_time, end_time): + """ + Create a time period. + + `start_time` and `end_time` should be of a type that can be handled + by the time_type object. + """ + self.time_type = time_type + self.start_time, self.end_time = self._update(start_time, end_time) + + def clone(self): + return TimePeriod(self.time_type, self.start_time, self.end_time) + + def __eq__(self, other): + if isinstance(other, TimePeriod): + return (self.start_time == other.start_time and + self.end_time == other.end_time) + return False + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "TimePeriod<%s, %s>" % (self.start_time, self.end_time) + + def update(self, start_time, end_time, + start_delta=None, end_delta=None): + new_start, new_end = self._update(start_time, end_time, start_delta, end_delta) + return TimePeriod(self.time_type, new_start, new_end) + + def _update(self, start_time, end_time, + start_delta=None, end_delta=None): + """ + Change the time period data. + + Optionally add the deltas to the times like this: time + delta. + + If data is invalid, it will not be set, and a ValueError will be raised + instead. + + Data is invalid if time + delta is not within the range + [self.time_type.get_min_time(), self.time_type.get_max_time()] or if + the start time is larger than the end time. + """ + new_start = self._ensure_within_range(start_time, start_delta, + _("Start time ")) + new_end = self._ensure_within_range(end_time, end_delta, + _("End time ")) + self._assert_period_is_valid(new_start, new_end) + return (new_start, new_end) + + def _assert_period_is_valid(self, new_start, new_end): + self._assert_start_gt_end(new_start, new_end) + self._assert_period_lt_max(new_start, new_end) + + def _assert_start_gt_end(self, new_start, new_end): + if new_start > new_end: + raise ValueError(_("Start time can't be after end time")) + + def _assert_period_lt_max(self, new_start, new_end): + MAX_ZOOM_DELTA, max_zoom_error_text = self.time_type.get_max_zoom_delta() + if MAX_ZOOM_DELTA and (new_end - new_start > MAX_ZOOM_DELTA): + raise PeriodTooLongError(max_zoom_error_text) + + def inside(self, time): + """ + Return True if the given time is inside this period or on the border, + otherwise False. + """ + return time >= self.start_time and time <= self.end_time + + def overlap(self, time_period): + """Return True if this time period has any overlap with the given.""" + return not (time_period.end_time < self.start_time or + time_period.start_time > self.end_time) + + def is_period(self): + """ + Return True if this time period is longer than just a point in time, + otherwise False. + """ + return self.start_time != self.end_time + + def mean_time(self): + """ + Return the time in the middle if this time period is longer than just a + point in time, otherwise the point in time for this time period. + """ + return self.start_time + self.time_type.half_delta(self.delta()) + + def zoom(self, times, ratio=0.5): + MAX_ZOOM_DELTA, max_zoom_error_text = self.time_type.get_max_zoom_delta() + MIN_ZOOM_DELTA, min_zoom_error_text = self.time_type.get_min_zoom_delta() + start_delta = self.time_type.mult_timedelta(self.delta(), times * ratio / 5.0) + end_delta = self.time_type.mult_timedelta(self.delta(), -times * (1.0 - ratio) / 5.0) + new_delta = self.delta() - 2 * start_delta + if MAX_ZOOM_DELTA and new_delta > MAX_ZOOM_DELTA: + raise ValueError(max_zoom_error_text) + if new_delta < MIN_ZOOM_DELTA: + raise ValueError(min_zoom_error_text) + return self.update(self.start_time, self.end_time, start_delta, end_delta) + + def move(self, direction): + """ + Move this time period one 10th to the given direction. + + Direction should be -1 for moving to the left or 1 for moving to the + right. + """ + delta = self.time_type.mult_timedelta(self.delta(), direction / 10.0) + return self.move_delta(delta) + + def move_delta(self, delta): + return self.update(self.start_time, self.end_time, delta, delta) + + def delta(self): + """Return the length of this time period as a timedelta object.""" + return self.end_time - self.start_time + + def center(self, time): + """ + Center time period around time keeping the length. + + If we can't center because we are on the edge, we do as good as we can. + """ + delta = time - self.mean_time() + start_overflow = self._calculate_overflow(self.start_time, delta)[1] + end_overflow = self._calculate_overflow(self.end_time, delta)[1] + if start_overflow == -1: + delta = self.time_type.get_min_time()[0] - self.start_time + elif end_overflow == 1: + delta = self.time_type.get_max_time()[0] - self.end_time + return self.move_delta(delta) + + def _ensure_within_range(self, time, delta, error_prefix): + """ + Return new time (time + delta) or raise ValueError if it is not within + the range [self.time_type.get_min_time(), + self.time_type.get_max_time()]. + """ + if delta == None: + delta = self.time_type.get_zero_delta() + new_time, overflow, error_text = self._calculate_overflow(time, delta) + if overflow != 0: + error_text = "%s %s" % (error_prefix, error_text) + raise ValueError(error_text) + else: + return new_time + + def _calculate_overflow(self, time, delta): + """ + Return a tuple (new time, overflow flag). + + Overflow flag can be -1 (overflow to the left), 0 (no overflow), or 1 + (overflow to the right). + + If overflow flag is 0 new time is time + delta, otherwise None. + """ + try: + min_time, min_error_text = self.time_type.get_min_time() + max_time, max_error_text = self.time_type.get_max_time() + new_time = time + delta + if min_time and new_time < min_time: + return (None, -1, min_error_text) + if max_time and new_time > max_time: + return (None, 1, max_error_text) + return (new_time, 0, "") + except OverflowError: + if delta > self.time_type.get_zero_delta(): + return (None, 1, max_error_text) + else: + return (None, -1, min_error_text) + + def get_label(self): + """Returns a unicode string describing the time period.""" + return self.time_type.format_period(self) + + def has_nonzero_time(self): + return self.time_type.time_period_has_nonzero_time(self) + + +class TimeOutOfRangeLeftError(ValueError): + pass + + +class TimeOutOfRangeRightError(ValueError): + pass + + +class PeriodTooLongError(ValueError): + pass + + +def time_period_center(time_type, time, length): + """ + TimePeriod factory method. + + Return a time period with the given length (represented as a timedelta) + centered around `time`. + """ + half_length = time_type.mult_timedelta(length, 0.5) + start_time = time - half_length + end_time = time + half_length + return TimePeriod(time_type, start_time, end_time) diff --git a/timelinelib/db/observer.py b/timelinelib/db/observer.py new file mode 100644 index 0000000..65b9806 --- /dev/null +++ b/timelinelib/db/observer.py @@ -0,0 +1,39 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +# A category was added, edited, or deleted +STATE_CHANGE_CATEGORY = 1 +# Something happened that changed the state of the timeline +STATE_CHANGE_ANY = 2 + + +class Observable(object): + + def __init__(self): + self.observers = [] + + def register(self, fn): + self.observers.append(fn) + + def unregister(self, fn): + if fn in self.observers: + self.observers.remove(fn) + + def _notify(self, state_change): + for fn in self.observers: + fn(state_change) diff --git a/timelinelib/db/search.py b/timelinelib/db/search.py new file mode 100644 index 0000000..9dc9134 --- /dev/null +++ b/timelinelib/db/search.py @@ -0,0 +1,26 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +def generic_event_search(events, search_string): + def match(event): + return search_string.lower() in event.text.lower() + def mean_time(event): + return event.mean_time() + matches = [event for event in events if match(event)] + matches.sort(key=mean_time) + return matches diff --git a/timelinelib/db/strategies.py b/timelinelib/db/strategies.py index 394f2d5..7c51e9a 100644 --- a/timelinelib/db/strategies.py +++ b/timelinelib/db/strategies.py @@ -20,10 +20,10 @@ from timelinelib.db.interface import ContainerStrategy class DefaultContainerStrategy(ContainerStrategy): - + def __init__(self, container): ContainerStrategy.__init__(self, container) - + def register_subevent(self, subevent): if subevent not in self.container.events: self.container.events.append(subevent) @@ -43,18 +43,18 @@ class DefaultContainerStrategy(ContainerStrategy): self.unregister_subevent(subevent) self.register_subevent(subevent) self._set_time_period() - + def _set_time_period(self): """ The container time period starts where the subevent with the earliest - start time, starts, and it ends where the subevent whith the latest end + start time, starts, and it ends where the subevent whith the latest end time ends. Subevents +------+ +--------+ +--+ Container +--------------------------+ """ if len(self.container.events) == 0: return - self._set_start_time(self.container.events[0]) + self._set_start_time(self.container.events[0]) self._set_end_time(self.container.events[0]) for event in self.container.events: if self._container_starts_after_event(event): @@ -63,23 +63,23 @@ class DefaultContainerStrategy(ContainerStrategy): self._set_end_time(event) def _container_starts_after_event(self, subevent): - return (self.container.time_period.start_time > + return (self.container.time_period.start_time > subevent.time_period.start_time) def _container_ends_before_event(self, event): - return (self.container.time_period.end_time < + return (self.container.time_period.end_time < event.time_period.end_time) - + def _set_start_time(self, event): self.container.time_period.start_time = event.time_period.start_time def _set_end_time(self, event): self.container.time_period.end_time = event.time_period.end_time - + def _adjust_time_period(self, new_event): """ If the event to be added to the container overlaps any other - event in the container or if the new event is outside of the + event in the container or if the new event is outside of the container time period the container time period must be adjusted. """ event = self._event_totally_overlapping_new_event(new_event) @@ -110,17 +110,17 @@ class DefaultContainerStrategy(ContainerStrategy): self._move_events_left(new_event, event) else: self._move_events_right(new_event, event) - + def _move_events_left(self, new_event, event): delta = event.time_period.end_time - new_event.time_period.start_time latest_start_time = event.time_period.start_time self._move_early_events_left(new_event, latest_start_time, delta) def _move_events_right(self, new_event, event): - delta = new_event.time_period.end_time - event.time_period.start_time + delta = new_event.time_period.end_time - event.time_period.start_time earliest_start_time = event.time_period.start_time self._move_late_events_right(new_event, earliest_start_time, delta) - + def _adjust_when_new_event_partially_overlaps_other_events(self, new_event, events): # Situation: # V = threshold_time @@ -131,24 +131,24 @@ class DefaultContainerStrategy(ContainerStrategy): # or +-------------+ # or +-------------+ # or +-------------+ - threshold_time = self._calc_threshold_time(new_event) - event = self._some_event_in_new_event_threshold_time(new_event, - events, + threshold_time = self._calc_threshold_time(new_event) + event = self._some_event_in_new_event_threshold_time(new_event, + events, threshold_time) if event is not None: self._adjust_threshold_triggered_events(new_event, event, threshold_time) - earliest_start = self._earliest_start_time_for_event_that_starts_within_new_event(new_event, - events, + earliest_start = self._earliest_start_time_for_event_that_starts_within_new_event(new_event, + events, threshold_time) if earliest_start is not None: self._adjust_events_starting_in_new_event(new_event, earliest_start) - + def _calc_threshold_time(self, new_event): - td = new_event.time_span() + td = new_event.time_span() td = new_event.time_type.mult_timedelta(td, 0.2) threshold_time = new_event.time_period.start_time + td - return threshold_time - + return threshold_time + def _some_event_in_new_event_threshold_time(self, new_event, events, end): start = new_event.time_period.start_time for event in events: @@ -159,11 +159,11 @@ class DefaultContainerStrategy(ContainerStrategy): if event.time_period.start_time <= start and event.time_period.end_time > end: return event return None - + def _adjust_threshold_triggered_events(self, new_event, event, threshold_time): delta = event.time_period.end_time - new_event.time_period.start_time self._move_early_events_left(new_event, threshold_time, delta) - + def _earliest_start_time_for_event_that_starts_within_new_event(self, new_event, events, thr): start = new_event.time_period.start_time end = new_event.time_period.end_time @@ -179,11 +179,11 @@ class DefaultContainerStrategy(ContainerStrategy): if event.time_period.start_time < min_start: min_start = event.time_period.start_time return min_start - + def _adjust_events_starting_in_new_event(self, new_event, earliest_start): delta = new_event.time_period.end_time - earliest_start self._move_late_events_right(new_event, earliest_start, delta) - + def _event_totally_overlapping_new_event(self, new_event): for event in self.container.events: if event == new_event: @@ -191,20 +191,20 @@ class DefaultContainerStrategy(ContainerStrategy): if (self._event_totally_overlaps_new_event(new_event, event)): return event return None - + def _event_totally_overlaps_new_event(self, new_event, event): - return (event.time_period.start_time <= new_event.time_period.start_time and + return (event.time_period.start_time <= new_event.time_period.start_time and event.time_period.end_time >= new_event.time_period.end_time) - - def _events_overlapped_by_new_event(self, new_event): + + def _events_overlapped_by_new_event(self, new_event): overlapping_events = [] for event in self.container.events: if event != new_event: - if (self._starts_within(event, new_event) or + if (self._starts_within(event, new_event) or self._ends_within(event, new_event)): overlapping_events.append(event) return overlapping_events - + def _starts_within(self, event, new_event): s1 = event.time_period.start_time >= new_event.time_period.start_time s2 = event.time_period.start_time <= new_event.time_period.end_time @@ -214,7 +214,7 @@ class DefaultContainerStrategy(ContainerStrategy): s1 = event.time_period.end_time >= new_event.time_period.start_time s2 = event.time_period.end_time <= new_event.time_period.end_time return (s1 and s2) - + def _move_early_events_left(self, new_event, latest_start_time, delta): delta = -delta for event in self.container.events: @@ -222,17 +222,16 @@ class DefaultContainerStrategy(ContainerStrategy): continue if event.time_period.start_time <= latest_start_time: self._adjust_event_time_period(event, delta) - + def _move_late_events_right(self, new_event, earliest_start_time, delta): for event in self.container.events: if event == new_event: continue if event.time_period.start_time >= earliest_start_time: self._adjust_event_time_period(event, delta) - + def _adjust_event_time_period(self, event, delta): new_start = event.time_period.start_time + delta new_end = event.time_period.end_time + delta event.time_period.start_time = new_start event.time_period.end_time = new_end - diff --git a/timelinelib/db/utils.py b/timelinelib/db/utils.py index 4163e31..27d1524 100644 --- a/timelinelib/db/utils.py +++ b/timelinelib/db/utils.py @@ -20,7 +20,7 @@ import codecs import os import os.path -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError class IdCounter(object): @@ -33,16 +33,6 @@ class IdCounter(object): return self.id -def generic_event_search(events, search_string): - def match(event): - return search_string.lower() in event.text.lower() - def mean_time(event): - return event.mean_time() - matches = [event for event in events if match(event)] - matches.sort(key=mean_time) - return matches - - def safe_write(path, encoding, write_fn): """ Write to path in such a way that the contents of path is only modified diff --git a/timelinelib/drawing/__init__.py b/timelinelib/drawing/__init__.py index d980c6d..9f2d122 100644 --- a/timelinelib/drawing/__init__.py +++ b/timelinelib/drawing/__init__.py @@ -16,16 +16,6 @@ # along with Timeline. If not, see . -""" -Functionality for drawing timelines onto device contexts (wx.DC). -""" - - def get_drawer(): - """ - Factory method. - - Return the drawing algorithm that should be used by the application. - """ from timelinelib.drawing.drawers.default import DefaultDrawingAlgorithm return DefaultDrawingAlgorithm() diff --git a/timelinelib/drawing/drawers/default.py b/timelinelib/drawing/drawers/default.py index d821708..b48de53 100644 --- a/timelinelib/drawing/drawers/default.py +++ b/timelinelib/drawing/drawers/default.py @@ -22,7 +22,7 @@ import os.path import wx from timelinelib.config.paths import ICONS_DIR -from timelinelib.domain.category import sort_categories +from timelinelib.db.objects.category import sort_categories from timelinelib.drawing.interface import Drawer from timelinelib.drawing.scene import TimelineScene from timelinelib.drawing.utils import darken_color @@ -139,8 +139,7 @@ class DefaultDrawingAlgorithm(Drawer): left_strip_time, right_strip_time = self._snap_region(time) return right_strip_time - def _snap_region(self, time): - time_x = self.scene.x_pos_for_time(time) + def _snap_region(self, time): left_strip_time = self.scene.minor_strip.start(time) right_strip_time = self.scene.minor_strip.increment(left_strip_time) return (left_strip_time, right_strip_time) @@ -158,7 +157,7 @@ class DefaultDrawingAlgorithm(Drawer): return event container_event = event else: - return event + return event return container_event def event_with_rect_at(self, x, y, alt_down=False): @@ -172,9 +171,9 @@ class DefaultDrawingAlgorithm(Drawer): container_event = event container_rect = rect else: - return event, rect + return event, rect if container_event == None: - return None + return None return container_event, container_rect def event_rect(self, evt): @@ -308,14 +307,14 @@ class DefaultDrawingAlgorithm(Drawer): def _point_subevent(self, event): return event.is_subevent() and not event.is_period() - + def _get_container_y(self, id): for (event, rect) in self.scene.event_data: if event.is_container(): if event.container_id == id: return rect.y - 1 return self.scene.divider_y - + def _set_line_color(self, view_properties, event): if view_properties.is_selected(event): self.dc.SetPen(self.red_solid_pen) @@ -446,7 +445,7 @@ class DefaultDrawingAlgorithm(Drawer): p3 < \ p4 \p5 ---------- - """ + """ x1 = rect.x x2 = rect.x + rect.height / 2 y1 = rect.y @@ -462,7 +461,7 @@ class DefaultDrawingAlgorithm(Drawer): def _draw_fuzzy_end(self, rect, event): """ ---- P2\ p1 - \ + \ > p3 / ---- p4/ p4 @@ -561,7 +560,7 @@ class DefaultDrawingAlgorithm(Drawer): self.dc.SetClippingRect(rect_copy) text_x = rect.X + INNER_PADDING if event.fuzzy or event.locked: - text_x += rect.Height / 2 + text_x += rect.Height / 2 text_y = rect.Y + INNER_PADDING if text_x < INNER_PADDING: text_x = INNER_PADDING @@ -696,7 +695,7 @@ class DefaultDrawingAlgorithm(Drawer): (iw, ih) = icon.Size inner_rect_w = iw inner_rect_h = ih - max_text_width = max(MIN_TEXT_WIDTH, (self.scene.width - SLIDER_WIDTH - event_rect.X - iw)) + max_text_width = max(MIN_TEXT_WIDTH, (self.scene.width - SLIDER_WIDTH - event_rect.X - iw)) # Text self.dc.SetFont(get_default_font(8)) font_h = self.dc.GetCharHeight() diff --git a/timelinelib/drawing/interface.py b/timelinelib/drawing/interface.py index 95101a5..8292d0d 100644 --- a/timelinelib/drawing/interface.py +++ b/timelinelib/drawing/interface.py @@ -126,4 +126,4 @@ class Strip(object): def get_font(self, time_period): """ Return the preferred font for this strip - """ + """ diff --git a/timelinelib/drawing/scene.py b/timelinelib/drawing/scene.py index 8660d5e..a8b0255 100644 --- a/timelinelib/drawing/scene.py +++ b/timelinelib/drawing/scene.py @@ -37,8 +37,8 @@ class TimelineScene(object): self._baseline_padding = 15 self._period_threshold = 20 self._data_indicator_size = 10 - self._metrics = Metrics(size, self._db.get_time_type(), - self._view_properties.displayed_period, + self._metrics = Metrics(size, self._db.get_time_type(), + self._view_properties.displayed_period, self._view_properties.divider_position) self.width, self.height = size self.divider_y = self._metrics.half_height @@ -100,7 +100,7 @@ class TimelineScene(object): return None def _event_rect_drawn_as_period(self, event_rect): - return event_rect.Y >= self.divider_y + return event_rect.Y >= self.divider_y def _get_direction(self, period, up): if up: @@ -117,7 +117,7 @@ class TimelineScene(object): def _get_overlapping_event(self, period, direction, selected_event, rect): list = self._get_overlapping_events_list(period, rect) - event = self._get_overlapping_event_from_list(list, direction, + event = self._get_overlapping_event_from_list(list, direction, selected_event) return event @@ -131,7 +131,7 @@ class TimelineScene(object): def _get_overlapping_event_from_list(self, list, direction, selected_event): if direction == FORWARD: return self._get_next_overlapping_event(list, selected_event) - else: + else: return self._get_prev_overlapping_event(list, selected_event) def _get_next_overlapping_event(self, list, selected_event): @@ -158,13 +158,13 @@ class TimelineScene(object): self._calc_rects(visible_events) def _place_subevents_last(self, events): - reordered_events = [event for event in events + reordered_events = [event for event in events if not event.is_subevent()] - subevents = [event for event in events + subevents = [event for event in events if event.is_subevent()] reordered_events.extend(subevents) return reordered_events - + def _calc_rects(self, events): self.event_data = [] for event in events: @@ -181,7 +181,7 @@ class TimelineScene(object): def _period_subevent(self, event): return event.is_subevent() and event.is_period() - + def _create_rectangle_for_period_subevent(self, event): return self._create_ideal_rect_for_event(event) @@ -190,7 +190,7 @@ class TimelineScene(object): self._ensure_rect_is_not_far_outisde_screen(rect) self._prevent_overlapping_by_adjusting_rect_y(event, rect) return rect - + def _create_ideal_rect_for_event(self, event): if event.ends_today: event.time_period.end_time = self._db.get_time_type().now() @@ -219,11 +219,11 @@ class TimelineScene(object): return min_width def _calc_subevent_threshold_width(self, event): - # The enlarging factor allows sub-events to be smaller than a normal + # The enlarging factor allows sub-events to be smaller than a normal # event before the container becomes a point event. enlarging_factor = 2 return enlarging_factor * self._metrics.calc_width(event.time_period) - + def _create_ideal_rect_for_period_event(self, event): tw, th = self._get_text_size(event.text) ew = self._metrics.calc_width(event.time_period) @@ -237,20 +237,20 @@ class TimelineScene(object): return rect def _get_ry(self, event): - if event.is_subevent(): + if event.is_subevent(): if event.is_period(): return self._get_container_ry(event) else: return self._metrics.half_height - self._baseline_padding else: return self._metrics.half_height + self._baseline_padding - + def _get_container_ry(self, subevent): for (event, rect) in self.event_data: if event == subevent.container: return rect.y return self._metrics.half_height + self._baseline_padding - + def _create_ideal_rect_for_non_period_event(self, event): tw, th = self._get_text_size(event.text) rw = tw + 2 * self._inner_padding + 2 * self._outer_padding @@ -286,10 +286,14 @@ class TimelineScene(object): def fill(list, strip): """Fill the given list with the given strip.""" current_start = strip.start(self._view_properties.displayed_period.start_time) - while current_start < self._view_properties.displayed_period.end_time: - next_start = strip.increment(current_start) - list.append(TimePeriod(self._db.get_time_type(), current_start, next_start)) - current_start = next_start + try: + while current_start < self._view_properties.displayed_period.end_time: + next_start = strip.increment(current_start) + list.append(TimePeriod(self._db.get_time_type(), current_start, next_start)) + current_start = next_start + except: + #Exception occurs when major=century and when we are at the end of the calendar + pass self.major_strip_data = [] # List of time_period self.minor_strip_data = [] # List of time_period self.major_strip, self.minor_strip = self._db.get_time_type().choose_strip(self._metrics, self._config) @@ -326,7 +330,7 @@ class TimelineScene(object): return rect_with_largest_y def _get_list_with_overlapping_period_events(self, event_rect): - return [(event, rect) for (event, rect) in self.event_data + return [(event, rect) for (event, rect) in self.event_data if (self._rects_overlap(event_rect, rect) and rect.Y >= self.divider_y )] @@ -344,10 +348,10 @@ class TimelineScene(object): return rect_with_smallest_y def _get_list_with_overlapping_point_events(self, event_rect): - return [(event, rect) for (event, rect) in self.event_data + return [(event, rect) for (event, rect) in self.event_data if (self._rects_overlap(event_rect, rect) and rect.Y < self.divider_y )] def _rects_overlap(self, rect1, rect2): return (rect2.x <= rect1.x + rect1.width and - rect1.x <= rect2.x + rect2.width) + rect1.x <= rect2.x + rect2.width) diff --git a/timelinelib/drawing/viewproperties.py b/timelinelib/drawing/viewproperties.py index 77f06dc..cb2715e 100644 --- a/timelinelib/drawing/viewproperties.py +++ b/timelinelib/drawing/viewproperties.py @@ -47,7 +47,7 @@ class ViewProperties(object): if cat is None: return True elif e.is_subevent(): - container_visible = category_visible(e.container, + container_visible = category_visible(e.container, e.container.category) if container_visible: if self.category_visible(cat) == True: diff --git a/timelinelib/editors/category.py b/timelinelib/editors/category.py index aeb5c85..bb62004 100644 --- a/timelinelib/editors/category.py +++ b/timelinelib/editors/category.py @@ -16,9 +16,7 @@ # along with Timeline. If not, see . -import wx - -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import Category @@ -60,7 +58,7 @@ class CategoryEditor(object): self.view.handle_used_name(new_name) return if self.category is None: - self.category = Category(new_name, new_color, new_font_color, + self.category = Category(new_name, new_color, new_font_color, True, parent=new_parent) else: self.category.name = new_name diff --git a/timelinelib/editors/container.py b/timelinelib/editors/container.py index a8936f3..e6b4a0c 100644 --- a/timelinelib/editors/container.py +++ b/timelinelib/editors/container.py @@ -16,7 +16,7 @@ # along with Timeline. If not, see . -from timelinelib.db.container import Container +from timelinelib.db.objects import Container from timelinelib.repositories.dbwrapper import DbWrapperEventRepository @@ -30,8 +30,8 @@ class ContainerEditor(object): container is saved to the database. The reason for this behavior is that we don't want to have empty Conatiners in the database. - When updating the properties of an existing Container event the changes - are stored in the timeline database. + When updating the properties of an existing Container event the changes + are stored in the timeline database. """ def __init__(self, view, db, container): self._set_initial_values_to_member_variables(view, db, container) @@ -41,7 +41,7 @@ class ContainerEditor(object): self.view = view self.db = db self.container = container - self.container_exists = (self.container != None) + self.container_exists = (self.container != None) if self.container_exists: self.name = self.container.text self.category = self.container.category @@ -52,7 +52,7 @@ class ContainerEditor(object): def _set_view_initial_values(self): self.view.set_name(self.name) self.view.set_category(self.category) - + # # Dialog API # @@ -71,12 +71,12 @@ class ContainerEditor(object): def get_container(self): return self.container - + # # Internals # def _verify_name(self): - name_is_invalid = (self.name == "") + name_is_invalid = (self.name == "") if name_is_invalid: msg = _("Field '%s' can't be empty.") % _("Name") self.view.display_invalid_name(msg) @@ -85,7 +85,7 @@ class ContainerEditor(object): def _update_container(self): self.container.update_properties(self.name, self.category) self._save_to_db() - + def _save_to_db(self): try: DbWrapperEventRepository(self.db).save(self.container) @@ -97,5 +97,5 @@ class ContainerEditor(object): time_type = self.db.get_time_type() start = time_type.now() end = start - self.container = Container(time_type, start, end, self.name, + self.container = Container(time_type, start, end, self.name, self.category) diff --git a/timelinelib/editors/duplicateevent.py b/timelinelib/editors/duplicateevent.py index e4bbf84..fa808ed 100644 --- a/timelinelib/editors/duplicateevent.py +++ b/timelinelib/editors/duplicateevent.py @@ -16,7 +16,7 @@ # along with Timeline. If not, see . -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError FORWARD = 0 @@ -39,13 +39,13 @@ class DuplicateEventEditor(object): def create_duplicates_and_save(self): (periods, nbr_of_missing_dates) = self._repeat_period( - self.event.time_period, + self.event.time_period, self.view.get_move_period_fn(), self.view.get_frequency(), self.view.get_count(), self.view.get_direction()) try: - for period in periods: + for period in periods: event = self.event.clone() event.update_period(period.start_time, period.end_time) self.db.save_event(event) @@ -57,14 +57,14 @@ class DuplicateEventEditor(object): def _repeat_period(self, period, move_period_fn, frequency, repetitions, direction): - periods = [] + periods = [] nbr_of_missing_dates = 0 for index in self._calc_indicies(direction, repetitions): new_period = move_period_fn(period, index*frequency) if new_period == None: nbr_of_missing_dates += 1 else: - periods.append(new_period) + periods.append(new_period) return (periods, nbr_of_missing_dates) def _calc_indicies(self, direction, repetitions): diff --git a/timelinelib/editors/event.py b/timelinelib/editors/event.py index 7506bae..fbe6f18 100644 --- a/timelinelib/editors/event.py +++ b/timelinelib/editors/event.py @@ -17,11 +17,10 @@ from timelinelib.db.objects import Event -from timelinelib.db.subevent import Subevent from timelinelib.db.objects import PeriodTooLongError +from timelinelib.db.objects import Subevent from timelinelib.db.objects import TimePeriod from timelinelib.utils import ex_msg -from timelinelib.repositories.dbwrapper import DbWrapperEventRepository class EventEditor(object): @@ -53,6 +52,11 @@ class EventEditor(object): self.view.set_name(self.name) self.view.set_focus("start") + def start_is_in_history(self): + if self.start is None: + return False + return self.start < self.timeline.time_type.now() + def _set_values(self, start, end, event): self.event = event if self.event != None: @@ -71,7 +75,7 @@ class EventEditor(object): self.fuzzy = False self.locked = False self.ends_today = False - + def _set_view_content(self): if self.event != None: self.view.set_event_data(self.event.data) @@ -131,7 +135,7 @@ class EventEditor(object): return self.get_start_from_view() def _dialog_has_signalled_invalid_input(self, time): - return time == None + return time == None def _verify_that_time_has_not_been_changed(self, start, end): self._exception_if_start_has_changed(start) @@ -166,8 +170,8 @@ class EventEditor(object): if container_selected: if self.event.is_subevent(): if self.event.container == self.container: - self.event.update(self.start, self.end, self.name, - self.category, self.fuzzy, self.locked, + self.event.update(self.start, self.end, self.name, + self.category, self.fuzzy, self.locked, self.ends_today) else: self._change_container() @@ -178,49 +182,49 @@ class EventEditor(object): self._remove_event_from_container() pass else: - self.event.update(self.start, self.end, self.name, - self.category, self.fuzzy, self.locked, + self.event.update(self.start, self.end, self.name, + self.category, self.fuzzy, self.locked, self.ends_today) - + def _remove_event_from_container(self): self.event.container.unregister_subevent(self.event) self.timeline.delete_event(self.event) self._create_new_event() - + def _add_event_to_container(self): self.timeline.delete_event(self.event) self._create_subevent() - + def _change_container(self): self.event.container.unregister_subevent(self.event) self.container.register_subevent(self.event) - + def _create_new_event(self): if self.container != None: self._create_subevent() else: - self.event = Event(self.time_type, self.start, self.end, self.name, - self.category, self.fuzzy, self.locked, + self.event = Event(self.time_type, self.start, self.end, self.name, + self.category, self.fuzzy, self.locked, self.ends_today) - + def _create_subevent(self): if self.is_new_container(self.container): self.add_new_container() - self.event = Subevent(self.time_type, self.start, self.end, self.name, + self.event = Subevent(self.time_type, self.start, self.end, self.name, self.category, self.container) def is_new_container(self, container): return container not in self.timeline.get_containers() - + def add_new_container(self): max_id = 0 for container in self.timeline.get_containers(): if container.cid() > max_id: max_id = container.cid() max_id += 1 - self.container.set_cid(max_id) + self.container.set_cid(max_id) self._save_container_to_db() - + def _validate_and_save_start(self, start): if start == None: raise ValueError() diff --git a/timelinelib/editors/textdisplay.py b/timelinelib/editors/textdisplay.py new file mode 100644 index 0000000..5ca62a0 --- /dev/null +++ b/timelinelib/editors/textdisplay.py @@ -0,0 +1,32 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +class TextDisplayEditor(object): + + def __init__(self, view, text=""): + self.view = view + self.text = text + + def initialize(self): + self.set_text(self.text) + + def set_text(self, text): + self.view.set_text(text) + + def get_text(self): + return self.view.get_text() diff --git a/timelinelib/export/svg.py b/timelinelib/export/svg.py index 67fbfa7..6718984 100644 --- a/timelinelib/export/svg.py +++ b/timelinelib/export/svg.py @@ -18,6 +18,7 @@ from types import UnicodeType +import wx from pysvg.structure import * from pysvg.core import * from pysvg.text import * @@ -25,11 +26,9 @@ from pysvg.shape import * from pysvg.builders import * from pysvg.filter import * -from timelinelib.domain.category import sort_categories +from timelinelib.db.objects.category import sort_categories from timelinelib.drawing.utils import darken_color -from datetime import datetime - OUTER_PADDING = 5 # Space between event boxes (pixels) INNER_PADDING = 3 # Space inside event box to text (pixels) @@ -54,7 +53,7 @@ class SVGDrawingAlgorithm(object): self.path = path self.scene = scene self.view_properties = view_properties - # SVG document size, maybe TODO + # SVG document size, maybe TODO self.metrics = dict({'widthpx':1052, 'heightpx':744}); # SVG document handle self.svg = svg(width="%dpx" % self.metrics['widthpx'], height="%dpx" % self.metrics['heightpx']) @@ -64,8 +63,8 @@ class SVGDrawingAlgorithm(object): self.mySmallTextStyle = StyleBuilder() self.myHeaderStyle.setFontFamily(fontfamily="Verdana") self.mySmallTextStyle.setFontFamily(fontfamily="Verdana") - self.mySmallTextStyle.setFontSize('3') - self.myHeaderStyle.setFontSize('7') + self.mySmallTextStyle.setFontSize('3') + self.myHeaderStyle.setFontSize('7') self.mySmallTextStyle.setFilling("black") self.myHeaderStyle.setFilling("black") filterShadow = filter(x="-.3",y="-.5", width=1.9, height=1.9) @@ -76,7 +75,7 @@ class SVGDrawingAlgorithm(object): filtOffset.set_in("out1") filtOffset.set_dx(4) filtOffset.set_dy(-4) - filtOffset.set_result("out2") + filtOffset.set_result("out2") filtMergeNode1 = feMergeNode() filtMergeNode1.set_in("out2") filtMergeNode2 = feMergeNode() @@ -136,7 +135,6 @@ class SVGDrawingAlgorithm(object): myStyle.setFontFamily(fontfamily="Verdana") myStyle.setFontSize("2em") myStyle.setTextAnchor('left') - oh = ShapeBuilder() svgGroup = g() self._draw_minor_strips(svgGroup, myStyle) self._draw_major_strips(svgGroup, myStyle) @@ -179,7 +177,7 @@ class SVGDrawingAlgorithm(object): oh = ShapeBuilder() style.setStrokeDashArray("") fontSize = MAJOR_STRIP_FONT_SIZE - style.setFontSize("%dem" % fontSize) + style.setFontSize("%dem" % fontSize) for tp in self.scene.major_strip_data: # Divider line x = self.scene.x_pos_for_time(tp.end_time) @@ -199,7 +197,7 @@ class SVGDrawingAlgorithm(object): x = INNER_PADDING extra_vertical_padding = fontSize * 4 # since there is no function like textwidth() for SVG, just take into account that text can be overwritten - # do not perform a special handling for right border, SVG is unlimited + # do not perform a special handling for right border, SVG is unlimited myText = self._text(label, x, fontSize*4+INNER_PADDING+extra_vertical_padding) myText.set_style(style) group.addElement(myText) @@ -226,7 +224,7 @@ class SVGDrawingAlgorithm(object): oh = ShapeBuilder() line = oh.createLine(x, y, x, self.scene.divider_y, stroke=myStroke) group.addElement(line) - circle = oh.createCircle(x, self.scene.divider_y, 2) + circle = oh.createCircle(x, self.scene.divider_y, 2) group.addElement(circle) # self.dc.DrawLine(x, y, x, self.scene.divider_y) # self.dc.DrawCircle(x, self.scene.divider_y, 2) @@ -253,8 +251,8 @@ class SVGDrawingAlgorithm(object): return border_color def _map_svg_color(self, color): - """ - map (r,g,b) color to svg string + """ + map (r,g,b) color to svg string """ sColor = "#%02X%02X%02X" % color return sColor @@ -295,10 +293,10 @@ class SVGDrawingAlgorithm(object): Motivation for positioning in right corner: SVG text cannot be centered since the text width cannot be calculated and the first part of each event text is important. - ergo: text needs to be left aligned. + ergo: text needs to be left aligned. But then the probability is high that a lot of text is at the left bottom - ergo: put the legend to the right. + ergo: put the legend to the right. +----------+ | Name O | @@ -335,10 +333,10 @@ class SVGDrawingAlgorithm(object): cur_y, item_height, item_height, fill=base_color, stroke=border_color) svgGroup.addElement(color_box_rect) - myText = self._svg_clipped_text(cat.name, - (x + OUTER_PADDING + INNER_PADDING+item_height, + myText = self._svg_clipped_text(cat.name, + (x + OUTER_PADDING + INNER_PADDING+item_height, cur_y, width-OUTER_PADDING-INNER_PADDING-item_height, - item_height ), + item_height ), myStyle) svgGroup.addElement(myText) cur_y = cur_y + item_height + INNER_PADDING @@ -358,12 +356,12 @@ class SVGDrawingAlgorithm(object): # Ensure that we can't draw content outside inner rectangle boxColor = self._get_box_color(event) boxBorderColor = self._get_box_border_color(event) - svgRect = oh.createRect(rect.X, rect.Y, - rect.GetWidth(), rect.GetHeight(), + svgRect = oh.createRect(rect.X, rect.Y, + rect.GetWidth(), rect.GetHeight(), stroke=boxBorderColor, fill=boxColor ) if self.shadowFlag: - svgRect.set_filter("url(#filterShadow)") + svgRect.set_filter("url(#filterShadow)") svgGroup.addElement(svgRect) if rect.Width > 0: # Draw the text (if there is room for it) @@ -393,7 +391,7 @@ class SVGDrawingAlgorithm(object): myString = self._encode_unicode_text(myString) # Put text,clipping into a SVG group group=g() - rx, ry, width, height = rectTuple + rx, ry, width, height = rectTuple text_x = rx + INNER_PADDING text_y = ry + height - INNER_PADDING # TODO: in SVG, negative value should be OK, but they @@ -405,7 +403,7 @@ class SVGDrawingAlgorithm(object): text_x = INNER_PADDING pathId = "path%d_%d" % (text_x, text_y) p = path(pathData= "M %d %d H %d V %d H %d" % \ - (rx, ry + height, + (rx, ry + height, text_x+width-INNER_PADDING, ry, rx)) clip = clipPath() diff --git a/timelinelib/help/pages.py b/timelinelib/help/pages.py index b327729..5bd83a2 100644 --- a/timelinelib/help/pages.py +++ b/timelinelib/help/pages.py @@ -90,7 +90,7 @@ The date data object used does not support week numbers for weeks that start on # Stars produce emphasized text. DON'T remove them. # Dashes produce bullet lists. DON'T remove them. body=_(""" -The timeline shows dates according to the Gregorian calendar on the x-axis. Currently the dates are limited to dates between year 10 and year 9989. +The timeline shows dates according to the Gregorian calendar on the x-axis. Currently the dates are limited to dates between year 10 and year 9989. Future versions might support various kinds of timelines so that you for example can specify a time in terms of number of minutes since a start time. If you are interested in such a feature, please get in touch. """)) diff --git a/timelinelib/meta/version.py b/timelinelib/meta/version.py index ce3fd16..4cdb852 100644 --- a/timelinelib/meta/version.py +++ b/timelinelib/meta/version.py @@ -16,7 +16,7 @@ # along with Timeline. If not, see . -VERSION = (0, 17, 0) +VERSION = (0, 18, 0) DEV = False diff --git a/timelinelib/play/playcontroller.py b/timelinelib/play/playcontroller.py index f132c08..0c4afcd 100644 --- a/timelinelib/play/playcontroller.py +++ b/timelinelib/play/playcontroller.py @@ -16,13 +16,11 @@ # along with Timeline. If not, see . -import datetime import time from timelinelib.db.objects import TimePeriod from timelinelib.db.objects import time_period_center from timelinelib.drawing.viewproperties import ViewProperties -from timelinelib.time.pytime import PyTimeType class PlayController(object): diff --git a/timelinelib/time/__init__.py b/timelinelib/time/__init__.py index 383e478..d320ea5 100644 --- a/timelinelib/time/__init__.py +++ b/timelinelib/time/__init__.py @@ -16,7 +16,7 @@ # along with Timeline. If not, see . -from timelinelib.time.pytime import PyTimeType from timelinelib.time.numtime import NumTimeType -from timelinelib.time.wxtime import WxTimeType +from timelinelib.time.pytime import PyTimeType from timelinelib.time.wxtime import try_to_create_wx_date_time_from_dmy +from timelinelib.time.wxtime import WxTimeType diff --git a/timelinelib/time/numtime.py b/timelinelib/time/numtime.py index ab898cc..3318ab8 100644 --- a/timelinelib/time/numtime.py +++ b/timelinelib/time/numtime.py @@ -16,11 +16,8 @@ # along with Timeline. If not, see . -import sys import re -import wx - from timelinelib.time.typeinterface import TimeType from timelinelib.db.objects import time_period_center from timelinelib.drawing.interface import Strip @@ -76,7 +73,6 @@ class NumTimeType(TimeType): def choose_strip(self, metrics, config): start_time = 1 end_time = 2 - period_width = 0 limit = 30 period = TimePeriod(self, start_time, end_time) period_width = metrics.calc_exact_width(period) @@ -108,7 +104,7 @@ class NumTimeType(TimeType): return time_period.start_time + delta * x_percent_of_width def div_timedeltas(self, delta1, delta2): - return delta1 / delta2 + return delta1 / delta2 def get_max_zoom_delta(self): return (None, None) diff --git a/timelinelib/time/pytime.py b/timelinelib/time/pytime.py index e0f0afc..bd0e335 100644 --- a/timelinelib/time/pytime.py +++ b/timelinelib/time/pytime.py @@ -130,7 +130,7 @@ class PyTimeType(TimeType): collector.append(u"1 %s" % _("minute")) elif minutes > 1: collector.append(u"%d %s" % (minutes, _("minutes"))) - delta_string = u" ".join(collector) + delta_string = u" ".join(collector) if delta_string == "": delta_string = "0" return delta_string @@ -194,10 +194,10 @@ class PyTimeType(TimeType): total_us1 = delta_to_microseconds(delta1) total_us2 = delta_to_microseconds(delta2) # Make sure that the result is a floating point number - return total_us1 / float(total_us2) + return total_us1 / float(total_us2) def get_max_zoom_delta(self): - return (timedelta(days=1200*365), + return (timedelta(days=1200*365), _("Can't zoom wider than 1200 years")) def get_min_zoom_delta(self): @@ -238,11 +238,11 @@ class PyTimeType(TimeType): return "%02d:%02d" % (time.hour, time.minute) def eventtimes_equals(self, time1, time2): - s1 = "%s %s" % (self.event_date_string(time1), + s1 = "%s %s" % (self.event_date_string(time1), self.event_date_string(time1)) - s2 = "%s %s" % (self.event_date_string(time2), + s2 = "%s %s" % (self.event_date_string(time2), self.event_date_string(time2)) - return s1 == s2 + return s1 == s2 def go_to_today_fn(main_frame, current_period, navigation_fn): @@ -392,15 +392,35 @@ def backward_one_year_fn(main_frame, current_period, navigation_fn): def fit_millennium_fn(main_frame, current_period, navigation_fn): mean = current_period.mean_time() - start = datetime(int(mean.year/1000)*1000, 1, 1) - end = datetime(int(mean.year/1000)*1000 + 1000, 1, 1) + if mean.year > get_millenium_max_year(): + year = get_millenium_max_year() + else: + year = max(get_min_year(), int(mean.year/1000)*1000) + start = datetime(year, 1, 1) + end = datetime(year + 1000, 1, 1) navigation_fn(lambda tp: tp.update(start, end)) +def get_min_year(): + return PyTimeType().get_min_time()[0].year + + +def get_millenium_max_year(): + return PyTimeType().get_max_time()[0].year - 1000 + + +def get_century_max_year(): + return PyTimeType().get_max_time()[0].year - 100 + + def fit_century_fn(main_frame, current_period, navigation_fn): mean = current_period.mean_time() - start = datetime(int(mean.year/100)*100, 1, 1) - end = datetime(int(mean.year/100)*100 + 100, 1, 1) + if mean.year > get_century_max_year(): + year = get_century_max_year() + else: + year = max(get_min_year(), int(mean.year/100)*100) + start = datetime(year, 1, 1) + end = datetime(year + 100, 1, 1) navigation_fn(lambda tp: tp.update(start, end)) @@ -449,13 +469,16 @@ class StripCentury(Strip): return datetime(max(self._century_start_year(time.year), 10), 1, 1) def increment(self, time): - return time.replace(year=time.year+100) + return time.replace(year=time.year + 100) def get_font(self, time_period): return get_default_font(8) def _century_start_year(self, year): - return (int(year) / 100) * 100 + year = (int(year) / 100) * 100 + #if year > get_century_max_year(): + # year = get_century_max_year + return year class StripDecade(Strip): @@ -643,8 +666,8 @@ def delta_to_microseconds(delta): def move_period_num_days(period, num): delta = timedelta(days=1) * num - start_time = period.start_time + delta - end_time = period.end_time + delta + start_time = period.start_time + delta + end_time = period.end_time + delta return TimePeriod(period.time_type, start_time, end_time) diff --git a/timelinelib/time/wxtime.py b/timelinelib/time/wxtime.py index 7ded2ec..61b50d2 100644 --- a/timelinelib/time/wxtime.py +++ b/timelinelib/time/wxtime.py @@ -18,10 +18,6 @@ import sys import re -from wx import DateTime -from wx import DateSpan -from wx import TimeSpan -import calendar import wx @@ -132,7 +128,7 @@ class WxTimeType(TimeType): collector.append(u"1 %s" % _("minute")) elif minutes > 1: collector.append(u"%d %s" % (minutes, _("minutes"))) - delta_string = u" ".join(collector) + delta_string = u" ".join(collector) if delta_string == "": delta_string = "0" return delta_string @@ -195,7 +191,7 @@ class WxTimeType(TimeType): total_us1 = delta_to_microseconds(delta1) total_us2 = delta_to_microseconds(delta2) # Make sure that the result is a floating point number - return float(total_us1) / float(total_us2) + return float(total_us1) / float(total_us2) def get_max_zoom_delta(self): max_zoom_delta = wx.TimeSpan.Days(1200 * 365) @@ -241,11 +237,11 @@ class WxTimeType(TimeType): return time.Format("%H:%M") def eventtimes_equals(self, time1, time2): - s1 = "%s %s" % (self.event_date_string(time1), + s1 = "%s %s" % (self.event_date_string(time1), self.event_date_string(time1)) - s2 = "%s %s" % (self.event_date_string(time2), + s2 = "%s %s" % (self.event_date_string(time2), self.event_date_string(time2)) - return s1 == s2 + return s1 == s2 def go_to_today_fn(main_frame, current_period, navigation_fn): @@ -318,7 +314,7 @@ def _move_smart_month_backward(navigation_fn, start, end): new_start_year, new_start_month = _months_to_year_and_month( start_months - month_diff) - new_start = wx.DateTimeFromDMY(start.Day, new_start_month, new_start_year, + new_start = wx.DateTimeFromDMY(start.Day, new_start_month, new_start_year, start.Hour, start.Minute, start.Second) navigation_fn(lambda tp: tp.update(new_start, new_end)) @@ -331,7 +327,7 @@ def _move_smart_month_forward(navigation_fn, start, end): new_end_year, new_end_month = _months_to_year_and_month( end_months + month_diff) - new_end = wx.DateTimeFromDMY(end.Day, new_end_month, new_end_year, + new_end = wx.DateTimeFromDMY(end.Day, new_end_month, new_end_year, end.Hour, end.Minute, end.Second) navigation_fn(lambda tp: tp.update(new_start, new_end)) @@ -619,7 +615,7 @@ class StripHour(Strip): return str(time.Hour) def start(self, time): - start_time = wx.DateTimeFromDMY(time.Day, time.Month, time.Year, time.Hour) + start_time = wx.DateTimeFromDMY(time.Day, time.Month, time.Year, time.Hour) return start_time def increment(self, time): @@ -642,7 +638,7 @@ def microseconds_to_delta(microsecs): counter += 1 delta = wx.TimeSpan.Milliseconds(milliseconds) while counter > 0: - delta = delta * 2; + delta = delta * 2; counter -= 1 return delta @@ -657,7 +653,7 @@ def delta_to_microseconds(delta): neg = False if days < 0: neg = True - days = -days + days = -days hours = -hours minutes = -minutes seconds = -seconds @@ -670,9 +666,9 @@ def delta_to_microseconds(delta): if seconds >= 0: microsecs = seconds * US_PER_SEC if milliseconds >= 0: - microsecs = milliseconds * 1000 + microsecs = milliseconds * 1000 if neg: - microsecs = -microsecs + microsecs = -microsecs return microsecs diff --git a/timelinelib/view/drawingarea.py b/timelinelib/view/drawingarea.py index 78e473e..f1b5426 100644 --- a/timelinelib/view/drawingarea.py +++ b/timelinelib/view/drawingarea.py @@ -18,11 +18,11 @@ import wx -from timelinelib.db.interface import STATE_CHANGE_ANY -from timelinelib.db.interface import STATE_CHANGE_CATEGORY -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db.objects import TimeOutOfRangeLeftError from timelinelib.db.objects import TimeOutOfRangeRightError +from timelinelib.db.observer import STATE_CHANGE_ANY +from timelinelib.db.observer import STATE_CHANGE_CATEGORY from timelinelib.drawing.viewproperties import ViewProperties from timelinelib.utils import ex_msg from timelinelib.view.move import MoveByDragInputHandler @@ -34,8 +34,8 @@ from timelinelib.view.zoom import ZoomByDragInputHandler # The width in pixels of the vertical scroll zones. -# When the mouse reaches the any of the two scroll zone areas, scrolling -# of the timeline will take place if there is an ongoing selection of the +# When the mouse reaches the any of the two scroll zone areas, scrolling +# of the timeline will take place if there is an ongoing selection of the # timeline. The scroll zone areas are found at the beginning and at the # end of the timeline. SCROLL_ZONE_WIDTH = 20 @@ -184,7 +184,7 @@ class DrawingArea(object): """ Event handler used when the right mouse button has been pressed. - If the mouse hits an event and the timeline is not readonly, the + If the mouse hits an event and the timeline is not readonly, the context menu for that event is displayed. """ if self.timeline.is_read_only(): @@ -218,13 +218,13 @@ class DrawingArea(object): if len(selected_event_ids) > 0: id = selected_event_ids[0] return self.timeline.find_event_with_id(id) - return None + return None def _context_menu_on_edit_event(self, evt): - self.view.edit_event(self.context_menu_event) + self.view.open_event_editor_for(self.context_menu_event) def _context_menu_on_duplicate_event(self, evt): - self.view.duplicate_event(self.context_menu_event) + self.view.open_duplicate_event_dialog_for_event(self.context_menu_event) def _context_menu_on_delete_event(self, evt): self.context_menu_event.selected = True @@ -239,7 +239,7 @@ class DrawingArea(object): Event handler used when the left mouse button has been double clicked. If the timeline is readonly, no action is taken. - If the mouse hits an event, a dialog opens for editing this event. + If the mouse hits an event, a dialog opens for editing this event. Otherwise a dialog for creating a new event is opened. """ if self.timeline.is_read_only(): @@ -252,10 +252,10 @@ class DrawingArea(object): self._toggle_event_selection(x, y, ctrl_down, alt_down) event = self.drawing_algorithm.event_at(x, y, alt_down) if event: - self.view.edit_event(event) + self.view.open_event_editor_for(event) else: current_time = self.get_time(x) - self.view.create_new_event(current_time, current_time) + self.view.open_create_event_editor(current_time, current_time) def get_time(self, x): return self.drawing_algorithm.get_time(x) @@ -290,9 +290,9 @@ class DrawingArea(object): Mouse event handler, when the mouse is entering the window. If there is an ongoing selection-marking (dragscroll timer running) - and the left mouse button is not down when we enter the window, we - want to simulate a 'mouse left up'-event, so that the dialog for - creating an event will be opened or sizing, moving stops. + and the left mouse button is not down when we enter the window, we + want to simulate a 'mouse left up'-event, so that the dialog for + creating an event will be opened or sizing, moving stops. """ if self.dragscroll_timer_running: if not left_is_down: @@ -301,10 +301,10 @@ class DrawingArea(object): def mouse_moved(self, x, y, alt_down=False): self.input_handler.mouse_moved(x, y, alt_down) - def mouse_wheel_moved(self, rotation, ctrl_down, shift_down): + def mouse_wheel_moved(self, rotation, ctrl_down, shift_down, x): direction = _step_function(rotation) if ctrl_down: - self._zoom_timeline(direction) + self._zoom_timeline(direction, x) elif shift_down: self.divider_line_slider.SetValue(self.divider_line_slider.GetValue() + direction) self._redraw_timeline() @@ -327,15 +327,15 @@ class DrawingArea(object): def _move_event_vertically(self, up=True): if self._one_and_only_one_event_selected(): selected_event = self._get_first_selected_event() - (overlapping_event, direction) = self.drawing_algorithm.get_closest_overlapping_event(selected_event, - up=up) + (overlapping_event, direction) = self.drawing_algorithm.get_closest_overlapping_event(selected_event, + up=up) if overlapping_event is None: return if direction > 0: - self.timeline.place_event_after_event(selected_event, + self.timeline.place_event_after_event(selected_event, overlapping_event) else: - self.timeline.place_event_before_event(selected_event, + self.timeline.place_event_before_event(selected_event, overlapping_event) self._redraw_timeline() @@ -362,7 +362,7 @@ class DrawingArea(object): self._redraw_timeline() def _timeline_changed(self, state_change): - if (state_change == STATE_CHANGE_ANY or + if (state_change == STATE_CHANGE_ANY or state_change == STATE_CHANGE_CATEGORY): self._redraw_timeline() @@ -448,21 +448,24 @@ class DrawingArea(object): def _scroll_timeline_view_by_factor(self, factor): time_period = self.view_properties.displayed_period - delta = self.time_type.mult_timedelta(time_period.delta(), factor) + delta = self.time_type.mult_timedelta(time_period.delta(), factor) self._scroll_timeline(delta) def _scroll_timeline(self, delta): self.navigate_timeline(lambda tp: tp.move_delta(-delta)) - def _zoom_timeline(self, direction=0): - self.navigate_timeline(lambda tp: tp.zoom(direction)) + def _zoom_timeline(self, direction, x): + """ zoom time line at position x """ + width, height = self.view.GetSizeTuple() + x_percent_of_width=float(x)/width + self.navigate_timeline(lambda tp: tp.zoom(direction, x_percent_of_width)) def _delete_selected_events(self): """After acknowledge from the user, delete all selected events.""" selected_event_ids = self.view_properties.get_selected_event_ids() nbr_of_selected_event_ids = len(selected_event_ids) if nbr_of_selected_event_ids > 1: - text = _("Are you sure you want to delete %d events?" % + text = _("Are you sure you want to delete %d events?" % nbr_of_selected_event_ids) else: text = _("Are you sure you want to delete this event?") @@ -475,7 +478,7 @@ class DrawingArea(object): def balloon_visibility_changed(self, visible): self.view_properties.show_balloons_on_hover = visible - # When display on hovering is disabled we have to make sure + # When display on hovering is disabled we have to make sure # that any visible balloon is removed. # TODO: Do we really need that? if not visible: diff --git a/timelinelib/view/move.py b/timelinelib/view/move.py index 09eb67d..811dc7f 100644 --- a/timelinelib/view/move.py +++ b/timelinelib/view/move.py @@ -33,7 +33,7 @@ class MoveByDragInputHandler(ScrollViewInputHandler): selected_events = self.drawing_area.get_selected_events() if not event_being_dragged in selected_events: return - for event in selected_events: + for event in selected_events: period_pair = (event, event.time_period) if event == event_being_dragged: self.event_periods.insert(0, period_pair) diff --git a/timelinelib/view/periodbase.py b/timelinelib/view/periodbase.py index 011b658..8b530f3 100644 --- a/timelinelib/view/periodbase.py +++ b/timelinelib/view/periodbase.py @@ -60,7 +60,7 @@ class SelectPeriodByDragInputHandler(ScrollViewInputHandler): start = t1 end = t2 return TimePeriod( - self.controller.get_timeline().get_time_type(), + self.controller.get_timeline().get_time_type(), self.controller.get_drawer().snap(start), self.controller.get_drawer().snap(end)) diff --git a/timelinelib/view/periodevent.py b/timelinelib/view/periodevent.py index 41edd44..9cab5ca 100644 --- a/timelinelib/view/periodevent.py +++ b/timelinelib/view/periodevent.py @@ -27,4 +27,4 @@ class CreatePeriodEventByDragInputHandler(SelectPeriodByDragInputHandler): def end_action(self): period = self.get_last_valid_period() - self.view.create_new_event(period.start_time, period.end_time) + self.view.open_create_event_editor(period.start_time, period.end_time) diff --git a/timelinelib/view/resize.py b/timelinelib/view/resize.py index 9ac7412..ea215b7 100644 --- a/timelinelib/view/resize.py +++ b/timelinelib/view/resize.py @@ -71,6 +71,6 @@ class ResizeByDragInputHandler(ScrollViewInputHandler): def _adjust_container_edges(self): self.event.strategy._set_time_period() - + def _clear_status_text(self): self.status_bar.set_text("") diff --git a/timelinelib/view/scrolldrag.py b/timelinelib/view/scrolldrag.py index 0b8bd62..c698d37 100644 --- a/timelinelib/view/scrolldrag.py +++ b/timelinelib/view/scrolldrag.py @@ -51,7 +51,7 @@ class ScrollByDragInputHandler(InputHandler): if elapsed_clock_time == 0: self.speed_px_per_sec = MAX_SPEED else: - self.speed_px_per_sec = min(MAX_SPEED, abs(self.last_x_distance / + self.speed_px_per_sec = min(MAX_SPEED, abs(self.last_x_distance / elapsed_clock_time)) self.last_clock_time = current_clock_time @@ -62,7 +62,7 @@ class ScrollByDragInputHandler(InputHandler): def _inertial_scrolling(self): frame_time = self._calculate_frame_time() - value_factor = self._calculate_scroll_factor() + value_factor = self._calculate_scroll_factor() inertial_func = (0.20, 0.15, 0.10, 0.10, 0.10, 0.08, 0.06, 0.06, 0.05) #inertial_func = (0.20, 0.15, 0.10, 0.10, 0.07, 0.05, 0.02, 0.05) self.controller.use_fast_draw(True) @@ -78,7 +78,7 @@ class ScrollByDragInputHandler(InputHandler): def _calculate_frame_time(self): MAX_FRAME_RATE = 26.0 - frames_per_second = (MAX_FRAME_RATE * self.speed_px_per_sec / + frames_per_second = (MAX_FRAME_RATE * self.speed_px_per_sec / (100 + self.speed_px_per_sec)) frame_time = 1.0 / frames_per_second return frame_time @@ -88,6 +88,6 @@ class ScrollByDragInputHandler(InputHandler): direction = 1 else: direction = -1 - scroll_factor = (direction * self.speed_px_per_sec / + scroll_factor = (direction * self.speed_px_per_sec / self.INERTIAL_SCROLLING_SPEED_THRESHOLD) - return scroll_factor + return scroll_factor diff --git a/timelinelib/wxgui/component.py b/timelinelib/wxgui/component.py new file mode 100644 index 0000000..410996c --- /dev/null +++ b/timelinelib/wxgui/component.py @@ -0,0 +1,86 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +import wx + +from timelinelib.db import db_open +from timelinelib.wxgui.dialogs.mainframe import TimelinePanel + + +class DummyConfig(object): + + def __init__(self): + self.window_size = (100, 100) + self.window_pos = (100, 100) + self.window_maximized = False + self.show_sidebar = True + self.show_legend = True + self.sidebar_width = 200 + self.recently_opened = [] + self.open_recent_at_startup = False + self.balloon_on_hover = True + self.week_start = "monaday" + self.use_wide_date_range = False + self.use_inertial_scrolling = False + + def get_sidebar_width(self): + return self.sidebar_width + + def get_show_sidebar(self): + return self.show_sidebar + + def get_show_legend(self): + return self.show_legend + + def get_balloon_on_hover(self): + return self.balloon_on_hover + + +class DummyStatusBarAdapter(object): + + def set_text(self, text): + pass + + def set_hidden_event_count_text(self, text): + pass + + def set_read_only_text(self, text): + pass + + +class DummyMainFrame(object): + + def enable_disable_menus(self): + pass + + +class TimelineComponent(TimelinePanel): + + def __init__(self, parent): + TimelinePanel.__init__( + self, parent, DummyConfig(), self.handle_db_error, + DummyStatusBarAdapter(), DummyMainFrame()) + self.activated() + + def handle_db_error(self, e): + pass + + def open_timeline(self, path): + timeline = db_open(path) + self.drawing_area.set_timeline(timeline) + self.sidebar.cattree.initialize_from_timeline_view(self.drawing_area) diff --git a/timelinelib/wxgui/components/__init__.py b/timelinelib/wxgui/components/__init__.py index a8a5fad..e69de29 100644 --- a/timelinelib/wxgui/components/__init__.py +++ b/timelinelib/wxgui/components/__init__.py @@ -1,23 +0,0 @@ -# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg -# -# This file is part of Timeline. -# -# Timeline 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 3 of the License, or -# (at your option) any later version. -# -# Timeline 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 Timeline. If not, see . - - -""" -All GUI components that can be used in multiple places in the GUI. - -One component per file. -""" diff --git a/timelinelib/wxgui/components/categorychoice.py b/timelinelib/wxgui/components/categorychoice.py index 1b00362..6901719 100644 --- a/timelinelib/wxgui/components/categorychoice.py +++ b/timelinelib/wxgui/components/categorychoice.py @@ -18,11 +18,11 @@ import wx -from timelinelib.wxgui.dialogs.categoryeditor import WxCategoryEdtiorDialog +from timelinelib.db.exceptions import TimelineIOError +from timelinelib.db.objects.category import sort_categories from timelinelib.wxgui.dialogs.categorieseditor import CategoriesEditor +from timelinelib.wxgui.dialogs.categoryeditor import WxCategoryEdtiorDialog import timelinelib.wxgui.utils as gui_utils -from timelinelib.db.interface import TimelineIOError -from timelinelib.domain.category import sort_categories class CategoryChoice(wx.Choice): @@ -59,7 +59,7 @@ class CategoryChoice(wx.Choice): selection = self.GetSelection() category = self.GetClientData(selection) return category - + def on_choice(self, e): new_selection_index = e.GetSelection() if new_selection_index > self.last_real_category_index: @@ -73,7 +73,7 @@ class CategoryChoice(wx.Choice): def _add_category(self): def create_category_editor(): - return WxCategoryEdtiorDialog(self, _("Add Category"), + return WxCategoryEdtiorDialog(self, _("Add Category"), self.timeline, None) def handle_success(dialog): if dialog.GetReturnCode() == wx.ID_OK: @@ -97,4 +97,4 @@ class CategoryChoice(wx.Choice): gui_utils.handle_db_error_in_dialog(self, e) gui_utils.show_modal(create_categories_editor, gui_utils.create_dialog_db_error_handler(self), - handle_success) + handle_success) diff --git a/timelinelib/wxgui/components/cattree.py b/timelinelib/wxgui/components/cattree.py index dd7955c..f5b812a 100644 --- a/timelinelib/wxgui/components/cattree.py +++ b/timelinelib/wxgui/components/cattree.py @@ -19,8 +19,8 @@ import wx import wx.lib.agw.customtreectrl as customtreectrl -from timelinelib.db.interface import STATE_CHANGE_CATEGORY -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError +from timelinelib.db.observer import STATE_CHANGE_CATEGORY from timelinelib.wxgui.dialogs.categoryeditor import WxCategoryEdtiorDialog from timelinelib.wxgui.utils import _ask_question from timelinelib.wxgui.utils import category_tree @@ -231,8 +231,8 @@ def delete_category(parent_ctrl, db, cat, fn_handle_db_error): update_warning = _("Events belonging to '%s' will no longer " "belong to a category.") % cat.name else: - update_warning = _("Events belonging to '%(name)s' will now belong to '%(parent)s'.") \ - % {'name': cat.name, 'parent': cat.parent.name} + update_warning = _("Events belonging to '%(name)s' will now belong to '%(parent)s'.") \ + % {'name': cat.name, 'parent': cat.parent.name} question = "%s\n\n%s" % (delete_warning, update_warning) if _ask_question(question, parent_ctrl) == wx.YES: try: diff --git a/timelinelib/wxgui/components/numtimepicker.py b/timelinelib/wxgui/components/numtimepicker.py index 2724895..ed3568e 100644 --- a/timelinelib/wxgui/components/numtimepicker.py +++ b/timelinelib/wxgui/components/numtimepicker.py @@ -16,20 +16,15 @@ # along with Timeline. If not, see . -import os.path -import re - import wx -from timelinelib.time import NumTimeType - class NumTimePicker(wx.Panel): def __init__(self, parent, show_time=False, config=None): wx.Panel.__init__(self, parent) self.time_picker = self._create_gui() - self.controller = NumTimePickerController(self, 0) + self.controller = NumTimePickerController(self, 0) def get_value(self): return self.time_picker.GetValue() @@ -59,7 +54,6 @@ class NumTimePicker(wx.Panel): class NumTimePickerController(object): def __init__(self, time_picker, default_num_time): - import sys self.time_picker = time_picker self.time_picker.set_range(-10000, 10000) self.default_num_time = default_num_time diff --git a/timelinelib/wxgui/components/pydatetimepicker.py b/timelinelib/wxgui/components/pydatetimepicker.py index df7c735..78a1656 100644 --- a/timelinelib/wxgui/components/pydatetimepicker.py +++ b/timelinelib/wxgui/components/pydatetimepicker.py @@ -131,14 +131,14 @@ class CalendarPopup(wx.PopupTransientWindow): def _create_calendar_control(self, wx_date, border): style = self._get_cal_style() - cal = wx.calendar.CalendarCtrl(self, -1, wx_date, + cal = wx.calendar.CalendarCtrl(self, -1, wx_date, pos=(border,border), style=style) self._set_cal_range(cal) return cal def _get_cal_style(self): - style = (wx.calendar.CAL_SHOW_HOLIDAYS | - wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION) + style = (wx.calendar.CAL_SHOW_HOLIDAYS | + wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION) if self.config.week_start == "monday": style |= wx.calendar.CAL_MONDAY_FIRST else: @@ -186,7 +186,7 @@ class CalendarPopupController(object): # month or day. The control is closed on a double-click on a day or # a single click outside of the control if self.repop and not self.repoped: - self.calendar_popup.Popup() + self.calendar_popup.Popup() self.repoped = True @@ -241,8 +241,8 @@ class PyDatePicker(wx.TextCtrl): elif evt.GetKeyCode() == wx.WXK_DOWN: self.controller.on_down() else: - evt.Skip() - self.Bind(wx.EVT_KEY_DOWN, on_key_down) + evt.Skip() + self.Bind(wx.EVT_KEY_DOWN, on_key_down) def _resize_to_fit_text(self): w, h = self.GetTextExtent("0000-00-00") @@ -285,7 +285,7 @@ class PyDatePickerController(object): self.py_date_picker.SetSelection(start, end) else: self._select_region_if_possible(self.region_year) - self.last_selection = self.py_date_picker.GetSelection() + self.last_selection = self.py_date_picker.GetSelection() def on_kill_focus(self): if self.last_selection: @@ -312,8 +312,8 @@ class PyDatePickerController(object): # To prevent saving of preferred day when year or month is changed # in on_up() and on_down()... # Save preferred day only when text is entered in the date text - # control and not when up or down keys has been used. - # When up and down keys are used, the preferred day is saved in + # control and not when up or down keys has been used. + # When up and down keys are used, the preferred day is saved in # on_up() and on_down() only when day is changed. if self.save_preferred_day: self._save_preferred_day(current_date) @@ -325,9 +325,9 @@ class PyDatePickerController(object): return date def increment_month(date): if date.month < 12: - return self._set_valid_day(date.year, date.month + 1, + return self._set_valid_day(date.year, date.month + 1, date.day) - elif date.year < PyTimeType().get_max_time()[0].year - 1: + elif date.year < PyTimeType().get_max_time()[0].year - 1: return self._set_valid_day(date.year + 1, 1, date.day) return date def increment_day(date): @@ -345,8 +345,8 @@ class PyDatePickerController(object): else: new_date = increment_day(current_date) self._save_preferred_day(new_date) - if current_date != new_date: - self._set_new_date_and_restore_selection(new_date, selection) + if current_date != new_date: + self._set_new_date_and_restore_selection(new_date, selection) def on_down(self): def decrement_year(date): @@ -356,7 +356,7 @@ class PyDatePickerController(object): def decrement_month(date): if date.month > 1: return self._set_valid_day(date.year, date.month - 1, date.day) - elif date.year > PyTimeType().get_min_time()[0].year: + elif date.year > PyTimeType().get_min_time()[0].year: return self._set_valid_day(date.year - 1, 12, date.day) return date def decrement_day(date): @@ -378,8 +378,8 @@ class PyDatePickerController(object): else: new_date = decrement_day(current_date) self._save_preferred_day(new_date) - if current_date != new_date: - self._set_new_date_and_restore_selection(new_date, selection) + if current_date != new_date: + self._set_new_date_and_restore_selection(new_date, selection) def _change_background_depending_on_date_validity(self): if self._current_date_is_valid(): @@ -409,7 +409,7 @@ class PyDatePickerController(object): self.py_date_picker.SetSelection(selection[0], selection[1]) self.save_preferred_day = False if self.preferred_day != None: - new_date = self._set_valid_day(new_date.year, new_date.month, + new_date = self._set_valid_day(new_date.year, new_date.month, self.preferred_day) self.set_py_date(new_date) restore_selection(selection) @@ -421,7 +421,7 @@ class PyDatePickerController(object): try: date = datetime.date(year=new_year, month=new_month, day=new_day) done = True - except Exception, ex: + except Exception, ex: new_day -= 1 return date @@ -449,16 +449,16 @@ class PyDatePickerController(object): return self.py_date_picker.GetInsertionPoint() in region_range def _get_region_range(self, n): - # Returns a range of valid cursor positions for a valid region year, + # Returns a range of valid cursor positions for a valid region year, # month or day. def region_is_not_valid(region): - return region not in (self.region_year, self.region_month, + return region not in (self.region_year, self.region_month, self.region_day) def date_has_exactly_two_seperators(datestring): return len(datestring.split(self.separator)) == 3 def calculate_pos_range(region, datestring): pos_of_separator1 = datestring.find(self.separator) - pos_of_separator2 = datestring.find(self.separator, + pos_of_separator2 = datestring.find(self.separator, pos_of_separator1 + 1) if region == self.region_year: return range(0, pos_of_separator1 + 1) @@ -526,8 +526,8 @@ class PyTimePicker(wx.TextCtrl): elif evt.GetKeyCode() == wx.WXK_DOWN: self.controller.on_down() else: - evt.Skip() - self.Bind(wx.EVT_KEY_DOWN, on_key_down) + evt.Skip() + self.Bind(wx.EVT_KEY_DOWN, on_key_down) def _resize_to_fit_text(self): w, h = self.GetTextExtent("00:00") @@ -600,7 +600,7 @@ class PyTimePickerController(object): def increment_minutes(time): new_hour = time.hour new_minute = time.minute + 1 - if new_minute > 59: + if new_minute > 59: new_minute = 0 new_hour = time.hour + 1 if new_hour > 23: @@ -614,8 +614,8 @@ class PyTimePickerController(object): new_time = increment_hour(current_time) else: new_time = increment_minutes(current_time) - if current_time != new_time: - self._set_new_time_and_restore_selection(new_time, selection) + if current_time != new_time: + self._set_new_time_and_restore_selection(new_time, selection) def on_down(self): def decrement_hour(time): @@ -626,7 +626,7 @@ class PyTimePickerController(object): def decrement_minutes(time): new_hour = time.hour new_minute = time.minute - 1 - if new_minute < 0: + if new_minute < 0: new_minute = 59 new_hour = time.hour - 1 if new_hour < 0: @@ -640,8 +640,8 @@ class PyTimePickerController(object): new_time = decrement_hour(current_time) else: new_time = decrement_minutes(current_time) - if current_time != new_time: - self._set_new_time_and_restore_selection(new_time, selection) + if current_time != new_time: + self._set_new_time_and_restore_selection(new_time, selection) def _set_new_time_and_restore_selection(self, new_time, selection): def restore_selection(selection): @@ -661,7 +661,7 @@ class PyTimePickerController(object): return if part == self.hour_part: self.py_time_picker.SetSelection(0, self._separator_pos()) - else: + else: time_string_len = len(self.py_time_picker.get_time_string()) self.py_time_picker.SetSelection(self._separator_pos() + 1, time_string_len) self.preferred_part = part diff --git a/timelinelib/wxgui/components/timelineview.py b/timelinelib/wxgui/components/timelineview.py index a928884..7879921 100644 --- a/timelinelib/wxgui/components/timelineview.py +++ b/timelinelib/wxgui/components/timelineview.py @@ -20,14 +20,20 @@ import wx from timelinelib.drawing import get_drawer from timelinelib.view.drawingarea import DrawingArea +from timelinelib.wxgui.dialogs.duplicateevent import open_duplicate_event_dialog_for_event +from timelinelib.wxgui.dialogs.eventeditor import open_create_event_editor +from timelinelib.wxgui.dialogs.eventeditor import open_event_editor_for from timelinelib.wxgui.utils import _ask_question class DrawingAreaPanel(wx.Panel): def __init__(self, parent, status_bar_adapter, divider_line_slider, - fn_handle_db_error, config): + fn_handle_db_error, config, main_frame): wx.Panel.__init__(self, parent, style=wx.NO_BORDER) + self.fn_handle_db_error = fn_handle_db_error + self.config = config + self.main_frame = main_frame self.controller = DrawingArea( self, status_bar_adapter, config, get_drawer(), divider_line_slider, fn_handle_db_error) @@ -79,16 +85,31 @@ class DrawingAreaPanel(wx.Panel): self.Update() def enable_disable_menus(self): - wx.GetTopLevelParent(self).enable_disable_menus() - - def edit_event(self, event): - wx.GetTopLevelParent(self).edit_event(event) - - def duplicate_event(self, event): - wx.GetTopLevelParent(self).duplicate_event(event) - - def create_new_event(self, start_time, end_time): - wx.GetTopLevelParent(self).create_new_event(start_time, end_time) + self.main_frame.enable_disable_menus() + + def open_event_editor_for(self, event): + open_event_editor_for( + self, + self.config, + self.controller.get_timeline(), + self.fn_handle_db_error, + event) + + def open_duplicate_event_dialog_for_event(self, event): + open_duplicate_event_dialog_for_event( + self, + self.controller.get_timeline(), + self.fn_handle_db_error, + event) + + def open_create_event_editor(self, start_time, end_time): + open_create_event_editor( + self, + self.config, + self.controller.get_timeline(), + self.fn_handle_db_error, + start_time, + end_time) def start_balloon_show_timer(self, milliseconds=-1, oneShot=False): self.balloon_show_timer.Start(milliseconds, oneShot) @@ -164,7 +185,7 @@ class DrawingAreaPanel(wx.Panel): self.controller.window_resized() def _on_left_down(self, evt): - self.controller.left_mouse_down(evt.m_x, evt.m_y, evt.m_controlDown, + self.controller.left_mouse_down(evt.m_x, evt.m_y, evt.m_controlDown, evt.m_shiftDown, evt.m_altDown) evt.Skip() @@ -172,7 +193,7 @@ class DrawingAreaPanel(wx.Panel): self.controller.right_mouse_down(evt.m_x, evt.m_y, evt.m_altDown) def _on_left_dclick(self, evt): - self.controller.left_mouse_dclick(evt.m_x, evt.m_y, evt.m_controlDown, + self.controller.left_mouse_dclick(evt.m_x, evt.m_y, evt.m_controlDown, evt.m_altDown) def _on_middle_up(self, evt): @@ -188,7 +209,7 @@ class DrawingAreaPanel(wx.Panel): self.controller.mouse_moved(evt.m_x, evt.m_y, evt.m_altDown) def _on_mousewheel(self, evt): - self.controller.mouse_wheel_moved(evt.m_wheelRotation, evt.ControlDown(), evt.ShiftDown()) + self.controller.mouse_wheel_moved(evt.m_wheelRotation, evt.ControlDown(), evt.ShiftDown(), evt.GetX()) def _on_key_down(self, evt): self.controller.key_down(evt.GetKeyCode(), evt.AltDown()) diff --git a/timelinelib/wxgui/components/wxdatetimepicker.py b/timelinelib/wxgui/components/wxdatetimepicker.py index 1d3e541..1267701 100644 --- a/timelinelib/wxgui/components/wxdatetimepicker.py +++ b/timelinelib/wxgui/components/wxdatetimepicker.py @@ -72,7 +72,7 @@ class WxDateTimePicker(wx.Panel): self._position_calendar_popup(evt) self.calendar_popup.Popup() except: - _display_error_message(_("Invalid date")) + _display_error_message(_("Invalid date")) def _create_calendar_popup(self): wx_date = self.controller.get_value() @@ -128,14 +128,14 @@ class CalendarPopup(wx.PopupTransientWindow): wx.PopupTransientWindow.__init__(self, parent, style=wx.BORDER_NONE) border = 2 style = self._get_cal_style() - self.cal = wx.calendar.CalendarCtrl(self, -1, wx_date, + self.cal = wx.calendar.CalendarCtrl(self, -1, wx_date, pos=(border,border), style=style) self._set_cal_range() self._set_size(border) self._bind_events() def _get_cal_style(self): - style = (wx.calendar.CAL_SHOW_HOLIDAYS | + style = (wx.calendar.CAL_SHOW_HOLIDAYS | wx.calendar.CAL_SEQUENTIAL_MONTH_SELECTION) if self.config.week_start == "monday": style |= wx.calendar.CAL_MONDAY_FIRST @@ -183,7 +183,7 @@ class CalendarPopupController(object): # month or day. The control is closed on a double-click on a day or # a single click outside of the control if self.repop and not self.repoped: - self.calendar_popup.Popup() + self.calendar_popup.Popup() self.repoped = True @@ -212,7 +212,7 @@ class WxDatePicker(wx.TextCtrl): self.Bind(wx.EVT_KILL_FOCUS, self._on_kill_focus) self.Bind(wx.EVT_CHAR, self._on_char) self.Bind(wx.EVT_TEXT, self._on_text) - self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) + self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) def _on_set_focus(self, evt): # CallAfter is a trick to prevent default behavior of selecting all @@ -244,7 +244,7 @@ class WxDatePicker(wx.TextCtrl): elif evt.GetKeyCode() == wx.WXK_DOWN: self.controller.on_down() else: - evt.Skip() + evt.Skip() def _resize_to_fit_text(self): w, h = self.GetTextExtent("0000-00-00") @@ -287,7 +287,7 @@ class WxDatePickerController(object): self.wx_date_picker.SetSelection(start, end) else: self._select_region_if_possible(self.region_year) - self.last_selection = self.wx_date_picker.GetSelection() + self.last_selection = self.wx_date_picker.GetSelection() def on_kill_focus(self): if self.last_selection: @@ -314,8 +314,8 @@ class WxDatePickerController(object): # To prevent saving of preferred day when year or month is changed # in on_up() and on_down()... # Save preferred day only when text is entered in the date text - # control and not when up or down keys has been used. - # When up and down keys are used, the preferred day is saved in + # control and not when up or down keys has been used. + # When up and down keys are used, the preferred day is saved in # on_up() and on_down() only when day is changed. if self.save_preferred_day: self._save_preferred_day(current_date) @@ -330,7 +330,7 @@ class WxDatePickerController(object): else: new_date = self._increment_day() self._save_preferred_day(new_date) - self._set_new_date_and_restore_selection(new_date) + self._set_new_date_and_restore_selection(new_date) def on_down(self): if not self._current_date_is_valid(): @@ -342,7 +342,7 @@ class WxDatePickerController(object): else: new_date = self._decrement_day() self._save_preferred_day(new_date) - self._set_new_date_and_restore_selection(new_date) + self._set_new_date_and_restore_selection(new_date) def _increment_year(self): date = self.get_date() @@ -354,7 +354,7 @@ class WxDatePickerController(object): date = self.get_date() if date.Month < 11: date = self._set_valid_day(date.Year, date.Month + 1, date.Day) - elif date.Year < WxTimeType().get_max_time()[0].Year - 1: + elif date.Year < WxTimeType().get_max_time()[0].Year - 1: date = self._set_valid_day(date.Year + 1, 0, date.Day) return date @@ -374,7 +374,7 @@ class WxDatePickerController(object): date = self.get_date() if date.Month > 0: date = self._set_valid_day(date.Year, date.Month - 1, date.Day) - elif date.Year > WxTimeType().get_min_time()[0].Year: + elif date.Year > WxTimeType().get_min_time()[0].Year: date = self._set_valid_day(date.Year - 1, 11, date.Day) return date @@ -399,10 +399,10 @@ class WxDatePickerController(object): def _parse_year_month_day(self): date_string = self.wx_date_picker.get_date_string() - date_bc = False + date_bc = False if (date_string[0:1] == self.separator): date_string = date_string[1:] - date_bc = True + date_bc = True components = date_string.split(self.separator) if len(components) != 3: raise ValueError() @@ -424,7 +424,7 @@ class WxDatePickerController(object): selection = self.wx_date_picker.GetSelection() self.save_preferred_day = False if self.preferred_day != None: - new_date = self._set_valid_day(new_date.Year, new_date.Month, + new_date = self._set_valid_day(new_date.Year, new_date.Month, self.preferred_day) self.set_date(new_date) restore_selection(selection) @@ -434,7 +434,7 @@ class WxDatePickerController(object): while True: try: return try_to_create_wx_date_time_from_dmy(new_day, new_month, new_year) - except ValueError: + except ValueError: new_day -= 1 def _save_preferred_day(self, date): @@ -462,16 +462,16 @@ class WxDatePickerController(object): return self.wx_date_picker.GetInsertionPoint() in region_range def _get_region_range(self, n): - # Returns a range of valid cursor positions for a valid region year, + # Returns a range of valid cursor positions for a valid region year, # month or day. def region_is_not_valid(region): - return region not in (self.region_year, self.region_month, + return region not in (self.region_year, self.region_month, self.region_day) def date_has_exactly_two_seperators(datestring): return len(datestring.split(self.separator)) == 3 def calculate_pos_range(region, datestring): pos_of_separator1 = datestring.find(self.separator) - pos_of_separator2 = datestring.find(self.separator, + pos_of_separator2 = datestring.find(self.separator, pos_of_separator1 + 1) if region == self.region_year: return range(0, pos_of_separator1 + 1) @@ -513,7 +513,7 @@ class WxTimePicker(wx.TextCtrl): self.Bind(wx.EVT_KILL_FOCUS, self._on_kill_focus) self.Bind(wx.EVT_CHAR, self._on_char) self.Bind(wx.EVT_TEXT, self._on_text) - self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) + self.Bind(wx.EVT_KEY_DOWN, self._on_key_down) def _on_set_focus(self, evt): # CallAfter is a trick to prevent default behavior of selecting all @@ -545,7 +545,7 @@ class WxTimePicker(wx.TextCtrl): elif evt.GetKeyCode() == wx.WXK_DOWN: self.controller.on_down() else: - evt.Skip() + evt.Skip() def _resize_to_fit_text(self): w, h = self.GetTextExtent("00:00") @@ -616,7 +616,7 @@ class WxTimePickerController(object): new_time = self._increment_hour() else: new_time = self._increment_minutes() - self._set_new_time_and_restore_selection(new_time) + self._set_new_time_and_restore_selection(new_time) def on_down(self): if not self._time_is_valid(): @@ -625,7 +625,7 @@ class WxTimePickerController(object): new_time = self._decrement_hour() else: new_time = self._decrement_minutes() - self._set_new_time_and_restore_selection(new_time) + self._set_new_time_and_restore_selection(new_time) def _increment_hour(self): time = self.get_time() @@ -639,13 +639,13 @@ class WxTimePickerController(object): time = self.get_time() new_hour = time.Hour new_minute = time.Minute + 1 - if new_minute > 59: + if new_minute > 59: new_minute = 0 new_hour = time.Hour + 1 if new_hour > 23: new_hour = 0 time.SetHour(new_hour) - time.SetMinute(new_minute) + time.SetMinute(new_minute) return time def _decrement_hour(self): @@ -660,7 +660,7 @@ class WxTimePickerController(object): time = self.get_time() new_hour = time.Hour new_minute = time.Minute - 1 - if new_minute < 0: + if new_minute < 0: new_minute = 59 new_hour = time.Hour - 1 if new_hour < 0: @@ -688,7 +688,7 @@ class WxTimePickerController(object): return if part == self.hour_part: self.wx_time_picker.SetSelection(0, self._separator_pos()) - else: + else: time_string_len = len(self.wx_time_picker.get_time_string()) self.wx_time_picker.SetSelection(self._separator_pos() + 1, time_string_len) self.preferred_part = part diff --git a/timelinelib/wxgui/dialogs/categorieseditor.py b/timelinelib/wxgui/dialogs/categorieseditor.py index af8980a..46a02d7 100644 --- a/timelinelib/wxgui/dialogs/categorieseditor.py +++ b/timelinelib/wxgui/dialogs/categorieseditor.py @@ -84,7 +84,7 @@ class CategoriesEditor(wx.Dialog): def _btn_edit_on_click(self, e): selected_category = self.cat_tree.get_selected_category() if selected_category is not None: - edit_category(self, self.db, selected_category, + edit_category(self, self.db, selected_category, self.db_error_handler) self._updateButtons() @@ -108,7 +108,7 @@ class CategoriesEditor(wx.Dialog): def _btn_del_on_click(self, e): selected_category = self.cat_tree.get_selected_category() if selected_category is not None: - delete_category(self, self.db, selected_category, + delete_category(self, self.db, selected_category, self.db_error_handler) self._updateButtons() diff --git a/timelinelib/wxgui/dialogs/containereditor.py b/timelinelib/wxgui/dialogs/containereditor.py index b78f58e..144604c 100644 --- a/timelinelib/wxgui/dialogs/containereditor.py +++ b/timelinelib/wxgui/dialogs/containereditor.py @@ -69,7 +69,7 @@ class StaticContainerEditorDialog(wx.Dialog): def _create_buttons(self, properties_box): button_box = self.CreateStdDialogButtonSizer(wx.OK|wx.CANCEL) properties_box.Add(button_box, flag=wx.EXPAND|wx.ALL, border=BORDER) - + class ContainerEditorControllerApi(object): @@ -79,19 +79,19 @@ class ContainerEditorControllerApi(object): def set_name(self, name): self.txt_name.SetValue(name) - + def get_name(self): return self.txt_name.GetValue().strip() def set_category(self, category): self.lst_category.select(category) - + def get_category(self): return self.lst_category.get() - + def display_invalid_name(self, message): _display_error_message(message, self) - _set_focus_and_select(self.txt_name) + _set_focus_and_select(self.txt_name) def display_db_exception(self, e): gui_utils.handle_db_error_in_dialog(self, e) @@ -102,12 +102,12 @@ class ContainerEditorControllerApi(object): def _bind_events(self): self.Bind(wx.EVT_BUTTON, self._btn_ok_on_click, id=wx.ID_OK) self.Bind(wx.EVT_CHOICE, self.lst_category.on_choice, self.lst_category) - + def _btn_ok_on_click(self, evt): - self.controller.save() - - -class ContainerEditorDialog(StaticContainerEditorDialog, + self.controller.save() + + +class ContainerEditorDialog(StaticContainerEditorDialog, ContainerEditorControllerApi): """ This dialog is used for two purposes, editing an existing container diff --git a/timelinelib/wxgui/dialogs/duplicateevent.py b/timelinelib/wxgui/dialogs/duplicateevent.py index fd5d11d..2480b00 100644 --- a/timelinelib/wxgui/dialogs/duplicateevent.py +++ b/timelinelib/wxgui/dialogs/duplicateevent.py @@ -18,10 +18,10 @@ import wx +from timelinelib.editors.duplicateevent import DuplicateEventEditor from timelinelib.wxgui.utils import BORDER -from timelinelib.wxgui.utils import _set_focus_and_select from timelinelib.wxgui.utils import _display_error_message -from timelinelib.editors.duplicateevent import DuplicateEventEditor +from timelinelib.wxgui.utils import _set_focus_and_select import timelinelib.wxgui.utils as gui_utils @@ -37,13 +37,13 @@ class DuplicateEventDialog(wx.Dialog): self.sc_count.SetValue(count) def get_count(self): - return self.sc_count.GetValue() + return self.sc_count.GetValue() def set_frequency(self, count): self.sc_frequency.SetValue(count) def get_frequency(self): - return self.sc_frequency.GetValue() + return self.sc_frequency.GetValue() def select_move_period_fn_at_index(self, index): self.rb_period.SetSelection(index) @@ -52,10 +52,10 @@ class DuplicateEventDialog(wx.Dialog): return self._move_period_fns[self.rb_period.GetSelection()] def set_direction(self, direction): - self.rb_direction.SetSelection(direction) + self.rb_direction.SetSelection(direction) def get_direction(self): - return self.rb_direction.GetSelection() + return self.rb_direction.GetSelection() def close(self): self.EndModal(wx.ID_OK) @@ -65,7 +65,7 @@ class DuplicateEventDialog(wx.Dialog): def handle_date_errors(self, error_count): _display_error_message( - _("%d Events not duplicated due to missing dates.") + _("%d Events not duplicated due to missing dates.") % error_count) def _create_gui(self, move_period_config): @@ -95,9 +95,9 @@ class DuplicateEventDialog(wx.Dialog): return hbox def _create_and_add_rb_period(self, form, period_list): - self.rb_period = wx.RadioBox(self, wx.ID_ANY, _("Period"), - wx.DefaultPosition, wx.DefaultSize, - period_list) + self.rb_period = wx.RadioBox(self, wx.ID_ANY, _("Period"), + wx.DefaultPosition, wx.DefaultSize, + period_list) form.Add(self.rb_period, flag=wx.ALL|wx.EXPAND, border=BORDER) def _create_and_add_sc_frequency_box(self, form): @@ -116,7 +116,7 @@ class DuplicateEventDialog(wx.Dialog): def _create_and_add_rb_direction(self, form): direction_list = [_("Forward"), _("Backward"), _("Both")] - self.rb_direction = wx.RadioBox(self, wx.ID_ANY, _("Direction"), + self.rb_direction = wx.RadioBox(self, wx.ID_ANY, _("Direction"), choices=direction_list) form.Add(self.rb_direction, flag=wx.ALL|wx.EXPAND, border=BORDER) @@ -129,3 +129,9 @@ class DuplicateEventDialog(wx.Dialog): gui_utils.set_wait_cursor(self) self.controller.create_duplicates_and_save() gui_utils.set_default_cursor(self) + + +def open_duplicate_event_dialog_for_event(parent, db, handle_db_error, event): + def create_dialog(): + return DuplicateEventDialog(parent, db, event) + gui_utils.show_modal(create_dialog, handle_db_error) diff --git a/timelinelib/wxgui/dialogs/eventeditor.py b/timelinelib/wxgui/dialogs/eventeditor.py index 37cc9a0..d2b2a53 100644 --- a/timelinelib/wxgui/dialogs/eventeditor.py +++ b/timelinelib/wxgui/dialogs/eventeditor.py @@ -20,16 +20,16 @@ import os.path import wx -from timelinelib.db.interface import TimelineIOError +from timelinelib.db.exceptions import TimelineIOError from timelinelib.editors.event import EventEditor -from timelinelib.wxgui.dialogs.containereditor import ContainerEditorDialog +from timelinelib.repositories.dbwrapper import DbWrapperEventRepository from timelinelib.wxgui.components.categorychoice import CategoryChoice +from timelinelib.wxgui.dialogs.containereditor import ContainerEditorDialog from timelinelib.wxgui.utils import BORDER from timelinelib.wxgui.utils import _display_error_message from timelinelib.wxgui.utils import _set_focus_and_select from timelinelib.wxgui.utils import time_picker_for import timelinelib.wxgui.utils as gui_utils -from timelinelib.repositories.dbwrapper import DbWrapperEventRepository class EventEditorDialog(wx.Dialog): @@ -63,12 +63,12 @@ class EventEditorDialog(wx.Dialog): main_box_content = wx.StaticBoxSizer(groupbox, wx.VERTICAL) self._create_detail_content(main_box_content) self._create_notebook_content(main_box_content) - sizer.Add(main_box_content, flag=wx.EXPAND|wx.ALL, + sizer.Add(main_box_content, flag=wx.EXPAND|wx.ALL, border=BORDER, proportion=1) def _create_detail_content(self, properties_box_content): details = self._create_details() - properties_box_content.Add(details, flag=wx.ALL|wx.EXPAND, + properties_box_content.Add(details, flag=wx.ALL|wx.EXPAND, border=BORDER) def _create_details(self): @@ -79,7 +79,7 @@ class EventEditorDialog(wx.Dialog): self._create_text_field(grid) self._create_categories_listbox(grid) self._create_container_listbox(grid) - return grid + return grid def _create_time_details(self, grid): grid.Add(wx.StaticText(self, label=_("When:")), @@ -123,7 +123,7 @@ class EventEditorDialog(wx.Dialog): label = wx.StaticText(self, label=_("Container:")) grid.Add(label, flag=wx.ALIGN_CENTER_VERTICAL) grid.Add(self.lst_containers) - self.Bind(wx.EVT_CHOICE, self._lst_containers_on_choice, + self.Bind(wx.EVT_CHOICE, self._lst_containers_on_choice, self.lst_containers) def _lst_containers_on_choice(self, e): @@ -137,14 +137,15 @@ class EventEditorDialog(wx.Dialog): else: self.current_container_selection = new_selection_index self._enable_disable_checkboxes() - + def _enable_disable_checkboxes(self): self._enable_disable_ends_today() self._enable_disable_locked() def _enable_disable_ends_today(self): - enable = (self._container_not_selected() and - not self.chb_locked.GetValue()) + enable = (self._container_not_selected() and + not self.chb_locked.GetValue() and + self.controller.start_is_in_history()) self.chb_ends_today.Enable(enable) def _enable_disable_locked(self): @@ -154,7 +155,7 @@ class EventEditorDialog(wx.Dialog): def _container_not_selected(self): index = self.lst_containers.GetSelection() return (index == 0) - + def _add_container(self): def create_container_editor(): return ContainerEditorDialog(self, _("Add Container"), self.timeline, None) @@ -167,7 +168,7 @@ class EventEditorDialog(wx.Dialog): gui_utils.show_modal(create_container_editor, gui_utils.create_dialog_db_error_handler(self), handle_success) - + def _create_period_checkbox(self, box): handler = self._chb_period_on_checkbox return self._create_chb(box, _("Period"), handler) @@ -220,7 +221,7 @@ class EventEditorDialog(wx.Dialog): def _create_notebook_content(self, properties_box_content): notebook = self._create_notebook() - properties_box_content.Add(notebook, border=BORDER, + properties_box_content.Add(notebook, border=BORDER, flag=wx.ALL|wx.EXPAND, proportion=1) def _create_notebook(self): @@ -242,7 +243,7 @@ class EventEditorDialog(wx.Dialog): "alert" : (_("Alert"), AlertEditor), "icon" : (_("Icon"), IconEditor) } if editors.has_key(editor_class_id): - return editors[editor_class_id] + return editors[editor_class_id] else: return None @@ -293,7 +294,7 @@ class EventEditorDialog(wx.Dialog): self.lst_containers.SetSelection(current_item_index) selection_set = True current_item_index += 1 - + self.last_real_container_index = current_item_index - 1 self.add_container_item_index = self.last_real_container_index + 2 self.edit_container_item_index = self.last_real_container_index + 3 @@ -343,19 +344,19 @@ class EventEditorDialog(wx.Dialog): def set_name(self, name): self.txt_text.SetValue(name) - + def get_name(self): return self.txt_text.GetValue().strip() def set_category(self, category): self.lst_category.select(category) - + def get_category(self): return self.lst_category.get() def set_container(self, container): self._fill_containers_listbox(container) - + def get_container(self): selection = self.lst_containers.GetSelection() if selection != -1: @@ -402,7 +403,7 @@ class EventEditorDialog(wx.Dialog): def _display_invalid_input(self, message, control): _display_error_message(message, self) - _set_focus_and_select(control) + _set_focus_and_select(control) def display_db_exception(self, e): gui_utils.handle_db_error_in_dialog(self, e) @@ -445,8 +446,8 @@ class IconEditor(wx.Panel): self.MAX_SIZE = (128, 128) # Controls self.img_icon = wx.StaticBitmap(self, size=self.MAX_SIZE) - label = _("Images will be scaled to fit inside a 128x128 box.") - description = wx.StaticText(self, label=label) + label = _("Images will be scaled to fit inside a %ix%i box.") + description = wx.StaticText(self, label=label % self.MAX_SIZE) btn_select = wx.Button(self, wx.ID_OPEN) btn_clear = wx.Button(self, wx.ID_CLEAR) self.Bind(wx.EVT_BUTTON, self._btn_select_on_click, btn_select) @@ -517,7 +518,7 @@ class AlertEditor(wx.Panel): self.editor = editor self._create_gui() self._initialize_data() - + def _create_gui(self): self._create_controls() self._layout_controls() @@ -526,7 +527,7 @@ class AlertEditor(wx.Panel): self._set_initial_time() self._set_initial_text() self._set_visible(False) - + def _set_initial_time(self): if self.editor.event is not None: self.dtp_start.set_value(self.editor.event.time_period.start_time) @@ -535,12 +536,12 @@ class AlertEditor(wx.Panel): def _set_initial_text(self): self.text_data.SetValue("") - + def _create_controls(self): self.btn_add = self._create_add_button() self.btn_clear = self._create_clear_button() self.alert_panel = self._create_input_controls() - + def _layout_controls(self): self._layout_input_controls(self.alert_panel) sizer = wx.GridBagSizer(5, 5) @@ -548,7 +549,7 @@ class AlertEditor(wx.Panel): sizer.Add(self.btn_clear, wx.GBPosition(0, 1), wx.GBSpan(1, 1)) sizer.Add(self.alert_panel, wx.GBPosition(1, 0), wx.GBSpan(4, 5)) self.SetSizerAndFit(sizer) - + def _create_add_button(self): btn_add = wx.Button(self, wx.ID_ADD) self.Bind(wx.EVT_BUTTON, self._btn_add_on_click, btn_add) @@ -558,7 +559,7 @@ class AlertEditor(wx.Panel): btn_clear = wx.Button(self, wx.ID_CLEAR) self.Bind(wx.EVT_BUTTON, self._btn_clear_on_click, btn_clear) return btn_clear - + def _create_input_controls(self): alert_panel = wx.Panel(self) time_type = self.editor.timeline.get_time_type() @@ -583,7 +584,7 @@ class AlertEditor(wx.Panel): return (time, text) else: return None - + def set_data(self, data): if data == None: self._set_visible(False) @@ -597,6 +598,9 @@ class AlertEditor(wx.Panel): self._set_visible(True) def _btn_clear_on_click(self, evt): + self.clear_data() + + def clear_data(self): self._set_initial_time() self._set_initial_text() self._set_visible(False) @@ -607,3 +611,21 @@ class AlertEditor(wx.Panel): self.btn_add.Enable(not value) self.btn_clear.Enable(value) self.GetSizer().Layout() + + +def open_event_editor_for(parent, config, db, handle_db_error, event): + def create_event_editor(): + if event.is_container(): + title = _("Edit Container") + return ContainerEditorDialog(parent, title, db, event) + else: + return EventEditorDialog( + parent, config, _("Edit Event"), db, event=event) + gui_utils.show_modal(create_event_editor, handle_db_error) + + +def open_create_event_editor(parent, config, db, handle_db_error, start=None, end=None): + def create_event_editor(): + return EventEditorDialog( + parent, config, _("Create Event"), db, start, end) + gui_utils.show_modal(create_event_editor, handle_db_error) diff --git a/timelinelib/wxgui/dialogs/mainframe.py b/timelinelib/wxgui/dialogs/mainframe.py index 51eb64f..179ebeb 100644 --- a/timelinelib/wxgui/dialogs/mainframe.py +++ b/timelinelib/wxgui/dialogs/mainframe.py @@ -23,8 +23,8 @@ import wx from timelinelib.application import TimelineApplication from timelinelib.config.dotfile import read_config from timelinelib.config.paths import ICONS_DIR +from timelinelib.db.exceptions import TimelineIOError from timelinelib.db import db_open -from timelinelib.db.interface import TimelineIOError from timelinelib.db.objects import TimePeriod from timelinelib.export.bitmap import export_to_image from timelinelib.meta.about import APPLICATION_NAME @@ -36,9 +36,8 @@ from timelinelib.wxgui.components.hyperlinkbutton import HyperlinkButton from timelinelib.wxgui.components.search import SearchBar from timelinelib.wxgui.components.timelineview import DrawingAreaPanel from timelinelib.wxgui.dialogs.categorieseditor import CategoriesEditor -from timelinelib.wxgui.dialogs.containereditor import ContainerEditorDialog -from timelinelib.wxgui.dialogs.duplicateevent import DuplicateEventDialog -from timelinelib.wxgui.dialogs.eventeditor import EventEditorDialog +from timelinelib.wxgui.dialogs.duplicateevent import open_duplicate_event_dialog_for_event +from timelinelib.wxgui.dialogs.eventeditor import open_create_event_editor from timelinelib.wxgui.dialogs.helpbrowser import HelpBrowser from timelinelib.wxgui.dialogs.playframe import PlayFrame from timelinelib.wxgui.dialogs.preferences import PreferencesDialog @@ -49,7 +48,6 @@ from timelinelib.wxgui.utils import _display_error_message from timelinelib.wxgui.utils import WildcardHelper import timelinelib.printing as printing import timelinelib.wxgui.utils as gui_utils -from timelinelib.time.wxtime import WxTimeType class MainFrame(wx.Frame): @@ -57,7 +55,7 @@ class MainFrame(wx.Frame): def __init__(self, application_arguments): self.config = read_config(application_arguments.get_config_file_path()) - wx.Frame.__init__(self, None, size=self.config.get_window_size(), + wx.Frame.__init__(self, None, size=self.config.get_window_size(), pos=self.config.get_window_pos(), style=wx.DEFAULT_FRAME_STYLE, name="main_frame") @@ -87,7 +85,7 @@ class MainFrame(wx.Frame): self.Bind(wx.EVT_TIMER, self._timer_tick, self.timer) self.timer.Start(10000) self.alert_dialog_open = False - + def _set_initial_values_to_member_variables(self): self.timeline = None self.timeline_wildcard_helper = WildcardHelper( @@ -112,7 +110,7 @@ class MainFrame(wx.Frame): self.status_bar_adapter = StatusBarAdapter(self.GetStatusBar()) def _create_main_panel(self): - self.main_panel = MainPanel(self, self.config) + self.main_panel = MainPanel(self, self.config, self) def _create_main_menu_bar(self): main_menu_bar = wx.MenuBar() @@ -303,7 +301,6 @@ class MainFrame(wx.Frame): def _mnu_file_exit_on_click(self, evt): self.Close() - exit() def _create_edit_menu(self, main_menu_bar): edit_menu = wx.Menu() @@ -387,14 +384,8 @@ class MainFrame(wx.Frame): self.menu_controller.add_menu_requiring_writable_timeline(create_event_item) def _mnu_timeline_create_event_on_click(self, evt): - self.create_new_event() - - def create_new_event(self, start=None, end=None): - def create_event_editor(): - return EventEditorDialog( - self, self.config, _("Create Event"), self.timeline, - start, end) - gui_utils.show_modal(create_event_editor, self.handle_db_error) + open_create_event_editor( + self, self.config, self.timeline, self.handle_db_error) def _create_timeline_duplicate_event_menu_item(self, timeline_menu): self.mnu_timeline_duplicate_event = timeline_menu.Append( @@ -404,31 +395,27 @@ class MainFrame(wx.Frame): self.menu_controller.add_menu_requiring_writable_timeline(self.mnu_timeline_duplicate_event) def _mnu_timeline_duplicate_event_on_click(self, evt): - self.duplicate_event() + try: + drawing_area = self.main_panel.drawing_area + id = drawing_area.get_view_properties().get_selected_event_ids()[0] + event = self.timeline.find_event_with_id(id) + except IndexError, e: + # No event selected so do nothing! + return + open_duplicate_event_dialog_for_event( + self, + self.timeline, + self.handle_db_error, + event) def _create_timeline_measure_distance_between_events_menu_item(self, timeline_menu): self.mnu_timeline_measure_distance_between_events = timeline_menu.Append( - wx.ID_ANY, _("&Measure Distance between two Events..."), + wx.ID_ANY, _("&Measure Distance between two Events..."), _("Measure the Distance between two Events")) self.Bind(wx.EVT_MENU, self._mnu_timeline_measure_distance_between_events_on_click, self.mnu_timeline_measure_distance_between_events) self.menu_controller.add_menu_requiring_writable_timeline(self.mnu_timeline_measure_distance_between_events) - def duplicate_event(self, event=None): - def show_dialog(event): - def create_dialog(): - return DuplicateEventDialog(self, self.timeline, event) - gui_utils.show_modal(create_dialog, self.handle_db_error) - if event is None: - try: - drawing_area = self.main_panel.drawing_area - id = drawing_area.get_view_properties().get_selected_event_ids()[0] - event = self.timeline.find_event_with_id(id) - except IndexError, e: - # No event selected so do nothing! - return - show_dialog(event) - def _mnu_timeline_measure_distance_between_events_on_click(self, evt): self._measure_distance_between_events() @@ -445,12 +432,12 @@ class MainFrame(wx.Frame): event2 = self.timeline.find_event_with_id(event_id_2) return event1, event2 - def _calc_events_distance(self,event1, event2): + def _calc_events_distance(self,event1, event2): if event1.time_period.start_time <= event2.time_period.start_time: - distance = (event2.time_period.start_time - + distance = (event2.time_period.start_time - event1.time_period.end_time) - else: - distance = (event1.time_period.start_time - + else: + distance = (event1.time_period.start_time - event2.time_period.end_time) return distance @@ -462,7 +449,7 @@ class MainFrame(wx.Frame): self._display_text(header, distance_text) def _display_text(self, header, text): - dialog = wx.MessageDialog(self, text, header, + dialog = wx.MessageDialog(self, text, header, wx.OK | wx.ICON_INFORMATION) dialog.ShowModal() dialog.Destroy() @@ -504,7 +491,7 @@ class MainFrame(wx.Frame): if event: start = event.time_period.start_time delta = self.main_panel.drawing_area.get_view_properties().displayed_period.delta() - end = start + delta + end = start + delta margin_delta = self.timeline.get_time_type().margin_delta(delta) self._navigate_timeline(lambda tp: tp.update(start, end, -margin_delta)) @@ -630,18 +617,6 @@ class MainFrame(wx.Frame): self.controller.open_timeline(input_file) self._update_navigation_menu_items() - def edit_event(self, event): - def create_event_editor(): - if event.is_container(): - parent = self - title = _("Edit Container") - timeline = self.timeline - return ContainerEditorDialog(parent, title, timeline, event) - else: - return EventEditorDialog(self, self.config, _("Edit Event"), - self.timeline, event=event) - gui_utils.show_modal(create_event_editor, self.handle_db_error) - def handle_db_error(self, error): _display_error_message(ex_msg(error), self) self._switch_to_error_view(error) @@ -738,7 +713,7 @@ class MainFrame(wx.Frame): _display_error_message(_("File '%s' does not exist.") % path, self) def enable_disable_menus(self): - self.menu_controller.enable_disable_menus(self.main_panel.timeline_panel_visible()) + self.menu_controller.enable_disable_menus(self.main_panel.timeline_panel_visible()) self._enable_disable_duplicate_event_menu() self._enable_disable_measure_distance_between_two_events_menu() self._enable_disable_searchbar() @@ -753,7 +728,7 @@ class MainFrame(wx.Frame): two_events_selected = len(view_properties.selected_event_ids) == 2 self.mnu_timeline_measure_distance_between_events.Enable(two_events_selected) - def _enable_disable_searchbar(self): + def _enable_disable_searchbar(self): if self.timeline == None: self.main_panel.show_searchbar(False) @@ -792,7 +767,7 @@ class MainFrame(wx.Frame): def _timer_tick(self, evt): self._handle_event_alerts() - + def _handle_event_alerts(self): if self.timeline is None: return @@ -804,44 +779,43 @@ class MainFrame(wx.Frame): def _display_events_alerts(self): self.alert_dialog_open = True all_events = self.timeline.get_all_events() - AlertController().display_events_alerts(all_events, self.timeline.time_type) + AlertController().display_events_alerts(all_events, self.timeline.get_time_type()) class AlertController(object): - + def display_events_alerts(self, all_events, time_type): self.time_type = time_type for event in all_events: alert = event.get_data("alert") if alert is not None: - alert_time = self._alert_time_as_text(alert) - if self._time_has_expired(alert_time): + if self._time_has_expired(alert[0]): self._display_and_delete_event_alert(event, alert) def _display_and_delete_event_alert(self, event, alert): self._display_alert_dialog(alert, event) event.set_data("alert", None) - + def _alert_time_as_text(self, alert): return "%s" % alert[0] - - def _time_has_expired(self, time_as_text): - now_as_text = "%s" % self.time_type.now() - return time_as_text <= now_as_text - + + def _time_has_expired(self, time): + return time <= self.time_type.now() + + def _display_alert_dialog(self, alert, event): text = self._format_alert_text(alert, event) dialog = TextDisplayDialog("Alert", text) dialog.ShowModal() dialog.Destroy() - - def _format_alert_text(self, alert, event): + + def _format_alert_text(self, alert, event): text1 = "Trigger time: %s\n\n" % alert[0] text2 = "Event: %s\n\n" % event.get_label() text = "%s%s%s" % (text1, text2, alert[1]) return text - + class MenuController(object): def __init__(self): @@ -899,9 +873,10 @@ class MainPanel(wx.Panel): Also displays the search bar. """ - def __init__(self, parent, config): + def __init__(self, parent, config, main_frame): wx.Panel.__init__(self, parent) self.config = config + self.main_frame = main_frame self._create_gui() # Install variables for backwards compatibility self.cattree = self.timeline_panel.sidebar.cattree @@ -935,9 +910,11 @@ class MainPanel(wx.Panel): self.searchbar = SearchBar(self, search_close) self.searchbar.Show(False) # Panels - self.welcome_panel = WelcomePanel(self) - self.timeline_panel = TimelinePanel(self, self.config) - self.error_panel = ErrorPanel(self) + self.welcome_panel = WelcomePanel(self, self.main_frame) + self.timeline_panel = TimelinePanel( + self, self.config, self.main_frame.handle_db_error, + self.main_frame.status_bar_adapter, self.main_frame) + self.error_panel = ErrorPanel(self, self.main_frame) # Layout self.sizerOuter = wx.BoxSizer(wx.VERTICAL) self.sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -961,8 +938,9 @@ class MainPanel(wx.Panel): class WelcomePanel(wx.Panel): - def __init__(self, parent): + def __init__(self, parent, main_frame): wx.Panel.__init__(self, parent) + self.main_frame = main_frame self._create_gui() def _create_gui(self): @@ -985,7 +963,7 @@ class WelcomePanel(wx.Panel): self.SetSizer(hsizer) def _btn_tutorial_on_click(self, e): - wx.GetTopLevelParent(self).open_timeline(":tutorial:") + self.main_frame.open_timeline(":tutorial:") def activated(self): pass @@ -993,9 +971,13 @@ class WelcomePanel(wx.Panel): class TimelinePanel(wx.Panel): - def __init__(self, parent, config): + def __init__(self, parent, config, handle_db_error, status_bar_adapter, + main_frame): wx.Panel.__init__(self, parent) self.config = config + self.handle_db_error = handle_db_error + self.status_bar_adapter = status_bar_adapter + self.main_frame = main_frame self.sidebar_width = self.config.get_sidebar_width() self._create_gui() @@ -1023,16 +1005,16 @@ class TimelinePanel(wx.Panel): self.sidebar_width = self.splitter.GetSashPosition() def _create_sidebar(self): - self.sidebar = Sidebar(self.splitter) + self.sidebar = Sidebar(self.splitter, self.handle_db_error) def _create_drawing_area(self): - main_frame = wx.GetTopLevelParent(self) self.drawing_area = DrawingAreaPanel( self.splitter, - main_frame.status_bar_adapter, + self.status_bar_adapter, self.divider_line_slider, - main_frame.handle_db_error, - self.config) + self.handle_db_error, + self.config, + self.main_frame) def _layout_components(self): sizer = wx.BoxSizer(wx.HORIZONTAL) @@ -1058,8 +1040,9 @@ class TimelinePanel(wx.Panel): class ErrorPanel(wx.Panel): - def __init__(self, parent): + def __init__(self, parent, main_frame): wx.Panel.__init__(self, parent) + self.main_frame = main_frame self._create_gui() def populate(self, error): @@ -1085,7 +1068,7 @@ class ErrorPanel(wx.Panel): self.SetSizer(hsizer) def _btn_contact_on_click(self, e): - wx.GetTopLevelParent(self).help_browser.show_page("contact") + self.main_frame.help_browser.show_page("contact") def activated(self): pass @@ -1098,13 +1081,12 @@ class Sidebar(wx.Panel): Currently only shows the categories with visibility check boxes. """ - def __init__(self, parent): + def __init__(self, parent, handle_db_error): wx.Panel.__init__(self, parent, style=wx.BORDER_NONE) - self._create_gui() + self._create_gui(handle_db_error) - def _create_gui(self): - main_frame = wx.GetTopLevelParent(self) - self.cattree = CategoriesTree(self, main_frame.handle_db_error) + def _create_gui(self, handle_db_error): + self.cattree = CategoriesTree(self, handle_db_error) # Layout sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.cattree, flag=wx.GROW, proportion=1) diff --git a/timelinelib/wxgui/dialogs/preferences.py b/timelinelib/wxgui/dialogs/preferences.py index cb2d280..25cdd9c 100644 --- a/timelinelib/wxgui/dialogs/preferences.py +++ b/timelinelib/wxgui/dialogs/preferences.py @@ -36,7 +36,7 @@ class PreferencesDialog(wx.Dialog): def set_checkbox_use_inertial_scrolling(self, value): self.chb_inertial_scrolling.SetValue(value) - def set_checkbox_open_recent_at_startup(self, value): + def set_checkbox_open_recent_at_startup(self, value): self.chb_open_recent.SetValue(value) def set_week_start(self, index): @@ -69,7 +69,7 @@ class PreferencesDialog(wx.Dialog): return button_box def _create_general_tab(self, notebook): - panel = self._create_tab_panel(notebook, _("General")) + panel = self._create_tab_panel(notebook, _("General")) controls = self._create_general_tab_controls(panel) self._size_tab_panel(panel, controls) @@ -79,7 +79,7 @@ class PreferencesDialog(wx.Dialog): return (self.chb_open_recent, self.chb_inertial_scrolling) def _create_date_time_tab(self, notebook): - panel = self._create_tab_panel(notebook, _("Date && Time")) + panel = self._create_tab_panel(notebook, _("Date && Time")) controls = self._create_date_time_tab_controls(panel) self._size_tab_panel(panel, controls) @@ -87,7 +87,7 @@ class PreferencesDialog(wx.Dialog): self.chb_wide_date_range = self._create_chb_wide_date_range(panel) self.choice_week = self._create_choice_week(panel) grid = wx.FlexGridSizer(1, 2, BORDER, BORDER) - grid.Add(wx.StaticText(panel, label=_("Week start on:")), + grid.Add(wx.StaticText(panel, label=_("Week start on:")), flag=wx.ALIGN_CENTER_VERTICAL) grid.Add(self.choice_week, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) warning = _("This feature is experimental. If events are\ncreated in the extended range, you can not\ndisable this option and successfully load\nthe timeline again. A reload of the timeline\nis also needed for this to take effect.") @@ -110,19 +110,19 @@ class PreferencesDialog(wx.Dialog): label = _("Open most recent timeline on startup") handler = self._chb_open_recent_startup_on_checkbox chb = self._create_chb(panel, label, handler) - return chb + return chb def _create_chb_inertial_scrolling(self, panel): label = _("Use inertial scrolling") handler = self._chb_use_inertial_scrolling_on_checkbox - chb = self._create_chb(panel, label, handler) + chb = self._create_chb(panel, label, handler) return chb def _create_chb_wide_date_range(self, panel): label = _("Use extended date range (before 1 AD)") handler = self._chb_use_wide_date_range_on_checkbox - chb = self._create_chb(panel, label, handler) - return chb + chb = self._create_chb(panel, label, handler) + return chb def _create_chb(self, panel, label, handler): chb = wx.CheckBox(panel, label=label) @@ -140,7 +140,7 @@ class PreferencesDialog(wx.Dialog): btn_close.SetFocus() self.SetAffirmativeId(wx.ID_CLOSE) self.Bind(wx.EVT_BUTTON, self._btn_close_on_click, btn_close) - return btn_close + return btn_close def _chb_use_wide_date_range_on_checkbox(self, evt): self._controller.on_use_wide_date_range_changed(evt.IsChecked()) diff --git a/timelinelib/wxgui/dialogs/textdisplay.py b/timelinelib/wxgui/dialogs/textdisplay.py index cbc0713..4c13a95 100644 --- a/timelinelib/wxgui/dialogs/textdisplay.py +++ b/timelinelib/wxgui/dialogs/textdisplay.py @@ -16,45 +16,82 @@ # along with Timeline. If not, see . -from timelinelib.wxgui.utils import BORDER - import wx +from timelinelib.wxgui.utils import BORDER +from timelinelib.wxgui.utils import _display_error_message +from timelinelib.editors.textdisplay import TextDisplayEditor -class TextDisplayDialog(wx.Dialog): - def __init__(self, title, text, parent=None): +class TextDisplayDialogGui(wx.Dialog): + + def __init__(self, title, parent=None): wx.Dialog.__init__(self, parent, title=title) self._create_gui() - self._text.SetValue(text) def _create_gui(self): - self._text = wx.TextCtrl(self, size=(660, 300), style=wx.TE_MULTILINE) + self._text = self._create_text_control() + button_box = self._create_button_box() + vbox = self._create_vbox(self._text, button_box) + self.SetSizerAndFit(vbox) + + def _create_text_control(self): + return wx.TextCtrl(self, size=(660, 300), style=wx.TE_MULTILINE) + + def _create_button_box(self): + self.btn_copy = self._create_copy_btn() + self.btn_close = self._create_close_btn() + button_box = wx.BoxSizer(wx.HORIZONTAL) + button_box.Add(self.btn_copy, flag=wx.RIGHT, border=BORDER) + button_box.AddStretchSpacer() + button_box.Add(self.btn_close, flag=wx.LEFT, border=BORDER) + return button_box + + def _create_vbox(self, text, btn_box): + vbox = wx.BoxSizer(wx.VERTICAL) + vbox.Add(text, flag=wx.ALL|wx.EXPAND, border=BORDER) + vbox.Add(btn_box, flag=wx.ALL|wx.EXPAND, border=BORDER) + return vbox + + def _create_copy_btn(self): btn_copy = wx.Button(self, wx.ID_COPY) - self.Bind(wx.EVT_BUTTON, self._btn_copy_on_click, btn_copy) + return btn_copy + + def _create_close_btn(self): btn_close = wx.Button(self, wx.ID_CLOSE) btn_close.SetDefault() btn_close.SetFocus() self.SetAffirmativeId(wx.ID_CLOSE) - self.Bind(wx.EVT_BUTTON, self._btn_close_on_click, btn_close) - # Layout - vbox = wx.BoxSizer(wx.VERTICAL) - vbox.Add(self._text, flag=wx.ALL|wx.EXPAND, border=BORDER) - button_box = wx.BoxSizer(wx.HORIZONTAL) - button_box.Add(btn_copy, flag=wx.RIGHT, border=BORDER) - button_box.AddStretchSpacer() - button_box.Add(btn_close, flag=wx.LEFT, border=BORDER) - vbox.Add(button_box, flag=wx.ALL|wx.EXPAND, border=BORDER) - self.SetSizerAndFit(vbox) + return btn_close + + +class TextDisplayDialog(TextDisplayDialogGui): + + def __init__(self, title, text, parent=None): + TextDisplayDialogGui.__init__(self, title, parent) + self._bind_events() + self.controller = TextDisplayEditor(self, text) + self.controller.initialize() + + def set_text(self, text): + self._text.SetValue(text) + + def get_text(self): + return self._text.GetValue() + + def _bind_events(self): + self.Bind(wx.EVT_BUTTON, self._btn_copy_on_click, self.btn_copy) + self.Bind(wx.EVT_BUTTON, self._btn_close_on_click, self.btn_close) def _btn_copy_on_click(self, evt): if wx.TheClipboard.Open(): - obj = wx.TextDataObject(self._text.GetValue()) - wx.TheClipboard.SetData(obj) - wx.TheClipboard.Close() + self._copy_text_to_clipboard() else: - msg = _("Unable to copy to clipboard.") - _display_error_message(msg) + _display_error_message(_("Unable to copy to clipboard.")) + def _copy_text_to_clipboard(self): + obj = wx.TextDataObject(self.controller.get_text()) + wx.TheClipboard.SetData(obj) + wx.TheClipboard.Close() def _btn_close_on_click(self, evt): self.Close() diff --git a/timelinelib/wxgui/utils.py b/timelinelib/wxgui/utils.py index 0038e02..b7fc101 100644 --- a/timelinelib/wxgui/utils.py +++ b/timelinelib/wxgui/utils.py @@ -18,8 +18,8 @@ import wx -from timelinelib.db.interface import TimelineIOError -from timelinelib.domain.category import sort_categories +from timelinelib.db.exceptions import TimelineIOError +from timelinelib.db.objects.category import sort_categories # Border, in pixels, between controls in a window (should always be used when @@ -146,7 +146,7 @@ def set_wait_cursor(parent): def set_default_cursor(parent): - parent.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) + parent.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT)) def time_picker_for(time_type): diff --git a/timelinelib/xml/__init__.py b/timelinelib/xml/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/timelinelib/xml/__init__.py diff --git a/timelinelib/xml/parser.py b/timelinelib/xml/parser.py new file mode 100644 index 0000000..94933ee --- /dev/null +++ b/timelinelib/xml/parser.py @@ -0,0 +1,270 @@ +# Copyright (C) 2009, 2010, 2011 Rickard Lindberg, Roger Lindberg +# +# This file is part of Timeline. +# +# Timeline 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 3 of the License, or +# (at your option) any later version. +# +# Timeline 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 Timeline. If not, see . + + +""" +A simple, validating, SAX-based XML parser. + +Since it is simple, it has some limitations: + + - It can not parse attributes + - It can not parse arbitrary nested structures + - It can only parse text in leaf nodes: in other words, this piece of XML + is not possible to parse: some text here and there + +Here's an example how to parse a simple XML document using this module. + +First we create a file-like object containing the XML data (any file-like +object is fine, but we create a StringIO for the purpose of making a working +example): + + >>> from StringIO import StringIO + + >>> xml_stream = StringIO(''' + ... + ... + ... Rickard + ... + ... + ... James + ... 38 + ... + ... + ... ''') + +Then we define two parser functions that we later associate with Tag objects. +Parse functions are called when the end tag has been read. The first argument +to a parse function is the text that the tag contains. It will be empty for all +tags except leaf tags. The second argument is a dictionary that can be used to +store temporary variables. This dictionary is passed to all parse functions, +providing a way to share information between parse functions. + + >>> def parse_name(text, tmp_dict): + ... tmp_dict["tmp_name"] = text + + >>> def parse_person(text, tmp_dict): + ... # text is empty here since person is not a leaf tag + ... name = tmp_dict.pop("tmp_name") + ... age = tmp_dict.pop("tmp_age", None) + ... print("Found %s in db." % name) + ... if age is not None: + ... print("%s is %s years old." % (name, age)) + +Next we define the structure of the XML document that we are going to parse by +creating Tag objects. The first argument is the name of the tag, the second +specifies how many times it can occur inside its parent (should be one of +SINGLE, OPTIONAL, or ANY), the third argument is the parse function to be used +for this tag (can be None if no parsing is needed), and the fourth argument is +a list of child tags. + + >>> root_tag = Tag("db", SINGLE, None, [ + ... Tag("person", ANY, parse_person, [ + ... Tag("name", SINGLE, parse_name), + ... Tag("age", OPTIONAL, parse_fn_store("tmp_age")), + ... ]), + ... ]) + +The parse_fn_store function returns a parser function that works exactly like +parse_name: it takes the text of the tag and stores it in the dictionary with +the given key (tmp_age in this case). + +The last step is to call the parse function with the stream, the tag +configuration, and a dictionary. The dictionary can be populated with values +before parsing starts if needed. + + >>> parse(xml_stream, root_tag, {}) + Found Rickard in db. + Found James in db. + James is 38 years old. + +The parse function will raise a ValidationError if the XML is not valid and a +SAXException the if the XML is not well-formed. +""" + + +from xml.sax import parse as sax_parse +import sys +import xml.sax.handler + + +# Occurrence rules for tags +SINGLE = 1 +OPTIONAL = 2 +ANY = 3 + + +class ValidationError(Exception): + """Raised when parsed xml document does not follow the schema.""" + pass + + +class Tag(object): + """ + Represents a tag in an xml document. + + Used to define structure of an xml document and define parser functions for + individual parts of an xml document. + + Parser functions are called when the end tag has been read. + + See SaxHandler class defined below to see how this class is used. + """ + + def __init__(self, name, occurrence_rule, parse_fn, child_tags=[]): + self.name = name + self.occurrence_rule = occurrence_rule + self.parse_fn = parse_fn + self.child_tags = [] + self.add_child_tags(child_tags) + self.parent = None + # Variables defining state + self.occurrences = 0 + self.next_possible_child_pos = 0 + self.start_read = False + + def add_child_tags(self, tags): + for tag in tags: + self.add_child_tag(tag) + + def add_child_tag(self, tag): + tag.parent = self + self.child_tags.append(tag) + + def read_enough_times(self): + return self.occurrences > 0 or self.occurrence_rule in (OPTIONAL, ANY) + + def can_read_more(self): + return self.occurrences == 0 or self.occurrence_rule == ANY + + def handle_start_tag(self, name, tmp_dict): + if name == self.name: + if self.start_read == True: + # Nested tag + raise ValidationError("Did not expect <%s>." % name) + else: + self.start_read = True + return self + elif self.start_read == True: + next_child = self._find_next_child(name) + return next_child.handle_start_tag(name, tmp_dict) + else: + raise ValidationError("Expected <%s> but got <%s>." + % (self.name, name)) + + def handle_end_tag(self, name, text, tmp_dict): + self._ensure_end_tag_valid(name, text) + if self.parse_fn is not None: + self.parse_fn(text, tmp_dict) + self._ensure_all_children_read() + self._reset_parse_data() + self.occurrences += 1 + return self.parent + + def _ensure_end_tag_valid(self, name, text): + if name != self.name: + raise ValidationError("Expected but got ." + % (self.name, name)) + if self.child_tags: + if text.strip(): + raise ValidationError("Did not expect text but got '%s'." + % text) + + def _ensure_all_children_read(self): + num_child_tags = len(self.child_tags) + while self.next_possible_child_pos < num_child_tags: + child = self.child_tags[self.next_possible_child_pos] + if not child.read_enough_times(): + raise ValidationError("<%s> not read enough times." + % child.name) + self.next_possible_child_pos += 1 + + def _reset_parse_data(self): + for child_tag in self.child_tags: + child_tag.occurrences = 0 + self.next_possible_child_pos = 0 + self.start_read = False + + def _find_next_child(self, name): + num_child_tags = len(self.child_tags) + while self.next_possible_child_pos < num_child_tags: + child = self.child_tags[self.next_possible_child_pos] + if child.name == name: + if child.can_read_more(): + return child + else: + break + else: + if child.read_enough_times(): + self.next_possible_child_pos += 1 + else: + break + raise ValidationError("Did not expect <%s>." % name) + + +class SaxHandler(xml.sax.handler.ContentHandler): + + def __init__(self, root_tag, tmp_dict): + self.tag_to_parse = root_tag + self.tmp_dict = tmp_dict + self.text = "" + + def startElement(self, name, attrs): + """ + Called when a start tag has been read. + """ + if attrs.getLength() > 0: + raise ValidationError("Did not expect attributes on <%s>." % name) + if self.text.strip(): + raise ValidationError("Did not expect text but got '%s'." + % self.text) + self.tag_to_parse = self.tag_to_parse.handle_start_tag(name, + self.tmp_dict) + self.text = "" + + def endElement(self, name): + """ + Called when an end tag (and everything between the start and end tag) + has been read. + """ + self.tag_to_parse = self.tag_to_parse.handle_end_tag(name, self.text, + self.tmp_dict) + self.text = "" + + def characters(self, content): + self.text += content + + +def parse(xml, schema, tmp_dict): + """ + xml should be a filename or a file-like object containing xml data. + + schema should be a Tag object defining the structure of the xml document. + + tmp_dict is used by parser functions in Tag objects to share data. It can + be pre-populated with values. + """ + if isinstance(xml, unicode): + # Workaround for "Sax parser crashes if given unicode file name" bug: + # http://bugs.python.org/issue11159 + xml = xml.encode(sys.getfilesystemencoding()) + sax_parse(xml, SaxHandler(schema, tmp_dict)) + + +def parse_fn_store(store_key): + def fn(text, tmp_dict): + tmp_dict[store_key] = text + return fn -- cgit v0.9.1