From 800e3caabcd9c85b0b2ae9299217ea31d0309545 Mon Sep 17 00:00:00 2001 From: Antoine van Gelder Date: Sun, 28 Oct 2007 09:45:28 +0000 Subject: Initial import --- diff --git a/JokeMachine.wpr b/JokeMachine.wpr new file mode 100644 index 0000000..c806ff4 --- /dev/null +++ b/JokeMachine.wpr @@ -0,0 +1,1088 @@ +#!wing +#!version=3.0 +################################################################## +# Wing IDE project file # +################################################################## +[project attributes] +proj.directory-list = [{'dirloc': loc('persistence'), + 'excludes': (), + 'filter': '*', + 'include_hidden': 0, + 'recursive': 1, + 'watch_for_changes': 1}, + {'dirloc': loc('.'), + 'excludes': (), + 'filter': '*', + 'include_hidden': 0, + 'recursive': 1, + 'watch_for_changes': 1}] +proj.file-list = [loc('activity/activity-jokemachine.svg'), + loc('activity/activity.info'), + loc('globals.py'), + loc('i18n.py'), + loc('i18n_misc_strings.py'), + loc('images/GameLogoCharacter.png'), + loc('JokeMachineActivity.py'), + loc('lessons/Introduction/default.abw'), + loc('lessons/Lesson 1/default.abw'), + loc('lessons/Lesson 2/default.abw'), + loc('lessons/Lesson 3/default.abw'), + loc('lessons/Lesson 4/default.abw'), + loc('locale/af.po'), + loc('locale/de_DE/activity.linfo'), + loc('locale/org.worldwideworkshop.jokemachine.pot'), + loc('locale/org.worldwideworkshop.poll.pot'), + loc('locale/zh_CN/activity.linfo'), + loc('MANIFEST'), + loc('NEWS'), + loc('pages/choose.py'), + loc('pages/cover.py'), + loc('pages/frame.py'), + loc('pages/joke.py'), + loc('pages/page.py'), + loc('pages/submit.py'), + loc('pages/theme.py'), + loc('pages/__init__.py'), + loc('setup.py'), + loc('unit/persistence.py')] +proj.file-type = 'normal' +[user attributes] +debug.err-values = {None: {}} +debug.var-col-widths = [0.39499036608863197, + 0.60500963391136797] +edit.show-tab-force-warning = 0 +guimgr.overall-gui-state = {'windowing-policy': 'combined-window', + 'windows': [{'name': 'oQxUmNpVfpJewBNvtbSPVPBdPA'\ + 'TKgEGJ', + 'size-state': '', + 'type': 'dock', + 'view': {'area': 'tall', + 'current_pages': [0, + 2], + 'notebook_display': 'normal', + 'notebook_percent': 0.22585551330798478, + 'override_title': None, + 'pagelist': [('debug-stack', + 'tall', + 1, + None), + ('indent', + 'tall', + 2, + {}), + ('project', + 'tall', + 0, + {'tree-state': {'tree-states': {'deep': {'col'\ + 'umn-widths': [1.0], + 'expanded-nodes': [(0,)], + 'selected-nodes': [], + 'top-node': (0,)}}, + 'tree-style': 'deep'}}), + ('source-assistant', + 'tall', + 2, + {'docstring-during-complete': 0, + 'wrap-lines': True}), + ('browser', + 'tall', + 0, + {'all_tree_states': {u'By Module': {'column-w'\ + 'idths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [[('generic attribute', + loc('globals.py'), + '')]], + 'top-node': [('generic attribute', + loc('globals.py'), + '')]}, + loc('JokeMachineActivity.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('JokeMachineActivity.py'), + 'JokeMachineActivity')]}, + loc('MANIFEST'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': None}, + loc('TODO'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': None}, + loc('globals.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('generic attribute', + loc('globals.py'), + 'Globals')]}, + loc('gui/canvasimage.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': None}, + loc('gui/canvaslistbox.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('gui/canvaslistbox.py'), + 'CanvasListBox')]}, + loc('gui/frame.py'): {'column-widths': [1.0], + 'expanded-nodes': [[('class def', + loc('gui/frame.py'), + 'Frame')]], + 'selected-nodes': [], + 'top-node': [('class def', + loc('gui/frame.py'), + 'Frame')]}, + loc('gui/page.py'): {'column-widths': [1.0], + 'expanded-nodes': [[('class def', + loc('gui/page.py'), + 'Page')]], + 'selected-nodes': [[('class def', + loc('gui/page.py'), + 'Page')]], + 'top-node': [('class def', + loc('gui/page.py'), + 'Page')]}, + loc('gui/theme.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('generic attribute', + loc('gui/theme.py'), + 'BORDER_WIDTH')]}, + loc('pages/choose.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('pages/choose.py'), + 'Choose')]}, + loc('pages/cover.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('pages/cover.py'), + 'Cover')]}, + loc('pages/edit.py'): {'column-widths': [1.0], + 'expanded-nodes': [[('class def', + loc('pages/edit.py'), + 'Edit')], + [('class def', + loc('pages/edit.py'), + 'EditInfo')], + [('class def', + loc('pages/edit.py'), + 'EditJokes')], + [('class def', + loc('pages/edit.py'), + 'EditReview')], + [('class def', + loc('pages/edit.py'), + 'PageSelector')]], + 'selected-nodes': [], + 'top-node': [('class def', + loc('pages/edit.py'), + 'Edit')]}, + loc('pages/joke.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('pages/joke.py'), + 'Joke')]}, + loc('pages/preview.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('pages/preview.py'), + 'Preview')]}, + loc('pages/submit.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('pages/submit.py'), + 'JokeEditor')]}, + loc('persistence/joke.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('persistence/joke.py'), + 'Joke')]}, + loc('persistence/jokebook.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('persistence/jokebook.py'), + 'Jokebook')]}, + loc('persistence/jokemachinestate.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('persistence/jokemachinestate.py'), + 'JokeMachineState')]}, + loc('setup.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': None}, + loc('unit/test_persistence.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('function def', + loc('unit/test_persistence.py'), + 'dump')]}, + loc('../poll-builder/poll.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('generic attribute', + loc('../poll-builder/poll.py'), + 'COLOR_BG_BUTTONS')]}, + u'All Classes': {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [None], + 'top-node': None}, + loc('unit/unit.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('generic attribute', + loc('unit/unit.py'), + 'bar')]}, + loc('util/decorators.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('generic attribute', + loc('util/decorators.py'), + 'DecoratorWithArgs')]}, + loc('util/persistence.py'): {'column-widths': [1.0], + 'expanded-nodes': [], + 'selected-nodes': [], + 'top-node': [('class def', + loc('util/persistence.py'), + 'Persistent')]}}, + 'browse_mode': u'Current Module', + 'follow-selection': 0, + 'sort_mode': 'Alphabetically', + 'visibility_options': {u'Derived Classes': 0, + u'Imported': 0, + u'Inherited': 0, + u'Modules': 1}}), + ('templating#02EFWRQK9X23', + 'tall', + 0, + {'tree-states': {u'/home/antoine/.wingide3/templates': [], + u'/opt/wing/scripts/templates': []}})], + 'primary_view_state': {'area': 'wide', + 'current_pages': [3, + 3], + 'notebook_display': 'normal', + 'notebook_percent': 0.30000000000000004, + 'override_title': None, + 'pagelist': [('bookmarks', + 'wide', + 1, + None), + ('debug-breakpoints', + 'wide', + 0, + None), + ('debug-io', + 'wide', + 1, + {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}), + ('debug-probe', + 'wide', + 2, + {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 4, + 'selection_start': 4}), + ('debug-exceptions', + 'wide', + 0, + None), + ('debug-modules', + 'wide', + 1, + {}), + ('python-shell', + 'wide', + 2, + {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 3, + 'sel-line-start': 148, + 'selection_end': 152, + 'selection_start': 152}), + ('interactive-search', + 'wide', + 0, + {'fScope': {'fFileSetName': u'All Source Files', + 'fLocation': None, + 'fRecursive': True, + 'fType': 'current-file'}, + 'fSearchSpec': {'fEndPos': None, + 'fIncludeLinenos': True, + 'fInterpretBackslashes': False, + 'fMatchCase': 1, + 'fOmitBinary': True, + 'fRegexFlags': 44, + 'fReplaceText': u'self.__is_initiator', + 'fReverse': False, + 'fSearchText': u'width', + 'fStartPos': 0, + 'fStyle': 'text', + 'fWholeWords': False, + 'fWrap': True}, + 'fUIOptions': {'fAutoBackground': True, + 'fFilePrefix': 'short-file', + 'fFindAfterReplace': True, + 'fInSelection': False, + 'fIncremental': True, + 'fReplaceOnDisk': False, + 'fShowFirstMatch': False, + 'fShowLineno': True, + 'fShowReplaceWidgets': 1}, + 'replace-entry-expanded': False, + 'search-entry-expanded': False}), + ('batch-search', + 'wide', + 0, + {'fScope': {'fFileSetName': u'All Source Files', + 'fLocation': None, + 'fRecursive': True, + 'fType': 'project-files'}, + 'fSearchSpec': {'fEndPos': None, + 'fIncludeLinenos': True, + 'fInterpretBackslashes': False, + 'fMatchCase': False, + 'fOmitBinary': True, + 'fRegexFlags': 46, + 'fReplaceText': u'', + 'fReverse': False, + 'fSearchText': u'.png', + 'fStartPos': 0, + 'fStyle': 'text', + 'fWholeWords': False, + 'fWrap': True}, + 'fUIOptions': {'fAutoBackground': True, + 'fFilePrefix': 'short-file', + 'fFindAfterReplace': True, + 'fInSelection': False, + 'fIncremental': True, + 'fReplaceOnDisk': False, + 'fShowFirstMatch': False, + 'fShowLineno': True, + 'fShowReplaceWidgets': False}, + 'replace-entry-expanded': False, + 'search-entry-expanded': False}), + ('debug-data', + 'wide', + 0, + {}), + ('debug-watch', + 'wide', + 1, + {'node-states': [('eval', + ''), + ('eval', + ''), + ('eval', + '')], + 'tree-state': {'column-widths': [0.39468690702087289, + 0.60531309297912717], + 'expanded-nodes': [], + 'selected-nodes': [(2,)], + 'top-node': (0,)}})], + 'primary_view_state': {'editor_states': {'bookmarks': ([(loc('pages/choose.py'), + {'attrib-starts': [], + 'first-line': 13, + 'sel-line': 33, + 'sel-line-start': 1020, + 'selection_end': 1062, + 'selection_start': 1062}, + 1193494144.0174799), + (loc('gui/canvaslistbox.py'), + {'attrib-starts': [('CanvasListBox', + 25), + ('CanvasListBox.__init__', + 26)], + 'first-line': 0, + 'sel-line': 47, + 'sel-line-start': 1638, + 'selection_end': 1657, + 'selection_start': 1657}, + 1193494144.2723279), + (loc('pages/choose.py'), + {'attrib-starts': [('Choose', + 35), + ('Choose.__make_column_div', + 118)], + 'first-line': 62, + 'sel-line': 127, + 'sel-line-start': 4655, + 'selection_end': 4655, + 'selection_start': 4655}, + 1193494156.910459), + (loc('TODO'), + {'attrib-starts': [], + 'first-line': 37, + 'sel-line': 58, + 'sel-line-start': 1754, + 'selection_end': 1775, + 'selection_start': 1769}, + 1193563665.842417), + (loc('TODO'), + {'attrib-starts': [], + 'first-line': 37, + 'sel-line': 59, + 'sel-line-start': 1782, + 'selection_end': 1825, + 'selection_start': 1819}, + 1193563667.4626391), + (loc('globals.py'), + {'attrib-starts': [('__globals', + 28), + ('__globals.__init__', + 32)], + 'first-line': 13, + 'sel-line': 34, + 'sel-line-start': 1122, + 'selection_end': 1150, + 'selection_start': 1150}, + 1193563670.4271569), + (loc('i18n.py'), + {'attrib-starts': [('gather_other_translations', + 164)], + 'first-line': 133, + 'sel-line': 166, + 'sel-line-start': 5557, + 'selection_end': 5619, + 'selection_start': 5619}, + 1193563682.206208), + (loc('persistence/joke.py'), + {'attrib-starts': [('Joke', + 18), + ('Joke.test_data', + 78)], + 'first-line': 50, + 'sel-line': 79, + 'sel-line-start': 2386, + 'selection_end': 2391, + 'selection_start': 2391}, + 1193563696.6672399), + (loc('persistence/jokemachinestate.py'), + {'attrib-starts': [('JokeMachineState', + 20), + ('JokeMachineState.test_data', + 57)], + 'first-line': 38, + 'sel-line': 69, + 'sel-line-start': 2153, + 'selection_end': 2160, + 'selection_start': 2160}, + 1193563709.7158151), + (loc('gui/page.py'), + {'attrib-starts': [('Page', + 47), + ('Page.make_audiobox', + 82)], + 'first-line': 63, + 'sel-line': 84, + 'sel-line-start': 2487, + 'selection_end': 2540, + 'selection_start': 2540}, + 1193563715.156831), + (loc('persistence/jokemachinestate.py'), + {'attrib-starts': [('JokeMachineState', + 20), + ('JokeMachineState.test_data', + 57)], + 'first-line': 38, + 'sel-line': 69, + 'sel-line-start': 2153, + 'selection_end': 2184, + 'selection_start': 2178}, + 1193563726.00191), + (loc('gui/page.py'), + {'attrib-starts': [('Page', + 47), + ('Page.make_audiobox', + 82)], + 'first-line': 68, + 'sel-line': 89, + 'sel-line-start': 2679, + 'selection_end': 2705, + 'selection_start': 2699}, + 1193563726.9944689), + (loc('gui/theme.py'), + {'attrib-starts': [], + 'first-line': 103, + 'sel-line': 112, + 'sel-line-start': 3697, + 'selection_end': 3709, + 'selection_start': 3697}, + 1193563731.5031071), + (loc('gui/page.py'), + {'attrib-starts': [('Page', + 47), + ('Page.make_audiobox', + 82)], + 'first-line': 63, + 'sel-line': 84, + 'sel-line-start': 2487, + 'selection_end': 2548, + 'selection_start': 2548}, + 1193563805.340421), + (loc('globals.py'), + {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}, + 1193563815.940026), + (loc('gui/theme.py'), + {'attrib-starts': [], + 'first-line': 107, + 'sel-line': 110, + 'sel-line-start': 3611, + 'selection_end': 3611, + 'selection_start': 3611}, + 1193563828.094069), + (loc('persistence/joke.py'), + {'attrib-starts': [('Joke', + 18), + ('Joke.test_data', + 78)], + 'first-line': 50, + 'sel-line': 79, + 'sel-line-start': 2386, + 'selection_end': 2429, + 'selection_start': 2425}, + 1193563838.396673), + (loc('globals.py'), + {'attrib-starts': [('__globals', + 28), + ('__globals.__init__', + 32)], + 'first-line': 13, + 'sel-line': 34, + 'sel-line-start': 1122, + 'selection_end': 1172, + 'selection_start': 1168}, + 1193563842.62817), + (loc('gui/theme.py'), + {'attrib-starts': [], + 'first-line': 89, + 'sel-line': 110, + 'sel-line-start': 3611, + 'selection_end': 3646, + 'selection_start': 3642}, + 1193563845.760396), + [loc('gui/theme.py'), + {'attrib-starts': [], + 'first-line': 89, + 'sel-line': 110, + 'sel-line-start': 3611, + 'selection_end': 3646, + 'selection_start': 3642}, + 1193563846.4001839]], + 19), + 'current-loc': loc('gui/theme.py'), + 'editor-states': {loc('TODO'): {'attrib-starts': [], + 'first-line': 37, + 'sel-line': 59, + 'sel-line-start': 1782, + 'selection_end': 1825, + 'selection_start': 1819}, + loc('globals.py'): {'attrib-starts': [('__globals', + 28), + ('__globals.__init__', + 32)], + 'first-line': 13, + 'sel-line': 34, + 'sel-line-start': 1122, + 'selection_end': 1172, + 'selection_start': 1168}, + loc('gui/canvaslistbox.py'): {'attrib-starts': [(''\ + 'CanvasListBox', + 25), + ('CanvasListBox.__init__', + 26)], + 'first-line': 0, + 'sel-line': 47, + 'sel-line-start': 1638, + 'selection_end': 1657, + 'selection_start': 1657}, + loc('gui/page.py'): {'attrib-starts': [('Page', + 47), + ('Page.make_audiobox', + 82)], + 'first-line': 63, + 'sel-line': 84, + 'sel-line-start': 2487, + 'selection_end': 2548, + 'selection_start': 2548}, + loc('gui/theme.py'): {'attrib-starts': [], + 'first-line': 89, + 'sel-line': 103, + 'sel-line-start': 3392, + 'selection_end': 3440, + 'selection_start': 3440}, + loc('i18n.py'): {'attrib-starts': [('gather_other_'\ + 'translations', + 164)], + 'first-line': 133, + 'sel-line': 166, + 'sel-line-start': 5557, + 'selection_end': 5619, + 'selection_start': 5619}, + loc('pages/choose.py'): {'attrib-starts': [('Choos'\ + 'e', + 35), + ('Choose.__make_column_div', + 118)], + 'first-line': 62, + 'sel-line': 127, + 'sel-line-start': 4655, + 'selection_end': 4655, + 'selection_start': 4655}, + loc('persistence/joke.py'): {'attrib-starts': [('J'\ + 'oke', + 18), + ('Joke.test_data', + 78)], + 'first-line': 50, + 'sel-line': 79, + 'sel-line-start': 2386, + 'selection_end': 2429, + 'selection_start': 2425}, + loc('persistence/jokemachinestate.py'): {'attrib-s'\ + 'tarts': [('JokeMachineState', + 20), + ('JokeMachineState.test_data', + 57)], + 'first-line': 38, + 'sel-line': 69, + 'sel-line-start': 2153, + 'selection_end': 2184, + 'selection_start': 2178}}, + 'has-focus': True}, + 'open_files': [u'TODO', + u'gui/canvaslistbox.py', + u'pages/choose.py', + u'i18n.py', + u'persistence/jokemachinestate.py', + u'gui/page.py', + u'persistence/joke.py', + u'globals.py', + u'gui/theme.py']}, + 'split_percents': {0: 0.5}, + 'splits': 2, + 'tab_location': 'top', + 'user_data': {}}, + 'split_percents': {0: 0.70063694267515919}, + 'splits': 2, + 'tab_location': 'right', + 'user_data': {}}, + 'window-alloc': (0, + 0, + 1320, + 1120)}]} +guimgr.recent-documents = [loc('gui/theme.py'), + loc('globals.py'), + loc('persistence/joke.py'), + loc('gui/page.py'), + loc('persistence/jokemachinestate.py'), + loc('i18n.py'), + loc('TODO'), + loc('pages/choose.py')] +guimgr.visual-state = {loc('JokeMachineActivity.py'): {'attrib-starts': [('J'\ + 'okeMachineActivity', + 49), + ('JokeMachineActivity.write_file', + 345)], + 'first-line': 296, + 'sel-line': 352, + 'sel-line-start': 12664, + 'selection_end': 12669, + 'selection_start': 12669}, + loc('MANIFEST'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 23, + 'sel-line-start': 497, + 'selection_end': 515, + 'selection_start': 515}, + loc('TODO'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 23, + 'sel-line-start': 478, + 'selection_end': 605, + 'selection_start': 605}, + loc('activity/activity.info'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 2, + 'sel-line-start': 31, + 'selection_end': 87, + 'selection_start': 46}, + loc('activitysession.py'): {'attrib-starts': [('JokeM'\ + 'achineSession', + 15)], + 'first-line': 0, + 'sel-line': 15, + 'sel-line-start': 704, + 'selection_end': 704, + 'selection_start': 704}, + loc('globals.py'): {'attrib-starts': [('__globals', + 28), + ('__globals.set_activity_instance', + 46)], + 'first-line': 63, + 'sel-line': 47, + 'sel-line-start': 1440, + 'selection_end': 1499, + 'selection_start': 1499}, + loc('gui/canvasimage.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}, + loc('gui/canvaslistbox.py'): {'attrib-starts': [], + 'first-line': 16, + 'sel-line': 24, + 'sel-line-start': 808, + 'selection_end': 833, + 'selection_start': 833}, + loc('gui/frame.py'): {'attrib-starts': [('Frame', + 33), + ('Frame.__do_clicked_make', + 196)], + 'first-line': 128, + 'sel-line': 204, + 'sel-line-start': 6738, + 'selection_end': 6739, + 'selection_start': 6739}, + loc('gui/lessonplanwidget.py'): {'attrib-starts': [(''\ + 'LessonPlanWidget', + 21), + ('LessonPlanWidget.__init__', + 23)], + 'first-line': 10, + 'sel-line': 36, + 'sel-line-start': 1355, + 'selection_end': 1355, + 'selection_start': 1355}, + loc('gui/page.py'): {'attrib-starts': [('Page', + 47), + ('Page.make_imagebox', + 121)], + 'first-line': 40, + 'sel-line': 126, + 'sel-line-start': 4355, + 'selection_end': 4359, + 'selection_start': 4359}, + loc('gui/theme.py'): {'attrib-starts': [], + 'first-line': 60, + 'sel-line': 78, + 'sel-line-start': 2547, + 'selection_end': 2564, + 'selection_start': 2564}, + loc('i18n.py'): {'attrib-starts': [('LanguageComboBox', + 113), + ('LanguageComboBox.install', + 141)], + 'first-line': 103, + 'sel-line': 152, + 'sel-line-start': 5130, + 'selection_end': 5134, + 'selection_start': 5134}, + loc('locale/org.worldwideworkshop.jokemachine.pot'): {''\ + 'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 9, + 'selection_start': 9}, + loc('mesh/activitysession.py'): {'attrib-starts': [(''\ + 'JokeMachineSession', + 33), + ('JokeMachineSession.broadcast_joke_cb', + 167)], + 'first-line': 173, + 'sel-line': 190, + 'sel-line-start': 7053, + 'selection_end': 7061, + 'selection_start': 7061}, + loc('ontology/joke.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}, + loc('ontology/jokebook.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}, + loc('pages/choose.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 21, + 'sel-line-start': 767, + 'selection_end': 767, + 'selection_start': 767}, + loc('pages/cover.py'): {'attrib-starts': [('Cover', + 30), + ('Cover.__init__', + 32)], + 'first-line': 0, + 'sel-line': 36, + 'sel-line-start': 989, + 'selection_end': 1033, + 'selection_start': 1033}, + loc('pages/edit.py'): {'attrib-starts': [('PageSelect'\ + 'or', + 39), + ('PageSelector.page', + 94), + ('PageSelector.page.set', + 96)], + 'first-line': 75, + 'sel-line': 98, + 'sel-line-start': 3984, + 'selection_end': 4012, + 'selection_start': 4012}, + loc('pages/frame.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 702, + 'selection_start': 0}, + loc('pages/joke.py'): {'attrib-starts': [('Joke', + 89), + ('Joke.__do_clicked_answer', + 129)], + 'first-line': 31, + 'sel-line': 149, + 'sel-line-start': 5951, + 'selection_end': 5958, + 'selection_start': 5958}, + loc('pages/page.py'): {'attrib-starts': [('Page', + 10), + ('Page._make_textbox', + 22)], + 'first-line': 0, + 'sel-line': 33, + 'sel-line-start': 818, + 'selection_end': 865, + 'selection_start': 865}, + loc('pages/preview.py'): {'attrib-starts': [('Preview', + 33), + ('Preview.__init__', + 35)], + 'first-line': 6, + 'sel-line': 48, + 'sel-line-start': 1537, + 'selection_end': 1607, + 'selection_start': 1607}, + loc('pages/submit.py'): {'attrib-starts': [('Submit', + 80), + ('Submit.__do_clicked_submit', + 119)], + 'first-line': 61, + 'sel-line': 127, + 'sel-line-start': 4870, + 'selection_end': 4870, + 'selection_start': 4870}, + loc('pages/theme.py'): {'attrib-starts': [], + 'first-line': 36, + 'sel-line': 64, + 'sel-line-start': 2219, + 'selection_end': 2255, + 'selection_start': 2255}, + loc('persistence/joke'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}, + loc('persistence/joke.py'): {'attrib-starts': [('Joke', + 18), + ('Joke.test_data', + 78)], + 'first-line': 44, + 'sel-line': 93, + 'sel-line-start': 2703, + 'selection_end': 2705, + 'selection_start': 2705}, + loc('persistence/jokebook.py'): {'attrib-starts': [(''\ + 'Jokebook', + 18), + ('Jokebook.sound_blob', + 53)], + 'first-line': 0, + 'sel-line': 54, + 'sel-line-start': 1702, + 'selection_end': 1731, + 'selection_start': 1731}, + loc('persistence/jokemachine.py'): {'attrib-starts': [(''\ + 'JokeMachineState', + 18)], + 'first-line': 0, + 'sel-line': 18, + 'sel-line-start': 766, + 'selection_end': 788, + 'selection_start': 788}, + loc('persistence/jokemachinestate.py'): {'attrib-star'\ + 'ts': [('JokeMachineState', + 20), + ('JokeMachineState.jokebook', + 50)], + 'first-line': 16, + 'sel-line': 55, + 'sel-line-start': 1793, + 'selection_end': 1793, + 'selection_start': 1793}, + loc('persistence/unit.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}, + loc('setup.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 5, + 'sel-line-start': 127, + 'selection_end': 176, + 'selection_start': 176}, + loc('unit/persistence.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}, + loc('unit/test_persistence.py'): {'attrib-starts': [(''\ + 'dump', + 22)], + 'first-line': 0, + 'sel-line': 38, + 'sel-line-start': 1186, + 'selection_end': 1207, + 'selection_start': 1207}, + loc('unit/unit.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 18, + 'sel-line-start': 725, + 'selection_end': 758, + 'selection_start': 725}, + loc('util/audioplayer.py'): {'attrib-starts': [('Audi'\ + 'oPlayer', + 24), + ('AudioPlayer.raw', + 44), + ('AudioPlayer.raw.set', + 53)], + 'first-line': 50, + 'sel-line': 55, + 'sel-line-start': 1580, + 'selection_end': 1604, + 'selection_start': 1604}, + loc('util/decorators.py'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 19, + 'sel-line-start': 895, + 'selection_end': 895, + 'selection_start': 895}, + loc('util/journalpickler.py'): {'attrib-starts': [('J'\ + 'ournalPickler', + 18), + ('JournalPickler.dumps', + 33)], + 'first-line': 0, + 'sel-line': 36, + 'sel-line-start': 1316, + 'selection_end': 1334, + 'selection_start': 1334}, + loc('util/persistence.py'): {'attrib-starts': [('_is_'\ + 'persistent', + 73)], + 'first-line': 0, + 'sel-line': 78, + 'sel-line-start': 2866, + 'selection_end': 2898, + 'selection_start': 2898}, + loc('../poll-builder/poll.py'): {'attrib-starts': [(''\ + 'PollBuilder', + 141), + ('PollBuilder._select_canvas', + 294)], + 'first-line': 326, + 'sel-line': 359, + 'sel-line-start': 12121, + 'selection_end': 12389, + 'selection_start': 12121}, + loc('../../../../../../opt/wing/resources/builtin-pi-files/2.5/__builtin__.pi'): {''\ + 'attrib-starts': [], + 'first-line': 1049, + 'sel-line': 1076, + 'sel-line-start': 33540, + 'selection_end': 33548, + 'selection_start': 33540}, + loc('unknown: #1'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 4, + 'selection_start': 4}, + loc('unknown: #2'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}, + loc('unknown: #3'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 4, + 'selection_start': 4}, + loc('unknown: #4'): {'attrib-starts': [], + 'first-line': 0, + 'sel-line': 0, + 'sel-line-start': 0, + 'selection_end': 0, + 'selection_start': 0}} +search.replace-history = [u'self.__is_initiator', + u'state_pickle', + u'__telepathy_connection', + u'./JokeMachine.activity/', + u'RoundBox', + u'self.__tab', + u'joke_box', + u'__do_clicked_tab', + u'jokebuilder'] +search.search-history = [u'.png', + u'audio.', + u'images', + u'TODO', + u'do_child', + u'__page_', + u'page_class', + u'refresh', + u'self.__is_initiating', + u'is_shared', + u'__telepathy_initiating', + u'is_joining', + u'True', + u'border=1', + u'Add Joke', + u'logging', + u'pickle', + u'Trying', + u'SESSION', + u'IFACE'] diff --git a/JokeMachineActivity.py b/JokeMachineActivity.py new file mode 100644 index 0000000..d9f09be --- /dev/null +++ b/JokeMachineActivity.py @@ -0,0 +1,372 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import os +import logging +import gtk + +from gettext import gettext as _ + +import hippo +from sugar.activity import activity + +from globals import Globals +from gui.frame import Frame + +import pages.choose +import pages.cover +import pages.joke +import pages.submit + + +# Mesh +import telepathy +import telepathy.client +from dbus import Interface +from dbus.service import method, signal +from dbus.gobject_service import ExportedGObject +from sugar.presence.tubeconn import TubeConnection # deprecated ?! Gone from build >542 ? Ke ? +from sugar.presence import presenceservice + +from mesh.activitysession import JokeMachineSession, MESH_IFACE, MESH_PATH, MESH_SERVICE + +# needed to unpickle state from journal +from persistence.jokemachinestate import JokeMachineState + + + +class JokeMachineActivity(activity.Activity): + """Sugar activity for jokes + + The Joke Machine is a fiendishly clever device cooked up by the mad + scientists at the worldwide workshop for sharing jokes with your friends. + + If we have enough jokes we might even be able to make all the angry people + in our world collapse with giggles of helpless laughter! + """ + + def __init__(self, handle): + activity.Activity.__init__(self, handle) + + # customize theme + gtkrc = os.path.join(Globals.pwd, 'gtkrc') + if os.path.exists(gtkrc): + logging.debug("Loading resources from %s" % gtkrc) + gtk.rc_add_default_file(gtkrc) + settings = gtk.settings_get_default() + #gtk.rc_reset_styles(settings) + gtk.rc_reparse_all_for_settings(settings, True) + logging.debug("Loading resources DONE") + + Globals.set_activity_instance(self) + + logging.debug("Starting the Joke Machine activity") + + os.chdir(Globals.pwd) # required for i18n.py to work TODO -> You're not initting i8n properly dude! + + # toolbox + self.__toolbox = activity.ActivityToolbox(self) + self.set_toolbox(self.__toolbox) + + # main activity frame + self.__activity_frame = Frame() + vbox = gtk.VBox() + vbox.pack_start(self.__activity_frame) + vbox.show() + self.set_canvas(vbox) + self.show_all() + + # Initialize mesh ########################################################## + + # init Presence Service + self.__presence_service = presenceservice.get_instance() + try: + name, path = self.__presence_service.get_preferred_connection() + self.__telepathy_connection = telepathy.client.Connection(name, path) + self.__telepathy_initiating = None + except TypeError: + logging.debug('Presence service offline') + + # Buddy object for you + owner = self.__presence_service.get_owner() + Globals.set_owner(owner) + + self.__session = None # ???? self.poll_session + self.connect('shared', self.__do_activity_shared) + + + # Check if we're joining another instance + self.__is_initiator = True + if self._shared_activity is not None: + self.__is_initiator = False + logging.debug('shared: %s' % self._shared_activity.props.joined) + # We are joining the activity + logging.debug('Joined activity') + self.connect('joined', self.__do_activity_joined) + self._shared_activity.connect('buddy-joined', self.__do_buddy_joined) + self._shared_activity.connect('buddy-left', self.__do_buddy_left) + if self.get_shared(): + # We've already joined + self.__do_activity_joined() + else: + logging.debug('Created activity') + + # ########################################################################## + + # set default startup page if we're the initiator + if self.is_initiator: + self.set_page(pages.choose.Choose) + + + # Mesh Callbacks ############################################################# + + def __setup(self): + '''Setup the Tubes channel + Called from: __do_activity_shared, __do_activity_joined.''' + + if self._shared_activity is None: + logging.error('Failed to share or join activity') + return + + bus_name, conn_path, channel_paths = self._shared_activity.get_channels() + + # Work out what our room is called and whether we have Tubes already + room = None + tubes_chan = None + text_chan = None + for channel_path in channel_paths: + channel = telepathy.client.Channel(bus_name, channel_path) + htype, handle = channel.GetHandle() + if htype == telepathy.HANDLE_TYPE_ROOM: + # TODO - this log message throws an exception + #logging.debug('Found our room: it has handle# %d %s', + # handle, + # self.__telepathy_connection.InspectHandles(htype, [handle][0])) + logging.debug('Found our room: it has handle# %d' % handle) + room = handle + ctype = channel.GetChannelType() + if ctype == telepathy.CHANNEL_TYPE_TUBES: + logging.debug('Found our Tubes channel at %s', channel_path) + tubes_chan = channel + elif ctype == telepathy.CHANNEL_TYPE_TEXT: + logging.debug('Found our Text channel at %s', channel_path) + text_chan = channel + + if room is None: + logging.debug('Presence service did not create a room') + return + if text_chan is None: + logging.debug('Presence service did not create a text channel') + return + + # Make sure we have a Tubes channel - PS doesn't yet provide one + if tubes_chan is None: + logging.debug('Did not find our Tubes channel, requesting one...') + tubes_chan = self.__telepathy_connection.request_channel(telepathy.CHANNEL_TYPE_TUBES, + telepathy.HANDLE_TYPE_ROOM, + room, + True) + self.tubes_chan = tubes_chan + self.text_chan = text_chan + + tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal('NewTube', self._new_tube_cb) + + + def __do_activity_joined(self, activity): + pass + '''Callback for completion of joining the activity.''' + if not self._shared_activity: + return + + # Find out who's already in the shared activity: + for buddy in self._shared_activity.get_joined_buddies(): + logging.debug('Buddy %s is already in the activity' % buddy.props.nick) + + logging.debug('Joined an existing shared activity') + self.__telepathy_initiating = False + self.__setup() + + logging.debug('This is not my activity: waiting for a tube...') + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=self._list_tubes_reply_cb, + error_handler=self._list_tubes_error_cb) + + + def _new_tube_cb(self, id, initiator, type, service, params, state): + '''Callback for when we have a Tube.''' + logging.debug('New tube: ID=%d initator=%d type=%d service=%s params=%r state=%d', + id, initiator, type, service, params, state) + + if (type == telepathy.TUBE_TYPE_DBUS and service == MESH_SERVICE): + if state == telepathy.TUBE_STATE_LOCAL_PENDING: + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id) + + tube_conn = TubeConnection(self.__telepathy_connection, + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES], + id, + group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) + + logging.info('Starting a new JokeMachineSession') + self.__session = JokeMachineSession(tube_conn, self.__telepathy_initiating, self._get_buddy, self) + + + def _get_buddy(self, cs_handle): + """Get a Buddy from a channel specific handle.""" + logging.debug('Trying to find owner of handle %u...', cs_handle) + group = self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP] + my_csh = group.GetSelfHandle() + logging.debug('My handle in that group is %u', my_csh) + if my_csh == cs_handle: + handle = self.__telepathy_connection.GetSelfHandle() + logging.debug('CS handle %u belongs to me, %u', cs_handle, handle) + elif group.GetGroupFlags() & telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + handle = group.GetHandleOwners([cs_handle])[0] + logging.debug('CS handle %u belongs to %u', cs_handle, handle) + else: + handle = cs_handle + logging.debug('non-CS handle %u belongs to itself', handle) + assert handle != 0 + + name, path = self.__presence_service.get_preferred_connection() # TODO - make sure this does not cause bugs + + return self.__presence_service.get_buddy_by_telepathy_handle(name, + path, + handle) + + + def __do_buddy_joined(self, activity, buddy): + logging.debug('Buddy %s joined' % buddy.props.nick) + + + def __do_buddy_left(self, activity, buddy): + logging.debug('Buddy %s left' % buddy.props.nick) + + + def __do_activity_shared(self, activity): + '''Callback for completion of sharing of activity''' + logging.debug('The activity was shared') + + self.__telepathy_initiating = True + self.__setup() # TODO - more civilized name + + for buddy in self._shared_activity.get_joined_buddies(): + logging.debug('Buddy %s is already in the activity' % buddy.props.nick) + + self._shared_activity.connect('buddy-joined', self.__do_buddy_joined) + self._shared_activity.connect('buddy-left', self.__do_buddy_left) + + logging.debug('This is my activity: making a tube...') + id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(MESH_SERVICE, {}) + + + def _list_tubes_reply_cb(self, tubes): + for tube_info in tubes: + self._new_tube_cb(*tube_info) + + def _list_tubes_error_cb(self, e): + logging.error('ListTubes() failed: %s', e) + + + @property + def tube(self): + logging.debug('Getting tube for activity: %r', self.__session) + return self.__session # TODO rename :-) + + @property + def is_shared(self): + ret = self.__session is not None + logging.debug('Getting is_shared for activity: %r', ret) + return ret + + @property + def is_initiator(self): + '''True if I'm the one joining an activity which was shared by someone else''' + ret = self.__is_initiator + logging.debug('Getting is_initiator for activity: %r', ret) + return ret + + # ############################################################################ + + + def refresh(self): + '''reload the current page''' + page_class = self.__activity_frame.page_class + logging.debug('Refreshing Page %r' % page_class) + self.set_page(page_class) + + + # TODO -> Make generally cleverer + # TODO -> Cache constructed pages if necessary for performance + # TODO -> Handle multiple page constructor arguments + def set_page(self, page_class, *args): + page = page_class(*args) + self.__activity_frame.page = page + return page + + + + def read_file(self, file_path): + '''Callback to resume activity state from Journal''' + logging.debug('Reading file from datastore via Journal: %s' % file_path) + + # TODO - double check -> if I'm a shared activity, don't restore me + # TODO - this doesn't work here - not initted yet + #if not self.is_initiator: + # logging.debug('joining a shared activity - dont restore') + # return + + # read activity state from Journal + f = open(file_path, 'r') + pickle = f.read() + if len(pickle) == 0: + logging.debug('Activity.read_file() -> Journal has empty pickle - creating empty state') + activity_state = JokeMachineState().test_data() + else: + logging.debug('Unpickling state from Journal') + activity_state = JokeMachineState.loads(pickle) + f.close() + + # set Globals.ActivityState + Globals.set_activity_state(activity_state) + + + + def write_file(self, file_path): + '''Callback to persist activity state to Journal''' + + # TODO - double check -> if I'm a shared activity, don't persist me + # TODO - this doesn't work here - not initted yet + #if not self.is_initiator: + # logging.debug('joining a shared activity - dont persist') + # return + + if len(Globals.JokeMachineState.jokebooks) != 0: + logging.debug('Writing file to datastore via Journal: %s' % file_path) + # write activity state to journal + f = open(file_path, 'w') + pickle = Globals.JokeMachineState.dumps() + f.write(pickle) + f.close() + else: + logging.debug('nothing to persist') + + + + def close(self): + '''Called on activity close''' + logging.info('Exiting Activity. Performing cleanup...') + Globals.shutdown() + logging.info('Done') + activity.Activity.close(self) diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..cea6795 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,40 @@ +./JokeMachineActivity.py +./TODO +./globals.py +./gtkrc +./gui/__init__.py +./gui/canvaslistbox.py +./gui/frame.py +./gui/lessonplanwidget.py +./gui/page.py +./gui/theme.py +./i18n.py +./i18n_misc_strings.py +./resources/GameLogoCharacter.png +./resources/image.png +./resources/audio.png +./lessons/Introduction/default.abw +./lessons/Lesson 1/default.abw +./lessons/Lesson 2/default.abw +./lessons/Lesson 3/default.abw +./lessons/Lesson 4/default.abw +./locale/jokemachine.pot +./mesh/__init__.py +./mesh/activitysession.py +./pages/__init__.py +./pages/choose.py +./pages/cover.py +./pages/edit.py +./pages/joke.py +./pages/preview.py +./pages/submit.py +./persistence/__init__.py +./persistence/joke.py +./persistence/jokebook.py +./persistence/jokemachinestate.py +./setup.py +./util/__init__.py +./util/audioplayer.py +./util/decorators.py +./util/journalpickler.py +./util/persistence.py diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..1cabbd8 --- /dev/null +++ b/NEWS @@ -0,0 +1,5 @@ +JokeMachine-2.xo + + * Initial Import + + -- Antoine van Gelder Sun, 28 Oct 2007 diff --git a/TODO b/TODO new file mode 100644 index 0000000..cdaf90a --- /dev/null +++ b/TODO @@ -0,0 +1,121 @@ + +!!!! AIMING FOR OCTOBER 30 !!!! + + * Check in code & Trac component WAITING + * Mesh + * Bugs + * i18n + * Sample Data + * Memory + * Lesson Plans + + * Sound WORKABLE + ** check tempdir against a vmware img + + += Current = + +* Bugs +** Choose -> Delete not work + + +* Mesh +** Put text: Downloading... or somesuch when joining activity +** After submitted joke has been approved broadcast it back to participants so that it appears in their version of the jokebook DONE +** Do we want all changes in state initiator side broadcast ? A lot of work to do it... +** is_visible flag on jokes and jokebooks ? +** Do we want the initiator to see the jokes of folk who join ? Could be nice... could be confusing... +** Share icons getting left behind +** What's this with activities getting journalled as shared - and then not being visible when resumed ? +** Make mesh work! DONE +** Detect whether am sharer or sharee DONE +** Remove the crapload of UI functions if sharee DONE + += To Do = + +* Mesh +** Make mesh work! + + +* Check in code & Trac component WAITING +** My domains are either gone or down - wait a few hours, if not back up then + do the big txfer else do the big txfer THIS WEEK !! :-( + + +* GUI +** Not going back to read jokebooks from edit mode on build 613 ? +** Preview mode shows make jokebook button - shouldn't +** Prepend Cover on preview +** Submit a Joke button -> only for foreigners +** On read jokebook & empty -> "Edit My Jokes" -> Go to edit my jokes tab +** Jokebook Info -> Edit Jokebook Cover DONE +** Refactor Page.make_* infrastructure +*** Factor out CanvasText's and replace with a better make_text +*** Add a make_button as well +*** Refactor Page.make_imagebox & ObjectChooser code - it's squishy! +** Fix gtkrc for non-default frame states + + +* Multimedia - Images/Audio +** Filter ObjectChooser to only list images/audio -> https://dev.laptop.org/ticket/3060 +** Select a sound to play on Joke punchlines DONE +*** NOTE writing out sound_blob to temp file for now before handing to gstreamer +** fold image/image_blob into a single property? +** fold sound/sound_blob into a single property? + + +* Persistence +** Build# 613 - keep/save problems ? +** Some sample jokes +** Restore last known state of activity instead of starting from scratch when not started from Journal - YES +*** Chat to eben +** Create and set activity_version property for persistence.JokeMachineState.version +** Don't persist if there are no jokebooks +** Can we point to datastore objects rather than having to copy them into our + own datastructures - has disadvantage that deleting an object from the + datastore that's in use will delete it from the joke - also creates problems + when sharing... + + +* Memory +** If memory should become a problem... write pickle restored from journal out + to disk and extend Persistence interface to feed from disk rather than RAM + + +* i18n +** Do a test translation into Afrikaans +** Make language selection button work +** Double check that all strings are gettexted + + +* Lesson Plans +** Make Lesson Plans button work + + +* Unit Tests +** Split unit.py out into separate test cases + + + += Known Problems = + +* Sugar Versions +** Vanishing controls on Build 542.3 running on B2 hardware - bring up & hide developer terminal to reset +** gtk.TextViews have borders on Build 557 which shouldn't be there +** These problems are all fixed on Build >600 and sugar-jhbuild + +* Audio +** Audio b0rk3d on B2 Hardware w/ build 542.3 + +* Edit Jokebook -> Review Jokes +** Present&working, but no way to submit jokes to it as mesh code is still coming up :-) +** If you want to see some sample data with waiting submissions edit persistence/jokemachinestate.py -> test_data() -> num_jokebooks = a number bigger than zero + +* Bugs +** Activity Icon not being set to theme colors +** Language selection button not theming as it should DONE +*** We can also lose theme_widget seeing as we're using a custom gtkrc now + + + + diff --git a/activity/activity-jokemachine.svg b/activity/activity-jokemachine.svg new file mode 100644 index 0000000..a8c7c4a --- /dev/null +++ b/activity/activity-jokemachine.svg @@ -0,0 +1,32 @@ + + + + +]> + + + + + + + + + + + + + + diff --git a/activity/activity.info b/activity/activity.info new file mode 100644 index 0000000..f6b9c24 --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,9 @@ +[Activity] +name = Joke Machine +bundle_id = org.worldwideworkshop.JokeMachineActivity +service_name = org.worldwideworkshop.JokeMachineActivity +class = JokeMachineActivity.JokeMachineActivity +icon = activity-jokemachine +activity_version = 1 +host_version = 1 +show_launcher = 1 diff --git a/globals.py b/globals.py new file mode 100644 index 0000000..5cadaeb --- /dev/null +++ b/globals.py @@ -0,0 +1,118 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import os +import logging +from sugar.activity import activity +try: + from hashlib import sha1 +except ImportError: + # Python < 2.5 + from sha import new as sha1 + +from persistence.jokemachinestate import JokeMachineState + + +class __globals(object): + '''All applications have code that affects global state. These are the + globals for Jokemachine''' + + def __init__(self): + self.__pwd = activity.get_bundle_path() + self.__logo = 'resources/GameLogoCharacter.png' + self.__activity_state = None + self.__activity = None + + # owner + self.__owner = None + self.__owner_sha1 = None + + self.__temporary_filenames = [] + + + # convert all of these to @Property + def set_activity_instance(self, activity_instance): + logging.debug('setting actifity %r' % activity_instance) + self.__activity = activity_instance + + # TODO -> Should we refresh the GUI for this one ? + def set_activity_state(self, activity_state): + self.__activity_state = activity_state + + def set_owner(self, owner): + logging.debug('Owner is %s' % owner.props.nick) + self.__owner = owner + self.__owner_sha1 = sha1(owner.props.nick).hexdigest() + + @property + def pwd(self): + return self.__pwd + + @property + def tmpdir(self): + '''Temporary directory - currently this exists for the sole purpose of + having a place to dump sounds and images into so we don't have to keep + them in memory - don't know if this will still be valid under bitfrost, + don't know if sounds and images can be pulled directly out of the journal + when needing to be (dis)played''' + return '/tmp' #os.path.join(self.__pwd, 'tmp') + + @property + def logo(self): + return os.path.join(self.pwd, self.__logo) + + @property + def JokeMachineState(self): + if not self.__activity_state: + # Okay - When app is not run from Journal activity.read_file() is never + # called, which means we never call Globals.set_activity_state so we + # create a default state here: + logging.debug('Globals.JokeMachineState() - creating default data') + self.__activity_state = JokeMachineState().test_data() # TODO - implement JokeMachineState.new() + return self.__activity_state + + @property + def JokeMachineActivity(self): + if not self.__activity: + # todo log this!! + logging.debug('no activity set! - use Globals.set_activity_instance to register activity with Global manager') + return None + return self.__activity + + @property + def nickname(self): + return self.__owner.props.nick + + + # utility functions with global state + + def temporary_filename(self): + # TODO - remember these and delete them all on app exit + file_name = os.tempnam(self.tmpdir) + self.__temporary_filenames.append(file_name) + return file_name + + + def shutdown(self): + for filename in self.__temporary_filenames: + if os.path.exists(filename): + logging.info(' deleting temporary file: %s' % filename) + os.remove(filename) + + +Globals = __globals() + + \ No newline at end of file diff --git a/gtkrc b/gtkrc new file mode 100644 index 0000000..78cf7fd --- /dev/null +++ b/gtkrc @@ -0,0 +1,27 @@ +style "mamabutton" +{ + color["focus_line"] = "#027F01" + bg[NORMAL] = "#027F01" + bg[ACTIVE] = "#026002" + GtkButton::inner-border = { 10, 10, # left, right + 6, 6 } # top, bottom +} + +style "mamacombo" +{ + color["focus_line"] = "#027F01" + bg[NORMAL] = "#027F01" + bg[ACTIVE] = "#026002" +} + +style "fixshare" +{ + bg[NORMAL] = "#808080" +} + + +widget "*GtkButton*" style "mamabutton" +widget_class "**" style "mamacombo" +widget "*ComboBox*" style "fixshare" + + diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/gui/__init__.py diff --git a/gui/canvaslistbox.py b/gui/canvaslistbox.py new file mode 100644 index 0000000..feb21c0 --- /dev/null +++ b/gui/canvaslistbox.py @@ -0,0 +1,67 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import gtk +import hippo + +from sugar.graphics import style + +from gui import theme +from gui import page + + +# TODO- height seems bust +class CanvasListBox(hippo.CanvasWidget): + def __init__(self, width = 0, height = 0): + self._entries_div = hippo.CanvasBox() + + # props not set properly in constructor + self._entries_div.props.background_color=theme.COLOR_PAGE.get_int() + self._entries_div.props.spacing=style.DEFAULT_SPACING + self._entries_div.props.padding=10 + self._entries_div.props.orientation=hippo.ORIENTATION_VERTICAL + + # Munge it all up into something we can stick into a gtk.ScrolledWindow + canvas = hippo.Canvas() + canvas.set_root(self._entries_div) + canvas.show() + + hbox = gtk.HBox() + hbox.set_flags(gtk.HAS_FOCUS | gtk.CAN_FOCUS) + hbox.pack_start(canvas) + hbox.show() + + scroller = gtk.ScrolledWindow() + scroller.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + if width > 0 or height > 0: + scroller.set_size_request(width, height) # TODO -> Size according to _entries_div w/ a max_entries or somesuch ? + viewport = gtk.Viewport() + viewport.set_shadow_type(gtk.SHADOW_NONE) + viewport.add(hbox) + viewport.show() + scroller.add(viewport) + scroller.show() + + hippo.CanvasWidget.__init__(self, + widget=scroller, + padding=0, + spacing=0, + border=1, + border_color=theme.COLOR_DARK_GREEN.get_int()) + + + def append(self, entry): + self._entries_div.append(entry) + diff --git a/gui/frame.py b/gui/frame.py new file mode 100644 index 0000000..fcbd0de --- /dev/null +++ b/gui/frame.py @@ -0,0 +1,205 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import gobject +import gtk +import hippo +import logging + +from i18n import LanguageComboBox +from gettext import gettext as _ + +from globals import Globals + +from util.decorators import Property +from gui import theme + +import pages.choose +import pages.edit + +from persistence.jokebook import Jokebook + +class Frame(hippo.Canvas): + + def __init__(self): + hippo.Canvas.__init__(self) + + # Root Frame ############################################################### + # Holds: Everything + self.__root = hippo.CanvasBox( + orientation=hippo.ORIENTATION_VERTICAL) + self.set_root(self.__root) + + # Application Header ####################################################### + # Holds: App logo, language box, lessons plan box + self.__header = self.__make_header() + self.__root.append(self.__header) + + # Page Container ########################################################### + # Holds: The currently open UI page + self.__container = hippo.CanvasBox( + border=theme.BORDER_WIDTH, + border_color=theme.COLOR_FRAME.get_int(), + background_color=theme.COLOR_BACKGROUND.get_int(), + spacing=4, + padding_top=20, + padding_left=40, + padding_right=40, + padding_bottom=20, + orientation=hippo.ORIENTATION_VERTICAL) + self.__root.append(self.__container, hippo.PACK_EXPAND) + + self.__page = hippo.CanvasBox( + box_height=theme.PAGE_HEIGHT, # TODO -> Pull width/height from theme - and make sure all + # children pull their sizes relative to this box + background_color=theme.COLOR_PAGE.get_int(), + border=4, + border_color=theme.COLOR_PAGE_BORDER.get_int(), + spacing=8, + padding=20, + xalign=hippo.ALIGNMENT_CENTER, + orientation=hippo.ORIENTATION_VERTICAL) + self.__container.append(self.__page) + + self.__page_class = None + + # Application Footer ####################################################### + # Holds: Task buttons + self.__footer = self.__make_footer() + self.__container.append(self.__footer) + + + + def __make_header(self): + ret = hippo.CanvasBox( + orientation=hippo.ORIENTATION_HORIZONTAL) + + # logo + logo = gtk.Image() + logo.set_from_file(Globals.logo) + ret.append(hippo.CanvasWidget(widget=logo)) + + # language selection box + language = hippo.CanvasWidget( + background_color=theme.COLOR_BACKGROUND.get_int(), + border_top=theme.BORDER_WIDTH, + border_left=theme.BORDER_WIDTH, + border_color=theme.COLOR_FRAME.get_int(), + padding_top=12, + padding_bottom=12, + padding_left=100, + padding_right=100, + yalign=hippo.ALIGNMENT_CENTER, + orientation=hippo.ORIENTATION_VERTICAL) + button = LanguageComboBox() + button.install() + language.props.widget = button + button.set_name('Fubar') + ret.append(language, hippo.PACK_EXPAND) + + # lesson plans + lesson_plans = hippo.CanvasWidget( + background_color=theme.COLOR_BACKGROUND.get_int(), + border_top=theme.BORDER_WIDTH, + border_left=theme.BORDER_WIDTH, + border_right=theme.BORDER_WIDTH, + border_color=theme.COLOR_FRAME.get_int(), + padding_top=12, + padding_bottom=12, + padding_left=30, + padding_right=30, + yalign=hippo.ALIGNMENT_CENTER, + orientation=hippo.ORIENTATION_VERTICAL) + button = gtk.Button(_('Lesson Plans')) + lesson_plans.props.widget = theme.theme_widget(button) + ret.append(lesson_plans, hippo.PACK_EXPAND) + + return ret + + + + def __make_footer(self): + ret = hippo.CanvasBox( + padding_right=8, + padding_top=8, + padding_bottom=0, + spacing=8, + orientation=hippo.ORIENTATION_HORIZONTAL) + button = gtk.Button(_('Read Jokebooks')) + button.connect('clicked', self.__do_clicked_read) + self.__button_read = hippo.CanvasWidget(widget=theme.theme_widget(button)) + ret.append(self.__button_read) + button = gtk.Button(_('Make Jokebook')) + button.connect('clicked', self.__do_clicked_make) + self.__button_make = hippo.CanvasWidget(widget=theme.theme_widget(button)) + ret.append(self.__button_make) + return ret + + + + @property + def page_class(self): + if self.__page_class is None: + # say, for e.g. we're connecting to another activity and we haven't set a + # default page yet + self.__page_class = pages.choose.Choose + return self.__page_class + + + @Property + def page(): + def get(self): return self.__page + def set(self, value): + self.__page_class = type(value) + self.__page.clear() + self.__page.append(value) + + # some rules for the buttons in the footer + if not Globals.JokeMachineActivity.is_initiator \ + and type(value) is pages.choose.Choose: + self.__button_read.set_visible(False) + self.__button_make.set_visible(False) + elif not Globals.JokeMachineActivity.is_initiator: + self.__button_read.set_visible(True) + self.__button_make.set_visible(False) + elif type(value) is pages.choose.Choose: + self.__button_read.set_visible(False) + self.__button_make.set_visible(True) + elif type(value) is pages.edit.Edit: + self.__button_read.set_visible(True) + self.__button_make.set_visible(False) + elif type(value) is pages.preview.Preview: + self.__button_read.set_visible(True) + self.__button_make.set_visible(False) + else: + self.__button_read.set_visible(True) + self.__button_make.set_visible(False) + + + + def __do_clicked_read(self, button): + Globals.JokeMachineActivity.set_page(pages.choose.Choose) + + + + def __do_clicked_make(self, button): + # create a new jokebook + jokebook = Jokebook() + jokebook.id = Globals.JokeMachineState.next_jokebook_id + logging.info('Created new jokebook with id: %d' % jokebook.id) + jokebook.owner = Globals.nickname + Globals.JokeMachineState.jokebooks.append(jokebook) + Globals.JokeMachineActivity.set_page(pages.edit.Edit, jokebook) + \ No newline at end of file diff --git a/gui/lessonplanwidget.py b/gui/lessonplanwidget.py new file mode 100644 index 0000000..b9a1a62 --- /dev/null +++ b/gui/lessonplanwidget.py @@ -0,0 +1,59 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import locale +import gtk + +from abiword import Canvas as AbiCanvas + + +class LessonPlanWidget(gtk.Notebook): + + def __init__ (self, basepath): + """Create a Notebook widget for displaying lesson plans in tabs. + + basepath -- string, path of directory containing lesson plans. + """ + super(LessonPlanWidget, self).__init__() + lessons = filter(lambda x: os.path.isdir(os.path.join(basepath, 'lessons', x)), + os.listdir(os.path.join(basepath, 'lessons'))) + lessons.sort() + for lesson in lessons: + self._load_lesson(os.path.join(basepath, 'lessons', lesson), + _(lesson)) + + + def _load_lesson (self, path, name): + """Load the lesson content from a .abw, taking l10n into account. + + path -- string, path of lesson plan file, e.g. lessons/Introduction + lesson -- string, name of lesson + """ + code, encoding = locale.getdefaultlocale() + canvas = AbiCanvas() + canvas.show() + files = map(lambda x: os.path.join(path, '%s.abw' % x), + ('_'+code.lower(), '_'+code.split('_')[0].lower(), + 'default')) + files = filter(lambda x: os.path.exists(x), files) + # On jhbuild, the first works, on XO image 432 the second works: + try: + canvas.load_file('file://%s' % files[0], 'text/plain') + except: + canvas.load_file('file://%s' % files[0]) + canvas.view_online_layout() + canvas.zoom_width() + canvas.set_show_margin(False) + self.append_page(canvas, gtk.Label(name)) diff --git a/gui/page.py b/gui/page.py new file mode 100644 index 0000000..54dd596 --- /dev/null +++ b/gui/page.py @@ -0,0 +1,357 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import os +import gtk +import cairo # for getting png for CanvasImage +import pango +import hippo +import logging +import StringIO + +from gettext import gettext as _ + +from sugar.graphics import style +from sugar.graphics.objectchooser import ObjectChooser + +# argh! +try: + from sugar.graphics.roundbox import RoundBox +except ImportError: + try: + from sugar.graphics.roundbox import CanvasRoundBox as RoundBox + except ImportError: + from sugar.graphics.canvasroundbox import CanvasRoundBox as RoundBox + + +from util.persistence import Persistent, PersistentProperty + +from util.audioplayer import AudioPlayer + + +from gui import theme +from globals import Globals + + +class Page(hippo.CanvasBox): + + def __init__(self, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + + self.__alternate_color_listrows = False + self.__color_listrow = theme.COLOR_LIST_ROW_ALT.get_int() + + + def append(self, item, **kwargs): + hippo.CanvasBox.append(self, item, **kwargs) + + + @property + def color_listrow(self): + if not self.__alternate_color_listrows: + return theme.COLOR_LIST_ROW.get_int() + if self.__color_listrow == theme.COLOR_LIST_ROW_ALT.get_int(): + self.__color_listrow = theme.COLOR_LIST_ROW.get_int() + else: + self.__color_listrow = theme.COLOR_LIST_ROW_ALT.get_int() + return self.__color_listrow + + + def make_listrow(self, contents = None): + list_row = RoundBox() + list_row.props.border = 0 # properties not being set properly by constructor + list_row.props.padding = theme.DEFAULT_PADDING + list_row.props.padding_right=0 + list_row.props.background_color = self.color_listrow + if contents is not None: + list_row.append(contents) + return list_row + + + def make_audiobox(self, obj, property, width): + + image_file = os.path.join(Globals.pwd, theme.AUDIO_CHOOSE) + if not os.path.exists(image_file): + logging.debug('cannot find %s' % image_file) + return hippo.CanvasBox() + + surface = cairo.ImageSurface.create_from_png(image_file) + preview_sound = hippo.CanvasImage(image=surface, + xalign=hippo.ALIGNMENT_START, + yalign=hippo.ALIGNMENT_CENTER) + preview_sound.connect('button-press-event', self.__do_clicked_preview_sound, obj, property) + + if hasattr(obj, property) and getattr(obj, property) != None: + sound_name = getattr(obj, property) + else: + sound_name = _('Click to choose a sound') + + choose_sound = hippo.CanvasText(text=sound_name, + xalign=hippo.ALIGNMENT_START, + font_desc=theme.FONT_BODY.get_pango_desc()) + choose_sound.connect('button-press-event', self.__do_clicked_choose_sound, obj, property) + + sound_box = RoundBox() + sound_box.props.padding = 2 + sound_box.props.spacing = 10 + sound_box.props.box_width = width + sound_box.props.border=theme.BORDER_WIDTH_CONTROL / 2 + sound_box.props.border_color=theme.COLOR_DARK_GREEN.get_int() + sound_box.props.orientation=hippo.ORIENTATION_HORIZONTAL + sound_box.props.xalign=hippo.ALIGNMENT_START + sound_box.append(preview_sound) + sound_box.append(choose_sound) + + deglitch_box = hippo.CanvasBox(xalign=hippo.ALIGNMENT_START, box_width=width) + deglitch_box.append(sound_box) + return deglitch_box + + + def make_imagebox(self, obj, property = None, width=-1, height=-1, editable=True, padding=0): + image = self.__get_property_value(obj, property) + if image == '' or image == None: + image = theme.IMAGE_CHOOSE + file_name = os.path.join(Globals.pwd, image) + logging.debug('make_imagebox(%r)' % file_name) + + # TODO -> handle landscape/portrait properly + + # load image - could be cleaner on the whole... :) + try: + if hasattr(obj, 'image_blob') and getattr(obj, 'image_blob') is not None: + image_file = StringIO.StringIO(obj.image_blob) + surface = cairo.ImageSurface.create_from_png(image_file) + else: + surface = cairo.ImageSurface.create_from_png(file_name) + except Exception, e: + logging.error('Error while loading image: %r' % e) + + # set border + if editable: + border_width = 0 + else: + border_width = theme.BORDER_WIDTH_IMAGE + + # the image itself + cover_image = hippo.CanvasImage(image=surface, + border=border_width, + border_color=theme.COLOR_BLACK.get_int(), + xalign=hippo.ALIGNMENT_CENTER, + yalign=hippo.ALIGNMENT_CENTER, + scale_width=width, + scale_height=height) + if editable: + cover_image.set_clickable(True) + cover_image.connect('button-press-event', self.__do_clicked_image, obj, 'image_blob') + image_box = RoundBox() + image_box.props.padding = 0 + image_box.props.spacing = 0 + image_box.props.border=theme.BORDER_WIDTH_CONTROL + image_box.props.border_color=theme.COLOR_DARK_GREEN.get_int() + image_box.append(cover_image) + else: + image_box = cover_image + + # Grrr... RoundedBoxes and CanvasImages expand their width to their parent + # unless they're in a CanvasBox + deglitch_box = hippo.CanvasBox(xalign=hippo.ALIGNMENT_CENTER, padding=padding) + deglitch_box.append(image_box) + return deglitch_box + + + def make_bodytext(self, text, width=-1, xalign = hippo.ALIGNMENT_START, text_color = theme.COLOR_BLACK): + return hippo.CanvasText(text=text, + size_mode=hippo.CANVAS_SIZE_WRAP_WORD, + box_width=width, + xalign=xalign, + color=text_color.get_int(), + font_desc=theme.FONT_BODY.get_pango_desc()) + + + def make_textbox(self, obj, property, width=300, height=100, editable=True): + value = self.__get_property_value(obj, property) + textbox = self.__textview(value, width, height, editable, True) + textbox.control.get_buffer().connect('changed', self.__do_changed_control, obj, property) + return textbox + + + def make_field(self, label, label_width, obj, property, field_width, editable=True): + value = self.__get_property_value(obj, property) + field_box = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + xalign=hippo.ALIGNMENT_START, + spacing=10) + field_box.append(hippo.CanvasText(text=label, + box_width=label_width, + xalign=hippo.ALIGNMENT_START, + color=theme.COLOR_DARK_GREEN.get_int(), + font_desc=theme.FONT_BODY.get_pango_desc())) + #if editable: + textfield = self.__textview(value, field_width, -1, editable, False) + textfield.control.get_buffer().connect('changed', self.__do_changed_control, obj, property) + field_box.append(textfield) + #else: # TODO - move to __textview() + #glitch_box = CanvasBox(box_width=field_width) + #glitch_box.append(hippo.CanvasText(text=value, + #size_mode=hippo.CANVAS_SIZE_WRAP_WORD, + #box_width=field_width, + #xalign=hippo.ALIGNMENT_START, + #font_desc=theme.FONT_BODY.get_pango_desc())) + #field_box.append(glitch_box) + return field_box + + + # Refactor into a CanvasTextView class + # TODO: Implement editable and multiline + # TODO: Lose multiline and change height variable to num_lines + def __textview(self, text, width=300, height=-1, editable=True, multiline=False): + textview = gtk.TextView() + textview.get_buffer().set_text(text) + + # control props + textview.set_wrap_mode(gtk.WRAP_WORD) + font = font_desc=theme.FONT_TEXTBOX.get_pango_desc() + font.set_weight(pango.WEIGHT_LIGHT) + textview.modify_font(font) + textview.modify_base(gtk.STATE_NORMAL, theme.COLOR_TEXTBOX.get_gdk_color()) + textview.set_editable(editable) + textview.set_cursor_visible(editable) + if height == -1: + context = textview.create_pango_context() + layout = pango.Layout(context) + layout.set_font_description(font) # TODO theme.FONT_BODY should be a pango.FontDescription, not a string + layout.set_text(text[ : text.find('\n')]) + (w, h) = layout.get_pixel_size() + height = h #+ theme.BORDER_WIDTH_CONTROL / 2 # fudge factor - on the XO-1 hardware all known solutions evaporate + textview.set_size_request(width, height) + textview.set_border_window_size(gtk.TEXT_WINDOW_LEFT, 0) + textview.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, 0) + textview.set_border_window_size(gtk.TEXT_WINDOW_TOP, 0) + textview.set_border_window_size(gtk.TEXT_WINDOW_BOTTOM, 0) + textview.show() + + if editable: # because rounded corners are well... pretty + border_box = RoundBox() + border_box.control = textview + border_box.props.padding = 2 + border_box.props.spacing = 0 + border_box.props.border=theme.BORDER_WIDTH_CONTROL / 2 + border_box.props.border_color=theme.COLOR_DARK_GREEN.get_int() + border_box.props.background_color=theme.COLOR_TEXTBOX.get_int() + border_box.props.xalign=hippo.ALIGNMENT_START + #border_box.props.box_width = width + #border_box.props.box_height = height + + # TODO - File bug: RoundBox seriously messes with TextView's + # (and other things) width !! + deglitch_box = hippo.CanvasBox() + deglitch_box.append(hippo.CanvasWidget(widget=textview)) + border_box.append(deglitch_box) + return border_box + + no_edit_box = hippo.CanvasWidget(widget=textview) + no_edit_box.control = textview + return no_edit_box + + + def __get_property_value(self, obj, property): + # TODO - Clean entire Model/View mechanism up so that we're not + # passing objects and text properties around at all + if obj is None: + return '' + if type(obj) is str: + return obj + if hasattr(obj, '__metaclass__') and obj.__metaclass__ is Persistent and hasattr(obj, property): + value = getattr(obj, property) + if value is None: + return '' + return value + logging.debug('__get_property_value error: Unknown object type %r', type(obj)) + return obj + + + def __do_changed_control(self, control, obj, property): + '''Update object model with control values''' + if hasattr(obj, property) and hasattr(control.props, 'text'): + setattr(obj, property, control.props.text) + else: + print 'NO PROPERTY OR TEXT' + + + def __do_clicked_image(self, control, event, obj, property): + # Courtesy of Write.activity - toolbar.py + chooser = ObjectChooser(_('Choose image'), + Globals.JokeMachineActivity, #._parent, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT) + try: + result = chooser.run() + if result == gtk.RESPONSE_ACCEPT: + logging.debug('ObjectChooser: %r' % chooser.get_selected_object()) + journal_object = chooser.get_selected_object() + if hasattr(obj, 'image_blob') and journal_object and journal_object.file_path: + logging.debug('Getting journal object: %r, %s', journal_object, journal_object.file_path) + # Set the image now + f = open(journal_object.file_path, 'r') + raw = f.read() + f.close() + obj.image = str(journal_object.metadata['title']) + obj.image_blob = raw + # refresh the image + image_file = StringIO.StringIO(obj.image_blob) + surface = cairo.ImageSurface.create_from_png(image_file) + control.props.image = surface + finally: + chooser.hide() + chooser.destroy() + del chooser + + + def __do_clicked_choose_sound(self, control, event, obj, property): + logging.debug('choosing sound file') + chooser = ObjectChooser(_('Choose Sound'), + Globals.JokeMachineActivity, #._parent, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT) + try: + result = chooser.run() + if result == gtk.RESPONSE_ACCEPT: + logging.debug('ObjectChooser: %r' % chooser.get_selected_object()) + journal_object = chooser.get_selected_object() + if hasattr(obj, 'sound_blob') and journal_object and journal_object.file_path: + logging.debug('Getting journal object: %r, %s, %s', journal_object, journal_object.file_path, journal_object.metadata['title']) + # Set the sound now + f = open(journal_object.file_path, 'r') + raw = f.read() + f.close() + obj.sound = str(journal_object.metadata['title']) + obj.sound_blob = raw + control.props.text = obj.sound + finally: + chooser.hide() + chooser.destroy() + del chooser + + + def __do_clicked_preview_sound(self, control, event, obj, property): + + if not hasattr(obj, 'sound_blob') or getattr(obj, 'sound_blob') == None: + logging.debug('No sound to preview') + return + + player = AudioPlayer() + #player.uri = sound_file + player.raw = obj.sound_blob + player.play() + + + \ No newline at end of file diff --git a/gui/theme.py b/gui/theme.py new file mode 100644 index 0000000..9e3d393 --- /dev/null +++ b/gui/theme.py @@ -0,0 +1,159 @@ +import gtk + +from sugar.graphics import style + + +# This is all rather horrible - but it will keep us afloat while Sugar +# stabilizes + + +# colors ####################################################################### + +COLOR_LIGHT_GREEN = style.Color('#66CC00') +COLOR_DARK_GREEN = style.Color('#027F01') +COLOR_PINK = style.Color('#FF0198') +COLOR_YELLOW = style.Color('#FFFF00') +COLOR_GRAY = style.Color('#ACACAC') +COLOR_LIGHT_GRAY = style.Color('#E2E2E3') +COLOR_RED = style.Color('#FF0000') +COLOR_WHITE = style.Color('#FFFFFF') +COLOR_BLACK = style.Color('#000000') +COLOR_BLUE = style.Color('#0000FF') + +# deprecated colors from style.COLOR_* +COLOR_PANEL_GREY = style.Color('#C0C0C0') +COLOR_TOOLBAR_GREY = style.Color('#404040') +COLOR_TEXT_FIELD_GREY = style.Color('#E5E5E5') + + +COLOR_FG_BUTTONS = ( + (gtk.STATE_NORMAL, style.Color('#CCFF99')), + (gtk.STATE_ACTIVE, style.Color('#CCFF99')), + (gtk.STATE_PRELIGHT, style.Color('#CCFF99')), + (gtk.STATE_SELECTED, style.Color('#CCFF99')), + (gtk.STATE_INSENSITIVE, style.Color('#CCFF99')), +) +COLOR_BG_BUTTONS = ( + (gtk.STATE_NORMAL, style.Color('#027F01')), + (gtk.STATE_ACTIVE, style.Color('#014D01')), + (gtk.STATE_PRELIGHT, style.Color('#016D01')), + (gtk.STATE_SELECTED, style.Color('#027F01')), + (gtk.STATE_INSENSITIVE, style.Color('#027F01')), +) +COLOR_BG_RADIOBUTTONS = ( + (gtk.STATE_NORMAL, COLOR_LIGHT_GRAY), + (gtk.STATE_ACTIVE, COLOR_LIGHT_GRAY), + (gtk.STATE_PRELIGHT, COLOR_LIGHT_GRAY), + (gtk.STATE_SELECTED, COLOR_LIGHT_GRAY), + (gtk.STATE_INSENSITIVE, COLOR_LIGHT_GRAY), +) +COLOR_FG_RADIOBUTTONS = ( + (gtk.STATE_NORMAL, COLOR_DARK_GREEN), + (gtk.STATE_ACTIVE, COLOR_DARK_GREEN), + (gtk.STATE_PRELIGHT, COLOR_DARK_GREEN), + (gtk.STATE_SELECTED, COLOR_DARK_GREEN), + (gtk.STATE_INSENSITIVE, COLOR_DARK_GREEN), +) + + +# ui elements ################################################################## + +COLOR_BACKGROUND = COLOR_LIGHT_GREEN +COLOR_FRAME = COLOR_YELLOW +COLOR_PAGE = COLOR_WHITE +COLOR_PAGE_BORDER = COLOR_PINK +COLOR_LIST_BACKGROUND = COLOR_PANEL_GREY +COLOR_LIST_BORDER = COLOR_TOOLBAR_GREY +COLOR_LIST_ROW = COLOR_TEXT_FIELD_GREY +COLOR_LIST_ROW_ALT = COLOR_GRAY +COLOR_TEXTBOX = COLOR_PAGE +COLOR_LINK = COLOR_BLUE +COLOR_TAB_ACTIVE = COLOR_DARK_GREEN +COLOR_TAB_INACTIVE = COLOR_LIGHT_GREEN +COLOR_TAB_SEPERATOR = COLOR_LIGHT_GRAY +COLOR_TAB_TEXT = COLOR_WHITE + + +# constants #################################################################### + +zoom = style.zoom + +PAGE_HEIGHT = style.zoom(635) # don't ask +TABS_HEIGHT = style.zoom(480) # 465 450 +PREVIEW_HEIGHT = style.zoom(519) # 564 + +PADDING_TAB = style.zoom(6) +DEFAULT_PADDING = style.zoom(6) +DEFAULT_SPACING = style.zoom(8) +BORDER_WIDTH = style.zoom(6) +BORDER_WIDTH_CONTROL = style.zoom(12) +BORDER_WIDTH_IMAGE = style.zoom(1) +SPACER_VERTICAL = style.zoom(20) +SPACER_HORIZONTAL = style.zoom(20) + +# fonts ######################################################################## + +#FONT_SIZE = zoom(7 * _XO_DPI / _get_screen_dpi()) +#FONT_NORMAL = Font('Bitstream Vera Sans %d' % FONT_SIZE) +#_XO_DPI = 200.0 +FONT_SIZE_LARGE = style.zoom(10 * style._XO_DPI / style._get_screen_dpi()) +FONT_LARGE = style.Font('Bitstream Vera Sans %d' % FONT_SIZE_LARGE) + +#FONT_LARGE = style.FONT_NORMAL #'Sans 18' +FONT_BODY = style.FONT_NORMAL #'Sans 14' +FONT_BODY_BOLD = style.FONT_BOLD #'Sans Bold 14' +FONT_TABS = style.FONT_NORMAL #'Sans 12' +FONT_TEXTBOX = style.FONT_NORMAL #'Sans 10' + + +# images ####################################################################### + +IMAGE_CHOOSE = 'resources/image.png' +AUDIO_CHOOSE = 'resources/audio.png' + + +# helpers ###################################################################### + +# TODO - deprecate in favor of gtkrc +def theme_widget(widget, width=-1, height=-1, highlight=False): + """Apply colors to gtk Widgets + + widget is the widget + width, height are optional width and height for resizing the widget + highlight is a boolean to override the theme and apply a + different color to show "you are here". + + returns the modified widget. + """ + + return widget + + #if widget == None: + #print 'theme.theme_widget(widget=None) !' + #return + + #for state, color in COLOR_BG_BUTTONS: + #if highlight: + #widget.modify_bg(state, gtk.gdk.color_parse("#CCFF99")) + #else: + #widget.modify_bg(state, color.get_gdk_color()) + + #if hasattr(widget, 'get_child'): + #c = widget.get_child() + #if c is not None: + #for state, color in COLOR_FG_BUTTONS: + #if highlight: + #c.modify_fg(state, COLOR_DARK_GREEN.get_gdk_color()) + #else: + #c.modify_fg(state, color.get_gdk_color()) + #else: + #for state, color in COLOR_FG_BUTTONS: + #widget.modify_fg(state, color.get_gdk_color()) + + #if width > 0 or height > 0: + #widget.set_size_request(width, height) + + #return widget + + + diff --git a/i18n.py b/i18n.py new file mode 100644 index 0000000..ce26f5c --- /dev/null +++ b/i18n.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +### SliderPuzzeUI +### TODO: Describe +### $Id: $ +### +### author: Carlos Neves (cn (at) sueste.net) +### (c) 2007 World Wide Workshop Foundation + +import os +import gettext +import locale + +from gui import theme + +import gtk, gobject + +_ = lambda x: x + +# Images were taken from http://www.sodipodi.com/ +# except for korea taken from http://zh.wikipedia.org/wiki/Image:Unification_flag_of_Korea.svg + +lang_name_mapping = { + 'zh_cn':(None, _('Chinese (simplified)'), 'china'), + 'zh_tw':(None, _('Chinese (traditional)'), 'china'), + 'cs':(None, _('Czech'),'czech_republic'), + 'da':(None, _('Danish'),'denmark'), + 'nl':(None, _('Dutch'), 'netherlands'), + 'en':('English', _('English'),'united_states'), + 'en_gb':('English', _('English - Great Britain'),'united_kingdom'), + 'en_us':('English', _('English - U.S.'),'united_states'), + 'fi':(None, _('Finnish'),'finland'), + 'fr':('Français', _('French'),'france'), + 'de':(None, _('German'),'germany'), + 'hu':(None, _('Hungarian'),'hungary'), + 'it':(None, _('Italian'),'italy'), + 'ja':(None, _('Japanese'),'japan'), + 'ko':(None, _('Korean'),'korea'), + 'no':(None, _('Norwegian'),'norway'), + 'pl':(None, _('Polish'),'poland'), + 'pt':('Português', _('Portuguese'),'portugal'), + 'pt_br':('Português do Brasil', _('Portuguese - Brazilian'),'brazil'), + 'ru':(None, _('Russian'),'russian_federation'), + 'sk':(None, _('Slovak'),'slovenia'), + 'es':('Español', _('Spanish'),'spain'), + 'sv':(None, _('Swedish'),'sweden'), + 'tr':(None, _('Turkish'),'turkey'), +} + +class LangDetails (object): + def __init__ (self, code, name, image): + self.code = code + self.country_code = self.code.split('_')[0] + self.name = name + self.image = image + + def guess_translation (self, fallback=False): + locale_dir = os.path.join(os.getcwd(), 'locale') + domain = 'jokemachine' + self.gnutranslation = gettext.translation(domain, locale_dir, [self.code], fallback=fallback) + + def install (self): + self.gnutranslation.install() + + def matches (self, code, exact=True): + if code is None or self.code is None: + return False + if exact: + return code.lower() == self.code.lower() + return code.split('_')[0].lower() == self.country_code.lower() + +def get_lang_details (lang): + mapping = lang_name_mapping.get(lang.lower(), None) + if mapping is None: + # Try just the country code + lang = lang.split('_')[0] + mapping = lang_name_mapping.get(lang.lower(), None) + if mapping is None: + return None + if mapping[0] is None: + return LangDetails(lang, mapping[1], mapping[2]) + return LangDetails(lang, mapping[0], mapping[2]) + +def list_available_translations (): + rv = [get_lang_details('en'), get_lang_details('fr'), get_lang_details('es')] + rv[0].guess_translation(True) + for i,x in enumerate([x for x in os.listdir('locale') if os.path.isdir('locale/' + x) and not x.startswith('.')]): + try: + details = get_lang_details(x) + if details is not None: + details.guess_translation() + rv.append(details) + except: + raise + return rv + +class LanguageComboBox (gtk.ComboBox): + def __init__ (self): + liststore = gtk.ListStore(gobject.TYPE_STRING) + gtk.ComboBox.__init__(self, liststore) + + self.cell = gtk.CellRendererText() + #self.cell.props.background_gdk = theme.COLOR_DARK_GREEN.get_gdk_color() + #self.cell.props.background_set = True + + self.pack_start(self.cell, True) + self.add_attribute(self.cell, 'text', 0) + + self.translations = list_available_translations() + for i,x in enumerate(self.translations): + liststore.insert(i+1, (gettext.gettext(x.name), )) + self.connect('changed', self.install) + + self.set_title('MaMaLanguageComboBox') + + + def modify_bg (self, state, color): + # AVG - cell.props.background not cell.background + cell.props.background = '#027F01' + self.cell.props.background_gdk = color + self.cell.props.background_set = True + #setattr(self.cell, 'background-gdk',color) + #setattr(self.cell, 'background-set',True) + + def install (self, *args): + if self.get_active() > -1: + self.translations[self.get_active()].install() + else: + code, encoding = locale.getdefaultlocale() + # Try to find the exact translation + for i,t in enumerate(self.translations): + if t.matches(code): + self.set_active(i) + break + if self.get_active() < 0: + # Failed, try to get the translation based only in the country + for i,t in enumerate(self.translations): + if t.matches(code, False): + self.set_active(i) + break + if self.get_active() < 0: + # nothing found, select first translation + self.set_active(0) + # Allow for other callbacks + return False + +### +def gather_other_translations (): + from glob import glob + entries = filter(lambda x: os.path.isdir(x), glob('resources/*')) + entries.extend(filter(lambda x: os.path.isdir(x), glob('lessons/*'))) + entries = map(lambda x: os.path.basename(x), entries) + f = file('i18n_misc_strings.py', 'w') + for e in entries: + f.write('_("%s")\n' % e) + f.close() + +if __name__ == '__main__': + gather_other_translations() diff --git a/i18n_misc_strings.py b/i18n_misc_strings.py new file mode 100644 index 0000000..d31a70e --- /dev/null +++ b/i18n_misc_strings.py @@ -0,0 +1,5 @@ +_("Lesson 1") +_("Lesson 2") +_("Lesson 3") +_("Lesson 4") +_("Introduction") diff --git a/lessons/Introduction/default.abw b/lessons/Introduction/default.abw new file mode 100644 index 0000000..6fddc6a --- /dev/null +++ b/lessons/Introduction/default.abw @@ -0,0 +1,51 @@ + + + + + + + + + + + +application/x-abiword +AbiWord + + + + + + + + + + + + + + +
+

Poll Builder Lesson Plans Overview

+

+

A poll is a research tool that helps explain what a group of people think about a subject. First you present the group with a question and a set of answer choices. Then each person in the group gets to vote for the answer which best describes their personal feelings or opinion. After collecting votes from the whole group, you look for a pattern in their answers. You can also ask the same question of different groups (i.e. boys and girls or children and adults) and see how each group answers.

+

+

A poll can be used to help a community make a decision and take action. It can also be used to understand the similarities and differences among individuals and groups of people.

+

+

Polls are a helpful tool for any research project. Use this Poll tool and lesson plans with whatever subject you are studying. It can also be used to make collaborative decisions in the classroom about academic activities and among friends about social issues.

+

+

Skills:

+

Learn what a poll is and how it works.

+

Learn how to use polls to gather information and make decisions.

+

Practice choosing poll subjects.

+

Learn how to formulate good poll questions and answer choices.

+

Practice basic survey skills of data collection, analysis and presentation.

+

Learn how to use a bar graph to represent votes and opinions.

+

Learn how to represent poll results using ratios and percentages.

+

Learn how to help peers understand an idea or an opinion.

+

Learn from classmates.

+

Learn how to present poll results and make collaborative decisions based on those results.

+

Practice using new functions and features of the XO Laptop.

+

+
+
diff --git a/lessons/Lesson 1/default.abw b/lessons/Lesson 1/default.abw new file mode 100644 index 0000000..f9b3df0 --- /dev/null +++ b/lessons/Lesson 1/default.abw @@ -0,0 +1,52 @@ + + + + + + + + + + + +application/x-abiword +AbiWord + + + + + + + + + + + + + + +
+

Lesson 1: Participate in a Poll

+

+

From the Home screen of your XO, click on the MaMaMedia icon.

+

+

Open the Poll Builder activity from the MaMaMedia Activity Center.

+

+

Choose a Poll topic from the list, and click the “VOTE” button next to it.

+

+

A Poll question and answer choices will appear. Read them carefully, then click on your favorite answer. Click the “VOTE” button to submit your answer.

+

+

After you vote, current Poll results will appear. You will see the exact number of votes cast (so far) for each answer. You will also see a bar graph that shows which answer is currently the most popular among the whole group.

+

+

Look at the number of votes next to each answer. Which one has the most votes so far? How many people chose the same answer as you? Which answer do you think will win? Why?

+

+

Look at the bar graph. Which answer has the longest bar? What does that mean? Which answer has the shortest bar? What does that mean?

+

+

Let each person in your group vote in this poll. How did the votes and bar graphs change?

+

+

After everyone votes, discuss the final results. What did you learn from the poll? Were you surprised by the results? How could you use this information?

+

+

Generate a list of ideas for polls. Record your ideas on a blackboard, on paper or in the “Write” program on the XO Laptop. What would you like to learn from each poll you thought of? What would you do with the results?

+

+
+
diff --git a/lessons/Lesson 2/default.abw b/lessons/Lesson 2/default.abw new file mode 100644 index 0000000..404af7e --- /dev/null +++ b/lessons/Lesson 2/default.abw @@ -0,0 +1,55 @@ + + + + + + + + + + + +application/x-abiword +AbiWord + + + + + + + + + + + + + + +
+

Lesson 2: Build a Poll

+

+

Make a poll of your own! The goal is to build a poll that is interesting to you, and learn to collect votes from people and reach conclusions or make decisions based on your poll.

+

+

Example poll idea: “What is your favorite activity on the XO?”

+

You can use this poll in your classroom or your family, to find out what activity is the most favorite or popular.

+

+

Open the Poll Builder activity and click on the “Build a Poll” button at the bottom of the screen.

+

+

Fill in your poll details on the blank form that appears. Type in your poll title, question and answers, and the number of votes you want to collect.

+

+

Think about your “Poll Title” and “Question.” The title is the name of your poll, and the question is what you want to know. In our example poll, the title is “Favorite Food” and the question is “What is your favorite food?”

+

+

Next type in the number of votes you want to collect in your poll. This number is called a “sample size” by researchers. To do this correctly, think about how many people you want to poll. Is it all of the students in your class? Just the 12-year-olds? All of the people in your family? All of the women in your town?

+

+

Now type in the “Answer choices,” you want people to choose from. You can offer 2, 3, 4 or 5 answer choices in this poll. In our Favorite Food example, the answer choices could be: 1. Bread, 2. Rice, 3. Beans, 4. Candy, 5. Bananas.

+

+

Once you have filled in all of the “Build a Poll” fields, click “Step 1: Previewto see how your poll will look.

+

+

If you like how it looks, click “Save Poll.”

+

+

If you want to change something, click “Edit Poll.” You can change the information in any field.

+

+

When you are happy, click “Step 2: Save Poll” -- your poll will be built.

+

+
+
diff --git a/lessons/Lesson 3/default.abw b/lessons/Lesson 3/default.abw new file mode 100644 index 0000000..4643f92 --- /dev/null +++ b/lessons/Lesson 3/default.abw @@ -0,0 +1,42 @@ + + + + + + + + + + + +application/x-abiword +AbiWord + + + + + + + + + + + + + + +
+

Lesson 3: Collect Poll Data

+

+

Now that your poll is built, invite people to take your poll on the XO. Walk around with your laptop, and ask people to answer your research question.

+

+

Decide how you are going to get the information you need. You could read your question and the answer choices to each person and click their answer on your XO. Or, you can show each person how to select your poll from the list, read the poll by themselves and click on their answer choice by themselves.

+

+

For best results, each person should vote only once. The Poll will close when you reach the number of your “sample size.”

+

+

Try to collect votes from the people you originally thought about when you chose the “number of votes to collect.” If it was girls, ask only girls. Otherwise your results will not reflect the group you had in mind.

+

+

Remember, you can create the same poll again, and ask different kinds of people to answer each time. It will be very interesting to compare the results of the same question asked to different groups, such as boys and girls or adults and children.

+

+
+
diff --git a/lessons/Lesson 4/default.abw b/lessons/Lesson 4/default.abw new file mode 100644 index 0000000..1f0f57b --- /dev/null +++ b/lessons/Lesson 4/default.abw @@ -0,0 +1,46 @@ + + + + + + + + + + + +application/x-abiword +AbiWord + + + + + + + + + + + + + + +
+

Lesson 4: Use Data to make Observations and Decisions

+

+

After you have collected the poll data you needed from one or more groups, look at the results and think about what they mean.

+

+

Open the Poll Builder activity on your XO and find your poll title on the list. Click the “See Results” button to see how people voted.

+

+

Look at the Bar Graphs that represent the votes (results) of your poll.

+

Which answer has the longest bar? What does that mean? Which answer has the shortest bar? What does that mean?

+

+

If you tried your poll with different groups (girls, boys, teachers, students, adults, children, etc.) Open each poll and see if there are any differences.

+

+

Write down notes about what you see; put your findings on paper or record them with the “Write” activity on your XO. How does this kind of information help you draw conclusions about the people you surveyed? Does the data accurately represent an entire group? Does it matter if you questioned 25 people or 100 people? Why?

+

+

Present you findings to your class, or to the people who took the poll. Tell them what you learned for the poll and discuss the results together. Brainstorm about what could be done with this information.

+

+

+
+
diff --git a/locale/jokemachine.pot b/locale/jokemachine.pot new file mode 100644 index 0000000..6129dcc --- /dev/null +++ b/locale/jokemachine.pot @@ -0,0 +1,249 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2007-10-14 03:05+SAST\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: ENCODING\n" +"Generated-By: pygettext.py 1.5\n" + + +#: i18n.py:39 +msgid "Chinese (simplified)" +msgstr "" + +#: i18n.py:40 +msgid "Chinese (traditional)" +msgstr "" + +#: i18n.py:41 +msgid "Czech" +msgstr "" + +#: i18n.py:42 +msgid "Danish" +msgstr "" + +#: i18n.py:43 +msgid "Dutch" +msgstr "" + +#: i18n.py:44 +msgid "English" +msgstr "" + +#: i18n.py:45 +msgid "English - Great Britain" +msgstr "" + +#: i18n.py:46 +msgid "English - U.S." +msgstr "" + +#: i18n.py:47 +msgid "Finnish" +msgstr "" + +#: i18n.py:48 +msgid "French" +msgstr "" + +#: i18n.py:49 +msgid "German" +msgstr "" + +#: i18n.py:50 +msgid "Hungarian" +msgstr "" + +#: i18n.py:51 +msgid "Italian" +msgstr "" + +#: i18n.py:52 +msgid "Japanese" +msgstr "" + +#: i18n.py:53 +msgid "Korean" +msgstr "" + +#: i18n.py:54 +msgid "Norwegian" +msgstr "" + +#: i18n.py:55 +msgid "Polish" +msgstr "" + +#: i18n.py:56 +msgid "Portuguese" +msgstr "" + +#: i18n.py:57 +msgid "Portuguese - Brazilian" +msgstr "" + +#: i18n.py:58 +msgid "Russian" +msgstr "" + +#: i18n.py:59 +msgid "Slovak" +msgstr "" + +#: i18n.py:60 +msgid "Spanish" +msgstr "" + +#: i18n.py:61 +msgid "Swedish" +msgstr "" + +#: i18n.py:62 +msgid "Turkish" +msgstr "" + +#: i18n_misc_strings.py:1 +msgid "Lesson 1" +msgstr "" + +#: i18n_misc_strings.py:2 +msgid "Lesson 2" +msgstr "" + +#: i18n_misc_strings.py:3 +msgid "Lesson 3" +msgstr "" + +#: i18n_misc_strings.py:4 +msgid "Lesson 4" +msgstr "" + +#: i18n_misc_strings.py:5 +msgid "Introduction" +msgstr "" + +#: gui/frame.py:117 +msgid "Lesson Plans" +msgstr "" + +#: gui/frame.py:131 +msgid "Read Jokebooks" +msgstr "" + +#: gui/frame.py:134 +msgid "Make Jokebook" +msgstr "" + +#: gui/page.py:70 +msgid "Choose image" +msgstr "" + +#: pages/choose.py:95 +msgid "Edit" +msgstr "" + +#: pages/choose.py:100 +msgid "Delete" +msgstr "" + +#: pages/cover.py:37 +msgid "Jokes started by" +msgstr "" + +#: pages/cover.py:49 +msgid "Open" +msgstr "" + +#: pages/edit.py:53 +msgid "Jokebook Info" +msgstr "" + +#: pages/edit.py:63 +msgid "Edit My Jokes" +msgstr "" + +#: pages/edit.py:73 +msgid "Review Submitted Jokes" +msgstr "" + +#: pages/edit.py:121 +msgid "Preview" +msgstr "" + +#: pages/edit.py:146 +msgid "Title of Jokebook:" +msgstr "" + +#: pages/edit.py:147 +msgid "Sound Effect:" +msgstr "" + +#: pages/edit.py:170 +msgid "Add Joke" +msgstr "" + +#: pages/edit.py:200 +msgid "Status:" +msgstr "" + +#: pages/edit.py:207 +msgid "Approved" +msgstr "" + +#: pages/edit.py:210 +msgid "Rejected" +msgstr "" + +#: pages/edit.py:213 +msgid "Not Reviewed" +msgstr "" + +#: pages/joke.py:53 +msgid "Joke" +msgstr "" + +#: pages/joke.py:56 +msgid "By" +msgstr "" + +#: pages/joke.py:66 pages/submit.py:67 +msgid "Question" +msgstr "" + +#: pages/joke.py:75 pages/joke.py:105 pages/submit.py:75 +msgid "Answer" +msgstr "" + +#: pages/joke.py:126 +msgid "Next" +msgstr "" + +#: pages/joke.py:131 +msgid "Submit a Joke" +msgstr "" + +#: pages/submit.py:96 +msgid "Submission For:" +msgstr "" + +#: pages/submit.py:97 +msgid "Your Name:" +msgstr "" + +#: pages/submit.py:108 +msgid "Submit" +msgstr "" + +#: pages/submit.py:111 +msgid "Back" +msgstr "" + diff --git a/mesh/__init__.py b/mesh/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/mesh/__init__.py diff --git a/mesh/activitysession.py b/mesh/activitysession.py new file mode 100644 index 0000000..6c2b51b --- /dev/null +++ b/mesh/activitysession.py @@ -0,0 +1,254 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import logging + + +from dbus.service import method, signal +from dbus.gobject_service import ExportedGObject + + +from globals import Globals +from persistence.jokemachinestate import JokeMachineState +from persistence.joke import Joke + +MESH_SERVICE = 'org.worldwideworkshop.JokeMachine' +MESH_IFACE = MESH_SERVICE +MESH_PATH = '/org/worldwideworkshop/JokeMachine' + + + +class JokeMachineSession(ExportedGObject): + """The bit that talks over the TUBES!!!""" + + def __init__(self, tube, is_initiator, get_buddy, activity): + """Initialise the PollSession. + + tube -- TubeConnection + is_initiator -- boolean, True = we are sharing, False = we are joining + get_buddy -- function + activity -- JokeMachine (sugar.activity.Activity) + """ + super(JokeMachineSession, self).__init__(tube, MESH_PATH) + self.tube = tube + self.is_initiator = is_initiator + self.entered = False # Have we set up the tube? + self._get_buddy = get_buddy # Converts handle to Buddy object + self.activity = activity # JokeMachine + self.tube.watch_participants(self.participant_change_cb) + + + + # Signals and signal handlers ################################################ + + @signal(dbus_interface=MESH_IFACE, signature='') + def Hello(self): + """Signal to request that my UpdatePoll method is called to let me know about + other known polls.""" + + # a -> Say hello to incoming buddy and send him our state + def hello_cb(self, sender=None): + '''Tell the newcomer what's going on.''' + assert sender is not None + logging.debug('In hello_cb - Newcomer %s has joined and sent Hello', sender) + # sender is a bus name - check if it's me: + if sender == self.my_bus_name: + # then I don't want to respond to my own Hello + return + + logging.debug('Sending %s my state' % sender) + state_pickle = Globals.JokeMachineState.dumps() + logging.debug('PICKLE TYPE: %r (hello_cb)' % type(state_pickle)) + #state_pickle = ('This is my tubes message being sent to %s' % sender) + #logging.debug('PICKLE TYPE: %r (hello_cb)' % type(state_pickle)) + self.tube.get_object(sender, MESH_PATH).PumpActivityState(state_pickle, dbus_interface=MESH_IFACE) + + # Ask for other's jokes back + #self.HelloBack(sender) + + + + # b -> I am a buddy who be receiving some state + @method(dbus_interface=MESH_IFACE, + in_signature='s', + out_signature='') + def PumpActivityState(self, state_pickle): + state_pickle = str(state_pickle) + #logging.info('I JUST RECEIVED PICKLE TYPE: %r - %s (PumpActivityState)', type(state_pickle), state_pickle) + if len(state_pickle) == 0: + logging.debug('JokeMachineSession.ReceiveActivityState() -> empty state_pickle - creating empty state') + activity_state = JokeMachineState().test_data() + else: + logging.debug('JokeMachineSession.ReceiveActivityState() -> Unpickling state from remote') + activity_state = JokeMachineState.loads(state_pickle) + Globals.set_activity_state(activity_state) + + # refresh activity ui + Globals.JokeMachineActivity.refresh() + logging.debug('Finished receiving state') + + + # c -> I am the connecting buddy, I can send some state back here if I want + @signal(dbus_interface=MESH_IFACE, signature='s') + def HelloBack(self, recipient): + """Respond to Hello. + recipient -- string, sender of Hello. + """ + + def helloback_cb(self, recipient, sender): + """Reply to Hello. + + recipient -- string, the XO who send the original Hello. + + Other XOs should ignore this signal. + """ + logging.debug('*** In helloback_cb: recipient: %s, sender: %s' % + (recipient, sender)) + if sender == self.my_bus_name: + # Ignore my own signal + return + if recipient != self.my_bus_name: + # This is not for me + return + + # anything ? + + + # d -> I am the connecting buddy, I have a joke to submit to you + @signal (dbus_interface=MESH_IFACE, signature='us') + def Submit(self, jokebook_id, joke_pickle): + '''Submit a joke''' + + def submit_cb(self, jokebook_id, joke_pickle, sender=None): + '''Receive someones submission + jokebook_id -- the jokebook to submit joke to + joke_pickle -- a pickled joke''' + if sender == self.my_bus_name: # don't respond to own submit signal + return + logging.debug('In submit_cv. sender: %r' % sender) + + # 1. unpickle joke + joke_pickle = str(joke_pickle) + if len(joke_pickle) == 0: + logging.debug('JokeMachineSession.submit_cb() -> empty joke_pickle - doing nothing') + return + joke = Joke.loads(joke_pickle) + logging.debug('%s submitted a joke to my jokebook# %d with text: %s and answer %s', joke.joker, jokebook_id, joke.text, joke.answer) + + # 2. get the jokebook it belongs to + jokebook = Globals.JokeMachineState.jokebook(jokebook_id) + if jokebook is None: + logging.error('Joke was submitted to non-existent jokebook id %d', jokebook_id) + return + + # 3. add it to submissions in the appropriate jokebook + jokebook.submissions.append(joke) + + # 4. TODO - show some kind of alert - ask on #sugar + + + # e -> I am the initiator, I've just accepted a submission, tell everyone! + @signal (dbus_interface=MESH_IFACE, signature='us') + def BroadcastJoke(self, jokebook_id, joke_pickle): + '''broadcast newly accepted submission back to the mesh''' + + def broadcast_joke_cb(self, jokebook_id, joke_pickle, sender): + '''handle a BroadCast Joke by creating a new joke in the local store''' + if sender == self.my_bus_name: + # Ignore my own signal + return + + logging.debug('In broadcast_joke_cb. sender: %r' % sender) + + # 1. unpickle joke + joke_pickle = str(joke_pickle) + if len(joke_pickle) == 0: + logging.debug('JokeMachineSession.broadcast_joke_cb() -> empty joke_pickle - doing nothing') + return + joke = Joke.loads(joke_pickle) + logging.debug('%s broadcast a joke to my jokebook# %d with text: %s and answer %s', joke.joker, jokebook_id, joke.text, joke.answer) + + # 2. get the jokebook it belongs to + jokebook = Globals.JokeMachineState.jokebook(jokebook_id) + if jokebook is None: + logging.error('Joke was broadcast to non-existent jokebook id %d', jokebook_id) + return + + # 3. add it to jokes in the appropriate jokebook + jokebook.jokes.append(joke) + + # 4. TODO - show some kind of alert - ask on #sugar + + + # ############################################################################ + + + def participant_change_cb(self, added, removed): + '''Callback when tube participants change.''' + logging.debug('In participant_change_cb') + if added: + logging.debug('Adding participants: %r' % added) + if removed: + logging.debug('Removing participants: %r' % removed) + for handle, bus_name in added: + buddy = self._get_buddy(handle) + if buddy is not None: + logging.debug('Buddy %s was added' % buddy.props.nick) + for handle in removed: + buddy = self._get_buddy(handle) + if buddy is not None: + logging.debug('Buddy %s was removed' % buddy.props.nick) + + # TODO - participant changed + # Set buddy's polls to not active so I can't vote on them + #for poll in self.activity._polls: + #if poll.author == buddy.props.nick: + #poll.active = False + #logging.debug( + #'Closing poll %s of %s who just left.' % + #(poll.title, poll.author)) + + if not self.entered: + if self.is_initiator: + logging.debug("I'm initiating the tube") + else: + logging.debug('Joining, sending Hello') + self.Hello() + self.tube.add_signal_receiver(self.hello_cb, + 'Hello', + MESH_IFACE, + path=MESH_PATH, + sender_keyword='sender') + self.tube.add_signal_receiver(self.helloback_cb, + 'HelloBack', + MESH_IFACE, + path=MESH_PATH, + sender_keyword='sender') + self.tube.add_signal_receiver(self.submit_cb, + 'Submit', + MESH_IFACE, + path=MESH_PATH, + sender_keyword='sender') + self.tube.add_signal_receiver(self.broadcast_joke_cb, + 'BroadcastJoke', + MESH_IFACE, + path=MESH_PATH, + sender_keyword='sender') + + self.my_bus_name = self.tube.get_unique_name() + self.entered = True + + diff --git a/pages/__init__.py b/pages/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pages/__init__.py diff --git a/pages/choose.py b/pages/choose.py new file mode 100644 index 0000000..c115f94 --- /dev/null +++ b/pages/choose.py @@ -0,0 +1,127 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import os +import gtk +import hippo +import pango +import logging +from gettext import gettext as _ + +from gui.canvaslistbox import CanvasListBox + +from globals import Globals +from gui.page import Page +from gui import theme + +import pages.cover +import pages.edit + +#from persistence.jokemachinestate import JokeMachineState +#from persistence.jokebook import Jokebook + +class Choose(Page): + + def __init__(self): + Page.__init__(self) + + # page title + self.append(hippo.CanvasText( + text= 'Choose a Jokebook to read:', + xalign=hippo.ALIGNMENT_START, + padding=10, + font_desc=theme.FONT_BODY.get_pango_desc())) + + # list of Jokebooks + allow_edit = Globals.JokeMachineActivity.is_initiator + #jokebooks_div = CanvasListBox(1050, 500) + jokebooks_div = CanvasListBox(1050, theme.zoom(500)) # TODO -> This should be sizing relative to parent + for jokebook in Globals.JokeMachineState.jokebooks: + jokebooks_div.append(self.__make_jokebook_div(jokebook, allow_edit)) + self.append(jokebooks_div) + + + def __do_clicked_title(self, control, event, jokebook): + Globals.JokeMachineActivity.set_page(pages.cover.Cover, jokebook) + + + def __do_clicked_edit(self, button, jokebook): + Globals.JokeMachineActivity.set_page(pages.edit.Edit, jokebook) + + + def __do_clicked_delete(self, button, jokebook): + confirm = gtk.MessageDialog(Globals.JokeMachineActivity, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, + gtk.BUTTONS_YES_NO, + _('Are you sure you want to delete your') + \ + ' \'' + jokebook.title + '\' ' + _('jokebook ?')) + response = confirm.run() + confirm.hide() + confirm.destroy() + del confirm + if response == gtk.RESPONSE_YES: + logging.debug('Deleting jokebook: %s' % jokebook.title) + Globals.JokeMachineState.jokebooks.remove(jokebook) + Globals.JokeMachineActivity.set_page(pages.choose.Choose) + + + def __make_jokebook_div(self, jokebook, edit = False): + list_row = self.make_listrow() + + # thumbnail + thumbnail = self.make_imagebox(jokebook, 'image', 80, 60, False, 10) + list_row.append(self.__make_column_div(100, thumbnail)) + + # title + title = hippo.CanvasText(text=jokebook.title, + padding_left = 20, + xalign=hippo.ALIGNMENT_START, + font_desc=theme.FONT_LARGE.get_pango_desc(), + color=theme.COLOR_LINK.get_int()) + title.set_clickable(True) + title.connect('button-press-event', self.__do_clicked_title, jokebook) + list_row.append(self.__make_column_div(330, title)) + + list_row.append(hippo.CanvasBox(box_width=theme.SPACER_HORIZONTAL)) # TODO spacer + + # owner + list_row.append(self.__make_column_div(330, hippo.CanvasText(text= jokebook.owner, + xalign=hippo.ALIGNMENT_START, + font_desc=theme.FONT_LARGE.get_pango_desc()))) + + # buttons + if edit: + button = gtk.Button(_('Edit')) + button.connect('clicked', self.__do_clicked_edit, jokebook) + list_row.append(self.__make_column_div(100, hippo.CanvasWidget(widget=theme.theme_widget(button)))) + list_row.append(hippo.CanvasBox(box_width=theme.SPACER_HORIZONTAL)) # TODO spacer + button = gtk.Button(_('Delete')) + button.connect('clicked', self.__do_clicked_delete, jokebook) + list_row.append(self.__make_column_div(100, hippo.CanvasWidget(widget=theme.theme_widget(button)))) + + return list_row + + + def __make_column_div(self, width, content): + ret = hippo.CanvasBox( + box_width=width, + yalign=hippo.ALIGNMENT_CENTER) + ret.append(content) + return ret + + + diff --git a/pages/cover.py b/pages/cover.py new file mode 100644 index 0000000..23a2da0 --- /dev/null +++ b/pages/cover.py @@ -0,0 +1,60 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import os +import gtk +import hippo +import pango +import logging +from gettext import gettext as _ + +from globals import Globals +from gui.page import Page +from gui import theme + +import pages.joke + + +class Cover(Page): + + def __init__(self, jokebook): + Page.__init__(self) + + # title + self.append(hippo.CanvasText(text='"' + jokebook.title + '" ' + _('started by') + ' ' + jokebook.owner, + xalign=hippo.ALIGNMENT_CENTER, + padding_top=10, + font_desc=theme.FONT_BODY_BOLD.get_pango_desc())) + self.append(hippo.CanvasBox(box_height=theme.SPACER_VERTICAL)) + + # cover picture + cover_picture = self.make_imagebox(jokebook, 'image', 640, 480, False) + self.append(cover_picture) + self.append(hippo.CanvasBox(box_height=theme.SPACER_VERTICAL)) + + # open button + button = gtk.Button(_('Open')) + button.connect('clicked', self.__do_clicked_open, jokebook) + button.set_size_request(50, -1) + self.append(hippo.CanvasWidget(widget=theme.theme_widget(button), + box_width=50)) + + + def __do_clicked_open(self, button, jokebook): + Globals.JokeMachineActivity.set_page(pages.joke.Joke, jokebook) + + + \ No newline at end of file diff --git a/pages/edit.py b/pages/edit.py new file mode 100644 index 0000000..b213655 --- /dev/null +++ b/pages/edit.py @@ -0,0 +1,303 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import os +import gtk +import hippo +import pango +import logging +from gettext import gettext as _ + +from globals import Globals +from gui.page import Page +from gui import theme + +from gui.canvaslistbox import CanvasListBox + +from util.decorators import Property + +from pages.submit import JokeEditor +from pages.joke import JokeViewer + +import pages.preview +import persistence.joke + + + +class PageSelector(hippo.CanvasBox): + + def __init__(self, parent, **kwargs): + hippo.CanvasBox.__init__(self, **kwargs) + self.__parent = parent + + control_width = 1024 # TODO -> Figure this out from parent size + self.props.border = 1 + self.props.border_color=theme.COLOR_TAB_ACTIVE.get_int() + self.props.background_color=theme.COLOR_PAGE.get_int() + self.props.orientation=hippo.ORIENTATION_VERTICAL + + # button box -> # TODO -> Make into generic control + tab_width = control_width / 3.0 + tab_box = hippo.CanvasBox(background_color=theme.COLOR_TAB_SEPERATOR.get_int(), + spacing=2, + orientation=hippo.ORIENTATION_HORIZONTAL) + self.__tab_1 = hippo.CanvasText(text=_('Edit Jokebook Cover'), + box_width=tab_width, + padding=theme.PADDING_TAB, + xalign=hippo.ALIGNMENT_START, + background_color=theme.COLOR_TAB_ACTIVE.get_int(), + color=theme.COLOR_TAB_TEXT.get_int(), + font_desc=theme.FONT_TABS.get_pango_desc()) + self.__tab_1.page = EditInfo + self.__tab_1.connect('button-press-event', self.__do_clicked_tab) + tab_box.append(self.__tab_1) + self.__tab_2 = hippo.CanvasText(text=_('Edit My Jokes'), + box_width=tab_width, + padding=theme.PADDING_TAB, + xalign=hippo.ALIGNMENT_START, + background_color=theme.COLOR_TAB_INACTIVE.get_int(), + color=theme.COLOR_TAB_TEXT.get_int(), + font_desc=theme.FONT_TABS.get_pango_desc()) + self.__tab_2.page = EditJokes + self.__tab_2.connect('button-press-event', self.__do_clicked_tab) + tab_box.append(self.__tab_2) + self.__tab_3 = hippo.CanvasText(text=_('Review Submitted Jokes'), + box_width=tab_width, + padding=theme.PADDING_TAB, + xalign=hippo.ALIGNMENT_START, + background_color=theme.COLOR_TAB_INACTIVE.get_int(), + color=theme.COLOR_TAB_TEXT.get_int(), + font_desc=theme.FONT_TABS.get_pango_desc()) + self.__tab_3.page = EditReview + self.__tab_3.connect('button-press-event', self.__do_clicked_tab) + tab_box.append(self.__tab_3) + self.append(tab_box) + + self.__page = hippo.CanvasBox(background_color=theme.COLOR_PAGE.get_int(), + orientation=hippo.ORIENTATION_VERTICAL) + self.append(self.__page) + + + @Property + def page(): + def get(self): return self.__page.the_page + def set(self, value): + self.__page.clear() + self.__page.append(value) + self.__page.the_page = value + + + def __do_clicked_tab(self, control, event): + self.__tab_1.props.background_color=theme.COLOR_TAB_INACTIVE.get_int() + self.__tab_2.props.background_color=theme.COLOR_TAB_INACTIVE.get_int() + self.__tab_3.props.background_color=theme.COLOR_TAB_INACTIVE.get_int() + control.props.background_color=theme.COLOR_TAB_ACTIVE.get_int() + self.__parent.do_tab_clicked(control.page) + + + +class Edit(Page): + + def __init__(self, jokebook): + Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER) + + self.__jokebook = jokebook + + self.__page_selector = PageSelector(self) + self.append(self.__page_selector) + self.__page_selector.page = EditInfo(jokebook, self) + + button = gtk.Button(_('Preview')) + button.connect('clicked', self.__do_clicked_preview, jokebook) + self.append(hippo.CanvasWidget(widget=theme.theme_widget(button), padding_top=theme.SPACER_VERTICAL)) + + + def __do_clicked_preview(self, button, jokebook): + Globals.JokeMachineActivity.set_page(pages.preview.Preview, jokebook) + + + def do_tab_clicked(self, page_class): + print page_class + self.__page_selector.page = page_class(self.__jokebook, self) + + + +class EditInfo(Page): # TODO -> gui.Page should follow this pattern rather + def __init__(self, jokebook, parent): + Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER, + orientation=hippo.ORIENTATION_VERTICAL, + padding=20, + spacing=20, + box_height=theme.TABS_HEIGHT) + + # page title + self.append(self.make_field(_('Title of Jokebook:'), 250, jokebook, 'title', 300, True)) + #field = self.make_field(_('Sound Effect:'), 250, None, '', 300, False) + + sound_effect = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, spacing=10) + sound_effect.append(self.make_bodytext(_('Sound Effect:'), 250, hippo.ALIGNMENT_START, theme.COLOR_DARK_GREEN)) + sound_effect.append(self.make_audiobox(jokebook, 'sound', 316)) + self.append(sound_effect) + + + # cover picture + cover_image = self.make_imagebox(jokebook, 'image', 320, 240, True) + self.append(cover_image) + + # punchline sound + #self.append(self.make_audiobox(jokebook, 'sound')) + + + +class EditJokes(Page): + + def __init__(self, jokebook, parent): + Page.__init__(self) + + # list of jokes + jokes_div = CanvasListBox(800, theme.TABS_HEIGHT) + jokes_div.props.border=0 + for joke in jokebook.jokes: + list_row = self.make_listrow(JokeEditor(joke)) + button = gtk.Button(' ' + _('Delete') + ' ') + button.connect('clicked', self.__do_clicked_delete, jokebook, joke, parent) + list_row.append(hippo.CanvasWidget(widget=theme.theme_widget(button), + border_color=theme.COLOR_RED.get_int(), + border=0, + padding_top=10, + padding_bottom=10, + padding_left=85)) + #xalign=hippo.ALIGNMENT_END)) + jokes_div.append(list_row) + self.append(jokes_div) + + # new joke button + buttons = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + xalign=hippo.ALIGNMENT_START) + + button = gtk.Button(_('Add New Joke')) + button.connect('clicked', self.__do_clicked_add_joke, jokebook, parent) + buttons.append(hippo.CanvasWidget(widget=theme.theme_widget(button))) + jokes_div.append(buttons) + + + def __do_clicked_delete(self, button, jokebook, joke, parent): + confirm = gtk.MessageDialog(Globals.JokeMachineActivity, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + gtk.MESSAGE_QUESTION, + gtk.BUTTONS_YES_NO, + _('Are you sure you want to delete this joke ?')) + response = confirm.run() + confirm.hide() + confirm.destroy() + del confirm + if response == gtk.RESPONSE_YES: + logging.debug('Deleting joke: %s' % joke.id) + jokebook.jokes.remove(joke) + parent.do_tab_clicked(EditJokes) + + + def __do_clicked_add_joke(self, button, jokebook, parent): + # create a new joke + joke = persistence.joke.Joke() + joke.id = jokebook.next_joke_id + logging.info('Created new joke with id: %d' % joke.id) + joke.joker = Globals.nickname + jokebook.jokes.append(joke) + + # reload tab + parent.do_tab_clicked(EditJokes) + + + +class EditReview(Page): + def __init__(self, jokebook, parent): + Page.__init__(self) + + jokes_div = CanvasListBox(800, theme.TABS_HEIGHT) + jokes_div.props.border=0 + for joke in jokebook.submissions: + list_row = self.make_listrow(JokeViewer(joke, jokebook.title)) + list_row.props.orientation=hippo.ORIENTATION_VERTICAL + + buttons = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + xalign=hippo.ALIGNMENT_END, + spacing=10, + padding=10) + + button = gtk.Button(' ' + _('Reject') + ' ') + button.connect('clicked', self.__do_clicked_reject, jokebook, joke, parent) + buttons.append(hippo.CanvasWidget(widget=theme.theme_widget(button), + border_color=theme.COLOR_RED.get_int(), + border=0, + xalign=hippo.ALIGNMENT_CENTER)) + + button = gtk.Button(' ' + _('Accept') + ' ') + button.connect('clicked', self.__do_clicked_accept, jokebook, joke, parent) + buttons.append(hippo.CanvasWidget(widget=theme.theme_widget(button), + border_color=theme.COLOR_RED.get_int(), + border=0, + xalign=hippo.ALIGNMENT_CENTER)) + + list_row.append(buttons) + + #list_row.props.orientation=hippo.ORIENTATION_VERTICAL + #status_box = hippo.CanvasBox(orientation=hippo.ORIENTATION_HORIZONTAL, + #padding_top=4, + #padding_left=4) + #status_box.append(hippo.CanvasText(text=_('Status:'), + #color=theme.COLOR_DARK_GREEN.get_int(), + #box_width=100, + #xalign=hippo.ALIGNMENT_START, + #font_desc=theme.FONT_BODY.get_pango_desc())) + ##button = None + #button = gtk.RadioButton() + #button = gtk.RadioButton(button, _('Approved')) + #button.set_size_request(200, -1) + #status_box.append(hippo.CanvasWidget(widget = button)) + #button = gtk.RadioButton(button, _('Rejected')) + #button.set_size_request(200, -1) + #status_box.append(hippo.CanvasWidget(widget = button)) + #button = gtk.RadioButton(button, _('Not Reviewed')) + #button.set_size_request(200, -1) + #button.set_active(True) + #status_box.append(hippo.CanvasWidget(widget = button)) + #list_row.append(status_box) + + jokes_div.append(list_row) + + self.append(jokes_div) + + + def __do_clicked_accept(self, button, jokebook, joke, parent): + jokebook.jokes.append(joke) + jokebook.submissions.remove(joke) + parent.do_tab_clicked(EditReview) + + if not Globals.JokeMachineActivity.is_shared: + return + + # broadcast submission onto the mesh + logging.debug('Broadcasting joke to mesh') + pickle = joke.dumps() + Globals.JokeMachineActivity.tube.BroadcastJoke(jokebook.id, pickle) + logging.debug('Broadcasted joke to mesh') + + + + def __do_clicked_reject(self, button, jokebook, joke, parent): + jokebook.submissions.remove(joke) + parent.do_tab_clicked(EditReview) diff --git a/pages/joke.py b/pages/joke.py new file mode 100644 index 0000000..fedd50e --- /dev/null +++ b/pages/joke.py @@ -0,0 +1,164 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import os +import gtk +import hippo +import pango +from gettext import gettext as _ + +from globals import Globals +from gui.page import Page +from gui import theme +from util.audioplayer import AudioPlayer + +import pages.submit + +import persistence.joke + +class JokeViewer(Page): + + def __init__(self, joke, jokebook_title=''): + Page.__init__(self, + spacing=8, + #background_color=theme.COLOR_PAGE.get_int(), + padding=4, + border_color=theme.COLOR_RED.get_int(), + border=0, + xalign=hippo.ALIGNMENT_START, + orientation=hippo.ORIENTATION_HORIZONTAL) + + # left column + self.left = hippo.CanvasBox(border=0, + border_color=theme.COLOR_RED.get_int(), + box_width=450, + xalign=hippo.ALIGNMENT_START, + orientation=hippo.ORIENTATION_VERTICAL) + joke_image = self.make_imagebox(joke, 'image', 320, 240, False) + self.left.append(joke_image) + self.left.append(hippo.CanvasText(text=jokebook_title, + xalign=hippo.ALIGNMENT_START, + color=theme.COLOR_DARK_GREEN.get_int(), + font_desc=theme.FONT_BODY_BOLD.get_pango_desc())) + self.left.append(hippo.CanvasText(text=_('Joke') + ' ' + str(joke.id), + xalign=hippo.ALIGNMENT_START, + font_desc=theme.FONT_BODY.get_pango_desc())) + self.left.append(hippo.CanvasText(text=_('By') + ' ' + str(joke.joker), + xalign=hippo.ALIGNMENT_START, + font_desc=theme.FONT_BODY.get_pango_desc())) + + # right column + self.right = hippo.CanvasBox(border=0, + border_color=theme.COLOR_RED.get_int(), + box_width=350, + xalign=hippo.ALIGNMENT_START, + orientation=hippo.ORIENTATION_VERTICAL) + self.right.append(hippo.CanvasText(text=_('Question'), + xalign=hippo.ALIGNMENT_START, + color=theme.COLOR_DARK_GREEN.get_int(), + font_desc=theme.FONT_BODY.get_pango_desc())) + self.right.append(self.make_bodytext(joke.text)) + + self.right.append(hippo.CanvasBox(box_height=30)) # spacer + + self.answer_box = hippo.CanvasBox() + self.answer_box.append(hippo.CanvasText(text=_('Answer'), + xalign=hippo.ALIGNMENT_START, + color=theme.COLOR_DARK_GREEN.get_int(), + font_desc=theme.FONT_BODY.get_pango_desc())) + self.answer_box.append(self.make_bodytext(joke.answer)) + self.right.append(self.answer_box) + + self.append(self.left) + self.append(self.right) + + + +class Joke(Page): + + def __init__(self, jokebook, joke_id = 0): + Page.__init__(self) + + # handle empty jokebook + if len(jokebook.jokes) <= joke_id: + self.append(self.make_bodytext(_('This Jokebook is empty'))) + if not Globals.JokeMachineActivity.is_initiator: + button = gtk.Button(_('Submit a Joke')) + button.connect('clicked', self.__do_clicked_submit, jokebook, joke_id) + self.append(hippo.CanvasWidget(widget=theme.theme_widget(button), + padding_top=20)) + return + + # the joke box + joke = jokebook.jokes[joke_id] + self.joke_box = JokeViewer(joke, jokebook.title) + self.joke_box.answer_box.set_visible(False) + + # the navigation box + self.navigation_box = hippo.CanvasBox( + padding_right=8, + padding_top=8, + spacing=18, + orientation=hippo.ORIENTATION_HORIZONTAL) + + # the answer button + button = gtk.Button(_('Answer')) + button.connect('clicked', self.__do_clicked_answer, jokebook, joke_id) + self.navigation_box.append(hippo.CanvasWidget(widget=theme.theme_widget(button), padding_top=20)) + self.joke_box.right.append(self.navigation_box) + self.append(self.joke_box) + + + # for forcing the joke into the answered state from page.submit + def force_answer(self, jokebook, joke_id): + self.__do_clicked_answer(None, jokebook, joke_id) + + + def __do_clicked_answer(self, button, jokebook, joke_id): + # play a sound if the jokebook has one + if jokebook.sound_blob != None: + player = AudioPlayer() + player.raw = jokebook.sound_blob + player.play() + + # show the answer + self.joke_box.answer_box.set_visible(True) + + # reconfigure navigation box + self.navigation_box.clear() + + # check if there are any more jokes left + if len(jokebook.jokes) > joke_id + 1: + button = gtk.Button(_('Next')) + button.connect('clicked', self.__do_clicked_next, jokebook, joke_id + 1) + self.navigation_box.append(hippo.CanvasWidget(widget=theme.theme_widget(button), padding_right=10, padding_top=20)) + + # only allow submitting a joke if activity is shared and you are the one joining + if not Globals.JokeMachineActivity.is_initiator: + button = gtk.Button(_('Submit a Joke')) + button.connect('clicked', self.__do_clicked_submit, jokebook, joke_id) + self.navigation_box.append(hippo.CanvasWidget(widget=theme.theme_widget(button), + padding_top=20)) + + + def __do_clicked_submit(self, button, jokebook, joke_id): + Globals.JokeMachineActivity.set_page(pages.submit.Submit, jokebook, joke_id) + + + def __do_clicked_next(self, button, jokebook, joke_id): + Globals.JokeMachineActivity.set_page(pages.joke.Joke, jokebook, joke_id) + + diff --git a/pages/preview.py b/pages/preview.py new file mode 100644 index 0000000..de5bc3a --- /dev/null +++ b/pages/preview.py @@ -0,0 +1,56 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import os +import gtk +import hippo +import pango +import logging +from gettext import gettext as _ + +from globals import Globals +from gui.page import Page +from gui import theme +from gui.canvaslistbox import CanvasListBox + +from pages.joke import JokeViewer + +import pages.edit + + +class Preview(Page): + + def __init__(self, jokebook): + Page.__init__(self, xalign=hippo.ALIGNMENT_CENTER) + + preview_box = CanvasListBox(1028, theme.PREVIEW_HEIGHT) # TODO - really shouldn't be hardcoded + for joke in jokebook.jokes: + list_row = self.make_listrow(JokeViewer(joke, jokebook.title)) + preview_box.append(list_row) + self.append(preview_box) + + self.append(hippo.CanvasBox(box_height=theme.SPACER_VERTICAL)) + + button = gtk.Button(_('Edit')) + button.connect('clicked', self.__do_clicked_edit, jokebook) + self.append(hippo.CanvasWidget(widget=theme.theme_widget(button))) + + + def __do_clicked_edit(self, button, jokebook): + Globals.JokeMachineActivity.set_page(pages.edit.Edit, jokebook) + + + \ No newline at end of file diff --git a/pages/submit.py b/pages/submit.py new file mode 100644 index 0000000..77f230d --- /dev/null +++ b/pages/submit.py @@ -0,0 +1,137 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import os +import gtk +import hippo +import pango +import logging +from gettext import gettext as _ + +from globals import Globals +from gui.page import Page +from gui import theme + +from persistence.joke import Joke + +import pages.joke +import pages.choose + + +class JokeEditor(Page): + + def __init__(self, joke): + Page.__init__(self, + spacing=8, + padding=4, + border_color=theme.COLOR_RED.get_int(), + border=0, + xalign=hippo.ALIGNMENT_START, + orientation=hippo.ORIENTATION_HORIZONTAL) + + # left column + self.left = hippo.CanvasBox(border=0, + border_color=theme.COLOR_RED.get_int(), + box_width=450, + xalign=hippo.ALIGNMENT_START, + orientation=hippo.ORIENTATION_VERTICAL, + padding=theme.BORDER_WIDTH_CONTROL/2) + joke_image = self.make_imagebox(joke, 'image', 320, 240, True) + self.left.append(joke_image) + + # right column + self.right = hippo.CanvasBox(border=0, + border_color=theme.COLOR_RED.get_int(), + box_width=350, + xalign=hippo.ALIGNMENT_START, + orientation=hippo.ORIENTATION_VERTICAL, + padding_bottom=theme.BORDER_WIDTH_CONTROL/2, + spacing=theme.BORDER_WIDTH_CONTROL/2) + self.right.append(hippo.CanvasText(text=_('Question'), + xalign=hippo.ALIGNMENT_START, + color=theme.COLOR_DARK_GREEN.get_int(), + font_desc=theme.FONT_BODY.get_pango_desc())) + self.right.append(self.make_textbox(joke, 'text')) + + self.right.append(hippo.CanvasBox(box_height=theme.SPACER_VERTICAL)) + + self.right.append(hippo.CanvasText(text=_('Answer'), + xalign=hippo.ALIGNMENT_START, + color=theme.COLOR_DARK_GREEN.get_int(), + font_desc=theme.FONT_BODY.get_pango_desc())) + self.right.append(self.make_textbox(joke, 'answer')) + + self.append(self.left) + self.append(self.right) + + +class Submit(Page): + + def __init__(self, jokebook, last_joke=0): # last_joke is for 'back' button + Page.__init__(self, spacing=10) + + # create a new joke + joke = Joke() + joke.id = jokebook.next_joke_id + logging.info('Created new joke with id: %d' % joke.id) + joke.joker = Globals.nickname + + # info + self.append(self.make_field(_('Submission For:'), 250, jokebook, 'title', 300, False)) + self.append(self.make_field(_('Your Name:'), 250, joke, 'joker', 300, True)) + + self.append(hippo.CanvasBox(box_height=theme.SPACER_VERTICAL)) # spacer + + # joke editor + jokebox = JokeEditor(joke) + nav = hippo.CanvasBox( + padding_right=8, + padding_top=8, + spacing=18, + orientation=hippo.ORIENTATION_HORIZONTAL) + button = gtk.Button(_('Submit')) + button.connect('clicked', self.__do_clicked_submit, jokebook, joke) + nav.append(hippo.CanvasWidget(widget=theme.theme_widget(button), padding_right=10, padding_top=20)) + button = gtk.Button(_('Back')) + button.connect('clicked', self.__do_clicked_back, jokebook, last_joke) + nav.append(hippo.CanvasWidget(widget=theme.theme_widget(button), padding_top=20)) + jokebox.right.append(nav) + self.append(jokebox) + + + def __do_clicked_back(self, button, jokebook, last_joke): + joke_page = Globals.JokeMachineActivity.set_page(pages.joke.Joke, jokebook, last_joke) + joke_page.force_answer(jokebook, last_joke) # force joke into answered state + + + def __do_clicked_submit(self, button, jokebook, joke): + + Globals.JokeMachineActivity.set_page(pages.choose.Choose) + + # TODO -> Factor out of the page ? Should be transparent to UI layer ? + if not Globals.JokeMachineActivity.is_shared: + logging.error('pages.submit.Submit -> CANNOT SUBMIT WITHOUT A TUBE') + return + + logging.debug('Submitting joke to remote') + pickle = joke.dumps() + Globals.JokeMachineActivity.tube.Submit(jokebook.id, pickle) + logging.debug('Submitted joke to remote') + + + + + diff --git a/persistence/__init__.py b/persistence/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/persistence/__init__.py diff --git a/persistence/joke.py b/persistence/joke.py new file mode 100644 index 0000000..8523593 --- /dev/null +++ b/persistence/joke.py @@ -0,0 +1,94 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +from util.persistence import Persistent, PersistentProperty + + +class Joke(object): + __metaclass__ = Persistent + + @PersistentProperty + def id(): + '''some doc string''' + def default(self): return 0 + def get(self): return self.__id + def set(self, value): self.__id = value + + @PersistentProperty + def image(): + '''the image for the joke''' + def get(self): return self.__image + def set(self, value): self.__image = value + + @PersistentProperty + def image_blob(): + '''raw image data''' + def get(self): return self.__image_blob + def set(self, value): self.__image_blob = value + + @PersistentProperty + def text(): + '''the joke question''' + def get(self): return self.__text + def set(self, value): self.__text = value + + @PersistentProperty + def answer(): + '''the joke answer''' + def get(self): return self.__answer + def set(self, value): self.__answer = value + + @PersistentProperty + def joker(): + '''the author of the joke''' + def get(self): return self.__joker + def set(self, value): self.__joker = value + + @PersistentProperty + def joker_location(): + '''the location of the author''' + def get(self): return self.__joker_location + def set(self, value): self.__joker_location = value + + @PersistentProperty + def joker_country(): + '''the country of the author''' + def get(self): return self.__joker_country + def set(self, value): self.__joker_country = value + + @PersistentProperty + def show(): + '''should this joke be visible to others''' + def default(self): return False + def get(self): return self.__show + def set(self, value): self.__show = value + + + def test_data(self): + #self.image = 'resources/knockknock.png' + self.text = '''Knock, knock +Who's there ? +Alex. +Alex who?''' + self.answer = 'Alex plain later, just let me in!' + self.joker = 'hummingbird' + self.joker_location = 'Cape Town' + self.joker_country = 'South Africa' + self.show = True + + return self + + + \ No newline at end of file diff --git a/persistence/jokebook.py b/persistence/jokebook.py new file mode 100644 index 0000000..acadf04 --- /dev/null +++ b/persistence/jokebook.py @@ -0,0 +1,85 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +from util.persistence import Persistent, PersistentProperty + + +class Jokebook(object): + __metaclass__ = Persistent + + @PersistentProperty + def id(): + '''object id''' + def default(self): return 0 + def get(self): return self.__id + def set(self, value): self.__id = value + + @PersistentProperty + def title(): + '''the title of this jokebook''' + def get(self): return self.__title + def set(self, value): self.__title = value + + @PersistentProperty + def image(): + '''the cover image of this jokebook''' + def get(self): return self.__image + def set(self, value): self.__image = value + + @PersistentProperty + def image_blob(): + '''raw image data''' + def get(self): return self.__image_blob + def set(self, value): self.__image_blob = value + + @PersistentProperty + def sound(): + '''the sound we must play on punchline''' + def get(self): return self.__sound + def set(self, value): self.__sound = value + + @PersistentProperty + def sound_blob(): + '''raw data for the sound''' + def get(self): return self.__sound_blob + def set(self, value): self.__sound_blob = value + + @PersistentProperty + def owner(): + '''the owner of this jokebook''' + def get(self): return self.__owner + def set(self, value): self.__owner = value + + @PersistentProperty + def jokes(): + '''the jokes in the jokebook''' + def default(self): return [] + def get(self): return self.__jokes + + @PersistentProperty + def submissions(): + '''jokes submitted to this jokebook pending approval''' + def default(self): return [] + def get(self): return self.__submissions + + + # TODO - this should really be transparent + @property + def next_joke_id(self): + if len(self.jokes) == 0: + return 1 + return max([joke.id for joke in self.jokes]) + 1 + + \ No newline at end of file diff --git a/persistence/jokemachinestate.py b/persistence/jokemachinestate.py new file mode 100644 index 0000000..3d591cf --- /dev/null +++ b/persistence/jokemachinestate.py @@ -0,0 +1,82 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +from util.persistence import Persistent, PersistentProperty + +from persistence.jokebook import Jokebook +from persistence.joke import Joke + +class JokeMachineState(object): + __metaclass__ = Persistent + + @PersistentProperty + def id(): + '''object id''' + def default(self): return 0 + def get(self): return self.__id + def set(self, value): self.__id = value + + @PersistentProperty + def jokebooks(): + '''the jokebooks in this jokemachine''' + def default(self): return [] + def get(self): return self.__jokebooks + + @PersistentProperty + def version(): + '''The activity version used to create this Jokebook''' + def default(self): return 1 # TODO - pull from activity/activity.info + def get(self): return self.__version + + + @property + def next_jokebook_id(self): + if len(self.jokebooks) == 0: + return 1 + return max([jokebook.id for jokebook in self.jokebooks]) + 1 + + + def jokebook(self, id): + for jokebook in self.jokebooks: + if jokebook.id == id: + return jokebook + logging.error('Could not find jokebook with id %d' % d) + return None + + def test_data(self): + self.id = 1 + + # add some jokebooks with jokes + num_jokebooks = 0 + num_jokes = 2 + num_submissions = 2 + for jokebook_id in range(1, num_jokebooks + 1): + jokebook = Jokebook() + jokebook.id = jokebook_id + jokebook.owner = 'hummingbird' + jokebook.title = 'Jokebook ' + str(jokebook.id) + #jokebook.image = 'images/smile-big.png' + for joke_id in range(1, num_jokes + 1): + joke = Joke().test_data() + joke.id = joke_id + jokebook.jokes.append(joke) + for joke_id in range(1, num_submissions + 1): + joke = Joke().test_data() + joke.id = joke_id + jokebook.submissions.append(joke) + self.jokebooks.append(jokebook) + + return self + \ No newline at end of file diff --git a/resources/GameLogoCharacter.png b/resources/GameLogoCharacter.png new file mode 100644 index 0000000..bcfe5f6 --- /dev/null +++ b/resources/GameLogoCharacter.png Binary files differ diff --git a/resources/audio.png b/resources/audio.png new file mode 100644 index 0000000..63816d9 --- /dev/null +++ b/resources/audio.png Binary files differ diff --git a/resources/image.png b/resources/image.png new file mode 100644 index 0000000..f350158 --- /dev/null +++ b/resources/image.png Binary files differ diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..6e2061f --- /dev/null +++ b/setup.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +try: + from sugar.activity import bundlebuilder + bundlebuilder.start("JokeMachine") +except ImportError: + print 'Cannot find a working sugar environment' + diff --git a/unit/__init__.py b/unit/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/unit/__init__.py diff --git a/unit/test_persistence.py b/unit/test_persistence.py new file mode 100755 index 0000000..2062240 --- /dev/null +++ b/unit/test_persistence.py @@ -0,0 +1,48 @@ +#!/usr/bin/python +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import sys +sys.path.append('..') + +from persistence.jokemachinestate import JokeMachineState + +def dump(obj, indent = ' '): + print indent + str(obj) + for name, prop in obj.__properties__: + value = prop.fget(obj) + print indent + name, '=', value #, ' "' + prop.__doc__ + '"' + if value.__class__ is list: + for item in value: + dump(item, indent + ' ') + + print indent + 'is_dirty =', obj.__dirty__ + + + + +state = JokeMachineState().test_data() + +print state.jokebooks +print state.next_jokebook_id + +#[max(joke.id) for joke in self.__jokes] + +#pickle = state.dumps() +#new_state = JokeMachineState.loads(pickle) +#dump(new_state) + + diff --git a/unit/unit.py b/unit/unit.py new file mode 100755 index 0000000..cf06715 --- /dev/null +++ b/unit/unit.py @@ -0,0 +1,303 @@ +#!/usr/bin/python +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import sys +sys.path.append('..') + +#import json +import cPickle + +from persistence.joke import Joke +from persistence.jokebook import Jokebook +from persistence.jokemachinestate import JokeMachineState + +from util.decorators import Property + +# ############################################################################## +# test Property decorator +# + +#class Foo(object): + + #@Property + #def all(): + #def get(self): return self.__all + #def set(self, value): self.__all = value + #def default(self): return 'this is a default value' + + #@Property + #def onlyget(): + #def get(self): return self.__onlyget + + #@Property + #def nodefault(): + #def get(self): return self.__nodefault + #def set(self, value): self.__nodefault = value + + +#help(Property) +#foo = Foo() + +#print foo.all +#Foo.all = 'changed' +#print foo.all + +#print foo.nodefault +#foo.nodefault = 'set now' +#print foo.nodefault + +#print foo.onlyget +#foo.onlyget = 'cannot set this' + + +def f(x): + ret = 0 + counter = x + while (counter != 0): + ret = ret + x + counter = counter - 1 + return ret + + +buf = [] +def g(x): + if len(buf) <= x: + curpos = len(buf) + for i in range(curpos, x + 1): + buf.insert(i, i * i) + return buf[x] + + + +print g(4) +print g(1) +#print g(2) +#print g(3) +print g(8) +print g(6) + +sys.exit() + +class Plink(object): + def __init__(self): + self.b = 'a' + +class Plonk(Plink): + def __init__(self): + self.b = 'a' + +class Foo(Plonk): + def __init__(self): + + self.a = 'a' + +class Bar(Foo): + def plonk(self, value): + value = 'b' + +bar = Bar() + +def list_bases(c): + bases = [] + print 'looking at: ', c + if not hasattr(c, '__bases__'): + print 'quitting at', c + print dir(c) + return bases + + for base in c.__bases__: + print 'Has base: ', base + bases.append(base) + if len(base.__bases__) > 0: + bases.append(list_bases(base.__bases__)) + + return bases + +print list_bases(bar.__class__) + +sys.exit() + + +# ############################################################################## +# test PersistentProperty decorator +# + +# dump properties on persistent objects +def dump(obj, indent = ' '): + print indent + str(obj) + for name, prop in obj.__properties__: + value = prop.fget(obj) + print indent + name, '=', value #, ' "' + prop.__doc__ + '"' + if value.__class__ is list: + for item in value: + dump(item, indent + ' ') + + print indent + 'is_dirty =', obj.__dirty__ + + + + +state = JokeMachineState() +state = state.test_data() +dump(state) +print "\n========================================================================\n" +pickle = state.dumps() +j = JokeMachineState.loads(pickle) +jokebook = j.jokebooks[0] +jokebook.owner = 'new owner' +dump(j) + + + +sys.exit() + + +joke = Joke() + +print 'Joke.id.doc: ', Joke.id.__doc__ +print 'Joke.joke_text.doc: ', Joke.text.__doc__ +print + +print 'joke.id default should be 0 = ', joke.id +print 'joke is dirty = ', joke.__dirty__ +print + +joke.id = 66 +print 'joke.id set to 66 = ', joke.id +print 'joke is dirty = ', joke.__dirty__ +print + +print 'joke.joke_text has no default:', joke.text +joke.text = 'joke text' +print 'joke.joke_text has been set to \'joke text\' = ', joke.text +print + +print 'joke.id is still 66 = ', joke.id +joke.id = 23 +print + +print 'joke.id set to 23 = ', joke.id +print + + +jokebook = Jokebook() +dump(jokebook) +print + +jokebook.jokes.append(joke) +dump(jokebook) +print + +j = jokebook.jokes[0] +j.id = 991 + +dump(jokebook) + +print "========================================================================\n" + +# ############################################################################## +# test Pickling +# +#jokebook.__dirty__ = False +#jokebook.jokes[0].__dirty__ = False + +p = cPickle.dumps(jokebook) +#print p +#o = cPickle.loads(p) + +#dump(o) +#dump(o.jokes[0]) + +print 'Persisting...' +print +jokebook.id = 0 +dump(jokebook) +print +pickle = jokebook.dumps() +j = Jokebook.loads(pickle) +dump(j) + +#print j.jokes.__class__ + +#print joke +#print joke.__dirty__ +#print joke.dirty +#print joke.pdirty + + +# json +#p = json.write(j.__dict__) +#print p +#d = json.read(p) +#o = Joke() +#o.__dict__ = d +#print o.id + +# pickle +#p = cPickle.dumps(j) +#print p +#o = cPickle.loads(p) +#print o.id +#print o.some_prop +#print o.fn() + + +#meta +#class meta(type): + #def __init__(cls, name, bases, dct): + #print 'Init is called: ' + str(cls) + str(name) + str(bases) + str(dct) + + #method_list = [] + #for func in dct.values(): + #try: + #method_name = func._dbus_method_name() + #print func, method_name + #method_list.append(method_name) + #except: + #pass + + #print method_list + #super(meta, cls).__init__(name, bases, dct) + +#def method(func): + #def decorator(self, *args): + #func(self, *args) + + #def _dbus_method_name(): + #return 'dbus_' + func.__name__ + + #decorator._dbus_method_name = _dbus_method_name + #return decorator + + +#class bar: + #__metaclass__ = meta + + #@method + #def my_method(self): + #print 'my_first_method' + + #@method + #def another_method(self): + #print 'my_other_method' + +#x = bar() +#x.my_method() + +#print x.my_method._dbus_method_name + diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/util/__init__.py diff --git a/util/audioplayer.py b/util/audioplayer.py new file mode 100644 index 0000000..911fa42 --- /dev/null +++ b/util/audioplayer.py @@ -0,0 +1,132 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import os +import gst +import logging + +from globals import Globals + +from util.decorators import Property + + +class AudioPlayer(object): + + def __init__(self): + pass + + + @Property + def uri(): + def get(self): return self.__uri + def set(self, value): + if value is None or not os.path.exists(value): + logging.error('AudioPlayer- Invalid URI: %r', value) + return + self.__uri = value + size = os.path.getsize(self.__uri) + self.pipeline.get_by_name('source').set_property('location', self.__uri) + self.pipeline.get_by_name('source').set_property('mmapsize', size) + + + @Property + def raw(): + def get(self): + if self.uri is None: + logging.error('AudioPlayer - No data') + return None + f = open(self.uri, 'r') + raw = f.read() + f.close() + return raw + def set(self, value): + temp = Globals.temporary_filename() + f = open(temp, 'w') + f.write(value) + f.close() + #self.source.set_property('location', temp) + #self.source.set_property('mmapsize', len(value)) + self.uri = temp + logging.debug('AudioPlayer - set_raw wrote %d bytes to %s', len(value), temp) + + + @Property + def pipeline(): + def get(self): + if self.__pipeline is None: + self.__pipeline = self.__build_pipeline() + return self.__pipeline + + + def play(self): + logging.debug('AudioPlayer - started playing sound') + self.pipeline.set_state(gst.STATE_PLAYING) + logging.debug('AudioPlayer - finished playing sound') + + + + def __build_pipeline(self): + # pipeline + pipeline = gst.Pipeline('pipeline') + + # add source + source = gst.element_factory_make('filesrc', 'source') + pipeline.add(source) + + # add decoder + decoder = gst.element_factory_make('decodebin', 'decoder') + decoder.connect("new-decoded-pad", self.__new_decoded_pad) #, converter) + pipeline.add(decoder) + source.link(decoder) + + # add converter + converter = gst.element_factory_make("audioconvert", "converter") + pipeline.add(converter) + + # add output + sink = gst.element_factory_make('autoaudiosink', 'sink') + pipeline.add(sink) + converter.link(sink) + + bus = pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self.__on_audio_message) + + return pipeline + + + + # callbacks ################################################################## + + def __new_decoded_pad(self, dbin, pad, islast): #, converter) + converter = self.pipeline.get_by_name('converter') # TODO - pass by arg + pad.link(converter.get_pad("sink")) + + + def __on_audio_message(self, bus, message): + t = message.type + #logging.debug('message: %r' % t) + if t == gst.MESSAGE_EOS: + self.pipeline.set_state(gst.STATE_NULL) + logging.debug('AudioPlayer - EOS') + elif t == gst.MESSAGE_ERROR: + self.pipeline.set_state(gst.STATE_NULL) + err, debug = message.parse_error() + logging.debug('AudioPlayer - Error: %r %r', err, debug) + + + # Not used + def __on_source_handoff(self, source, buffer, pad): + logging.debug('on_source_handoff(%r, %r, %r)', source, buffer, pad) diff --git a/util/decorators.py b/util/decorators.py new file mode 100644 index 0000000..cd00afc --- /dev/null +++ b/util/decorators.py @@ -0,0 +1,64 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import sys + + +# Courtesy of: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/465427 +DecoratorWithArgs = \ + lambda decorator: lambda *args, **kwargs: lambda func: decorator(func, *args, **kwargs) + + + +def Property(function): + '''Property Decorator + Adapted from: http://wiki.python.org/moin/PythonDecoratorLibrary + + Example: + + class Foo(object): + @Property + def my_property(): + def get(self): return self.__my_property + def set(self, value): self.__my_property = value # (optional) + def default(self): return 'some default value' # (optional) + ''' + + keys = 'set', 'del' + func_locals = {'doc':function.__doc__} + + def fgetter(obj, name, getter, fdef): + attr_name = '_' + obj.__class__.__name__ + '__' + name + if not hasattr(obj, attr_name): + setattr(obj, attr_name, fdef(obj)) + return getter(obj) + + def introspect(frame, event, arg): + if event == 'return': + locals = frame.f_locals + func_locals.update(dict(('f' + k,locals.get(k)) for k in keys)) + if locals.has_key('default'): + func_locals['fget'] = lambda obj : fgetter(obj, function.__name__, locals['get'], locals['default']) + else: + func_locals['fget'] = lambda obj : fgetter(obj, function.__name__, locals['get'], lambda x : None) + + sys.settrace(None) + return introspect + + sys.settrace(introspect) + function() + + return property(**func_locals) + diff --git a/util/journalpickler.py b/util/journalpickler.py new file mode 100644 index 0000000..36f9311 --- /dev/null +++ b/util/journalpickler.py @@ -0,0 +1,47 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import pickle # TODO -> why does cPickle bitch about needing persistence.Jokebook etc ? + + +class JournalPickler: + '''Works with util.Persistence to persist objects to the Sugar Journal''' + + def __init__(self, obj = None): + pass + + def __set_dirty(self, obj, is_dirty): + obj.__dirty__ = False + for name, prop in obj.__properties__: + if prop.fget(obj).__class__ is list: + for item in prop.fget(obj): + if hasattr(item, '__dirty__'): + self.__set_dirty(item, is_dirty) + + + def dumps(self, obj, deep_dump): + self.__set_dirty(obj, False) + pickled = pickle.dumps(obj) + return pickled + + + def loads(self, pickled): + obj = pickle.loads(pickled) + self.__set_dirty(obj, False) + return obj + + +dumps = lambda obj, deep=False : JournalPickler(obj).dumps(obj, deep) +loads = JournalPickler().loads diff --git a/util/persistence.py b/util/persistence.py new file mode 100644 index 0000000..e73879b --- /dev/null +++ b/util/persistence.py @@ -0,0 +1,81 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import sys + + +class Persistent(type): + '''Metaclass providing object persistence''' + def __init__(cls, name, bases, dct): + super(Persistent, cls).__init__(name, bases, dct) + + setattr(cls, '__dirty__', False) + setattr(cls, '__properties__', filter(_is_persistent, dct.iteritems())) + + from util.journalpickler import dumps, loads + setattr(cls, 'dumps', dumps) + setattr(cls, 'loads', loads) + + + +def PersistentProperty(function): + '''Decorator to set up persistent properties + Adapted from: http://wiki.python.org/moin/PythonDecoratorLibrary + ''' + + func_locals = {'doc':function.__doc__} + + def fgetter(obj, name, getter, fdef): + attr_name = '_' + obj.__class__.__name__ + '__' + name + if not hasattr(obj, attr_name): + setattr(obj, attr_name, fdef(obj)) + return getter(obj) + + def fsetter(obj, name, setter, value): + setattr(obj, '__dirty__', True) # TODO -> Propogate dirty flag up to any + # parent objects ? + return setter(obj, value) + + def introspect(frame, event, arg): + if event == 'return': + locals = frame.f_locals + if locals.has_key('delete'): + func_locals['fdel'] = locals['delete'] + if locals.has_key('set'): + func_locals['fset'] = \ + lambda obj, value : fsetter(obj, function.__name__, locals['set'], value) + if locals.has_key('default'): + get_function = lambda obj : fgetter(obj, function.__name__, locals['get'], locals['default']) + else: + get_function = lambda obj : fgetter(obj, function.__name__, locals['get'], lambda x : None) + get_function.__decorator__ = PersistentProperty # tag the getter so we can id the + # decorator. Yeah, it's ugly. + func_locals['fget'] = get_function + sys.settrace(None) + return introspect + sys.settrace(introspect) + function() + return property(**func_locals) + + +# a wee bit ugly +def _is_persistent(item): + '''check if property is decorated with PersistentProperty decorator''' + prop = item[1] + return type(prop) is property and \ + hasattr(prop.fget, '__decorator__') and \ + prop.fget.__decorator__ is PersistentProperty + + diff --git a/util/persistence.py.args b/util/persistence.py.args new file mode 100644 index 0000000..1aa235f --- /dev/null +++ b/util/persistence.py.args @@ -0,0 +1,81 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import sys + +from util.decorators import DecoratorWithArgs + + +class Persistent(type): + '''Metaclass providing object persistence''' + def __init__(cls, name, bases, dct): + super(Persistent, cls).__init__(name, bases, dct) + + setattr(cls, '__dirty__', False) + setattr(cls, '__properties__', filter(_is_persistent, dct.iteritems())) + + from util.journalpickler import dumps, loads + setattr(cls, 'dumps', dumps) + setattr(cls, 'loads', loads) + + +@DecoratorWithArgs +def PersistentProperty(function, default = None): + '''Decorator to set persistent properties''' + keys = 'fset', 'fdel' + func_locals = {'doc':function.__doc__} + + def fgetter(obj, name, getter, default_value): + attr_name = '_' + obj.__class__.__name__ + '__' + name + if not hasattr(obj, attr_name): + setattr(obj, attr_name, default_value(obj)) + return getter(obj) + + def fsetter(obj, name, setter, value): + setattr(obj, '__dirty__', True) + return setter(obj, value) + + def introspect(frame, event, arg): + if event == 'return': + locals = frame.f_locals + func_locals.update(dict((k,locals.get(k)) for k in keys)) + if locals.has_key('fset'): + func_locals['fset'] = \ + lambda obj, value : fsetter(obj, function.__name__, locals['fset'], value) + if default != None: + get_function = \ + lambda obj : fgetter(obj, function.__name__, locals['fget'], lambda x : default) + else: + get_function = \ + lambda obj : fgetter(obj, function.__name__, locals['fget'], lambda x : None) + get_function.decorator = PersistentProperty # tag the getter so we can id + # the decorator. Yeah, it's ugly. + func_locals['fget'] = get_function + sys.settrace(None) + return introspect + + sys.settrace(introspect) + function() + return property(**func_locals) + + +# a wee bit ugly +def _is_persistent(item): + '''check if property is decorated with PersistentProperty decorator''' + prop = item[1] + return type(prop) is property and \ + hasattr(prop.fget, 'decorator') and \ + prop.fget.decorator is PersistentProperty diff --git a/util/persistence.py.noargs b/util/persistence.py.noargs new file mode 100644 index 0000000..bf982af --- /dev/null +++ b/util/persistence.py.noargs @@ -0,0 +1,75 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + + +import sys + + +def is_persistent(item): + prop = item[1] + return type(prop) is property and \ + hasattr(prop.fget, 'decorator') and \ + prop.fget.decorator is PersistentProperty + + +class Persistent(type): + def __init__(cls, name, bases, dct): + # is_dirty + setattr(cls, '__dirty__', False) + + # properties + setattr(cls, '__properties__', filter(is_persistent, dct.iteritems())) + + super(Persistent, cls).__init__(name, bases, dct) + + +def PersistentProperty(function): + function.decorator = PersistentProperty + + keys = 'fset', 'fdel' + func_locals = {'doc':function.__doc__} + + def fgetter(obj, name, getter, fdef): + attr_name = '_' + obj.__class__.__name__ + '__' + name + if not hasattr(obj, attr_name): + setattr(obj, attr_name, fdef(obj)) + return getter(obj) + + def fsetter(obj, name, setter, value): + setattr(obj, '__dirty__', True) + return setter(obj, value) + + def probe_function(frame, event, arg): + if event == 'return': + locals = frame.f_locals + func_locals.update(dict((k,locals.get(k)) for k in keys)) + + if locals.has_key('fset'): + func_locals['fset'] = lambda obj, value : fsetter(obj, function.__name__, locals['fset'], value) + + if locals.has_key('fdef'): + f = lambda obj : fgetter(obj, function.__name__, locals['fget'], locals['fdef']) + else: + f = lambda obj : fgetter(obj, function.__name__, locals['fget'], lambda x : None) + f.decorator = PersistentProperty # tag the getter + func_locals['fget'] = f + + sys.settrace(None) + return probe_function + sys.settrace(probe_function) + function() + return property(**func_locals) + + -- cgit v0.9.1