From 720985fc29dd3b0c9340d80ae2f0839b2d969897 Mon Sep 17 00:00:00 2001 From: Tony Anderson Date: Tue, 09 Jun 2009 08:50:15 +0000 Subject: add content --- diff --git a/activity/activity.info b/activity/activity.info new file mode 100755 index 0000000..5458bc7 --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,8 @@ +[Activity] +name = ShowNTell +service_name = org.laptop.showntell +class = showntell.ShowNTell +icon = showntell-activity +activity_version = 1 +show_launcher = yes +mime_types = application/x-classroompresenter; application/zip diff --git a/activity/mimetypes.xml b/activity/mimetypes.xml new file mode 100755 index 0000000..cf49d84 --- /dev/null +++ b/activity/mimetypes.xml @@ -0,0 +1,7 @@ + + + + Classroom Presenter Slide Deck + + + diff --git a/activity/permissions.info b/activity/permissions.info new file mode 100755 index 0000000..585d713 --- /dev/null +++ b/activity/permissions.info @@ -0,0 +1 @@ +constant-uid diff --git a/activity/showntell-activity.svg b/activity/showntell-activity.svg new file mode 100755 index 0000000..d57facb --- /dev/null +++ b/activity/showntell-activity.svg @@ -0,0 +1,431 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/empty.html b/empty.html new file mode 100644 index 0000000..18e00a5 --- /dev/null +++ b/empty.html @@ -0,0 +1,9426 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
<!--{{{-->
+<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
+<!--}}}-->
+
+
+
Background: #fff
+Foreground: #000
+PrimaryPale: #8cf
+PrimaryLight: #18f
+PrimaryMid: #04b
+PrimaryDark: #014
+SecondaryPale: #ffc
+SecondaryLight: #fe8
+SecondaryMid: #db4
+SecondaryDark: #841
+TertiaryPale: #eee
+TertiaryLight: #ccc
+TertiaryMid: #999
+TertiaryDark: #666
+Error: #f88
+
+
+
/*{{{*/
+body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+
+a {color:[[ColorPalette::PrimaryMid]];}
+a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
+a img {border:0;}
+
+h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
+h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
+h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}
+
+.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
+.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
+.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}
+
+.header {background:[[ColorPalette::PrimaryMid]];}
+.headerShadow {color:[[ColorPalette::Foreground]];}
+.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
+.headerForeground {color:[[ColorPalette::Background]];}
+.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}
+
+.tabSelected{color:[[ColorPalette::PrimaryDark]];
+	background:[[ColorPalette::TertiaryPale]];
+	border-left:1px solid [[ColorPalette::TertiaryLight]];
+	border-top:1px solid [[ColorPalette::TertiaryLight]];
+	border-right:1px solid [[ColorPalette::TertiaryLight]];
+}
+.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
+.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
+.tabContents .button {border:0;}
+
+#sidebar {}
+#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
+#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}
+
+.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
+.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
+.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
+.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
+	border:1px solid [[ColorPalette::PrimaryMid]];}
+.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
+.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
+.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
+.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
+	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
+.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
+.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
+	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}
+
+.wizard .notChanged {background:transparent;}
+.wizard .changedLocally {background:#80ff80;}
+.wizard .changedServer {background:#8080ff;}
+.wizard .changedBoth {background:#ff8080;}
+.wizard .notFound {background:#ffff80;}
+.wizard .putToServer {background:#ff80ff;}
+.wizard .gotFromServer {background:#80ffff;}
+
+#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
+#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}
+
+.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}
+
+.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
+.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
+.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
+.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
+.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
+.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
+.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}
+
+.tiddler .defaultCommand {font-weight:bold;}
+
+.shadow .title {color:[[ColorPalette::TertiaryDark]];}
+
+.title {color:[[ColorPalette::SecondaryDark]];}
+.subtitle {color:[[ColorPalette::TertiaryDark]];}
+
+.toolbar {color:[[ColorPalette::PrimaryMid]];}
+.toolbar a {color:[[ColorPalette::TertiaryLight]];}
+.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
+.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}
+
+.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
+.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
+.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
+.tagging .button, .tagged .button {border:none;}
+
+.footer {color:[[ColorPalette::TertiaryLight]];}
+.selected .footer {color:[[ColorPalette::TertiaryMid]];}
+
+.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
+.sparktick {background:[[ColorPalette::PrimaryDark]];}
+
+.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
+.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
+.lowlight {background:[[ColorPalette::TertiaryLight]];}
+
+.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}
+
+.imageLink, #displayArea .imageLink {background:transparent;}
+
+.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}
+
+.viewer .listTitle {list-style-type:none; margin-left:-2em;}
+.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
+.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}
+
+.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
+.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
+.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}
+
+.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
+.viewer code {color:[[ColorPalette::SecondaryDark]];}
+.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}
+
+.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}
+
+.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
+.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
+.editorFooter {color:[[ColorPalette::TertiaryMid]];}
+
+#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
+#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
+#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
+#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
+#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
+#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
+.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
+.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
+#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity:60)';}
+/*}}}*/
+
+
+
/*{{{*/
+* html .tiddler {height:1%;}
+
+body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}
+
+h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
+h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
+h4,h5,h6 {margin-top:1em;}
+h1 {font-size:1.35em;}
+h2 {font-size:1.25em;}
+h3 {font-size:1.1em;}
+h4 {font-size:1em;}
+h5 {font-size:.9em;}
+
+hr {height:1px;}
+
+a {text-decoration:none;}
+
+dt {font-weight:bold;}
+
+ol {list-style-type:decimal;}
+ol ol {list-style-type:lower-alpha;}
+ol ol ol {list-style-type:lower-roman;}
+ol ol ol ol {list-style-type:decimal;}
+ol ol ol ol ol {list-style-type:lower-alpha;}
+ol ol ol ol ol ol {list-style-type:lower-roman;}
+ol ol ol ol ol ol ol {list-style-type:decimal;}
+
+.txtOptionInput {width:11em;}
+
+#contentWrapper .chkOptionInput {border:0;}
+
+.externalLink {text-decoration:underline;}
+
+.indent {margin-left:3em;}
+.outdent {margin-left:3em; text-indent:-3em;}
+code.escaped {white-space:nowrap;}
+
+.tiddlyLinkExisting {font-weight:bold;}
+.tiddlyLinkNonExisting {font-style:italic;}
+
+/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
+a.tiddlyLinkNonExisting.shadow {font-weight:bold;}
+
+#mainMenu .tiddlyLinkExisting,
+	#mainMenu .tiddlyLinkNonExisting,
+	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
+#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}
+
+.header {position:relative;}
+.header a:hover {background:transparent;}
+.headerShadow {position:relative; padding:4.5em 0em 1em 1em; left:-1px; top:-1px;}
+.headerForeground {position:absolute; padding:4.5em 0em 1em 1em; left:0px; top:0px;}
+
+.siteTitle {font-size:3em;}
+.siteSubtitle {font-size:1.2em;}
+
+#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}
+
+#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
+#sidebarOptions {padding-top:0.3em;}
+#sidebarOptions a {margin:0em 0.2em; padding:0.2em 0.3em; display:block;}
+#sidebarOptions input {margin:0.4em 0.5em;}
+#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
+#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
+#sidebarOptions .sliderPanel input {margin:0 0 .3em 0;}
+#sidebarTabs .tabContents {width:15em; overflow:hidden;}
+
+.wizard {padding:0.1em 1em 0em 2em;}
+.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
+.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
+.wizardStep {padding:1em 1em 1em 1em;}
+.wizard .button {margin:0.5em 0em 0em 0em; font-size:1.2em;}
+.wizardFooter {padding:0.8em 0.4em 0.8em 0em;}
+.wizardFooter .status {padding:0em 0.4em 0em 0.4em; margin-left:1em;}
+.wizard .button {padding:0.1em 0.2em 0.1em 0.2em;}
+
+#messageArea {position:fixed; top:2em; right:0em; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
+.messageToolbar {display:block; text-align:right; padding:0.2em 0.2em 0.2em 0.2em;}
+#messageArea a {text-decoration:underline;}
+
+.tiddlerPopupButton {padding:0.2em 0.2em 0.2em 0.2em;}
+.popupTiddler {position: absolute; z-index:300; padding:1em 1em 1em 1em; margin:0;}
+
+.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
+.popup .popupMessage {padding:0.4em;}
+.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0em;}
+.popup li.disabled {padding:0.4em;}
+.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
+.listBreak {font-size:1px; line-height:1px;}
+.listBreak div {margin:2px 0;}
+
+.tabset {padding:1em 0em 0em 0.5em;}
+.tab {margin:0em 0em 0em 0.25em; padding:2px;}
+.tabContents {padding:0.5em;}
+.tabContents ul, .tabContents ol {margin:0; padding:0;}
+.txtMainTab .tabContents li {list-style:none;}
+.tabContents li.listLink { margin-left:.75em;}
+
+#contentWrapper {display:block;}
+#splashScreen {display:none;}
+
+#displayArea {margin:1em 17em 0em 14em;}
+
+.toolbar {text-align:right; font-size:.9em;}
+
+.tiddler {padding:1em 1em 0em 1em;}
+
+.missing .viewer,.missing .title {font-style:italic;}
+
+.title {font-size:1.6em; font-weight:bold;}
+
+.missing .subtitle {display:none;}
+.subtitle {font-size:1.1em;}
+
+.tiddler .button {padding:0.2em 0.4em;}
+
+.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
+.isTag .tagging {display:block;}
+.tagged {margin:0.5em; float:right;}
+.tagging, .tagged {font-size:0.9em; padding:0.25em;}
+.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
+.tagClear {clear:both;}
+
+.footer {font-size:.9em;}
+.footer li {display:inline;}
+
+.annotation {padding:0.5em; margin:0.5em;}
+
+* html .viewer pre {width:99%; padding:0 0 1em 0;}
+.viewer {line-height:1.4em; padding-top:0.5em;}
+.viewer .button {margin:0em 0.25em; padding:0em 0.25em;}
+.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
+.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}
+
+.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
+.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
+table.listView {font-size:0.85em; margin:0.8em 1.0em;}
+table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}
+
+.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
+.viewer code {font-size:1.2em; line-height:1.4em;}
+
+.editor {font-size:1.1em;}
+.editor input, .editor textarea {display:block; width:100%; font:inherit;}
+.editorFooter {padding:0.25em 0em; font-size:.9em;}
+.editorFooter .button {padding-top:0px; padding-bottom:0px;}
+
+.fieldsetFix {border:0; padding:0; margin:1px 0px 1px 0px;}
+
+.sparkline {line-height:1em;}
+.sparktick {outline:0;}
+
+.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
+.zoomer div {padding:1em;}
+
+* html #backstage {width:99%;}
+* html #backstageArea {width:99%;}
+#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em 0.3em 0.5em;}
+#backstageToolbar {position:relative;}
+#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em 0.3em 0.5em;}
+#backstageButton {display:none; position:absolute; z-index:175; top:0em; right:0em;}
+#backstageButton a {padding:0.1em 0.4em 0.1em 0.4em; margin:0.1em 0.1em 0.1em 0.1em;}
+#backstage {position:relative; width:100%; z-index:50;}
+#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin:0em 3em 0em 3em; padding:1em 1em 1em 1em;}
+.backstagePanelFooter {padding-top:0.2em; float:right;}
+.backstagePanelFooter a {padding:0.2em 0.4em 0.2em 0.4em;}
+#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}
+
+.whenBackstage {display:none;}
+.backstageVisible .whenBackstage {display:block;}
+/*}}}*/
+
+
+
/***
+StyleSheet for use when a translation requires any css style changes.
+This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
+***/
+/*{{{*/
+body {font-size:0.8em;}
+#sidebarOptions {font-size:1.05em;}
+#sidebarOptions a {font-style:normal;}
+#sidebarOptions .sliderPanel {font-size:0.95em;}
+.subtitle {font-size:0.8em;}
+.viewer table.listView {font-size:0.95em;}
+/*}}}*/
+
+
+
/*{{{*/
+@media print {
+#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none ! important;}
+#displayArea {margin: 1em 1em 0em 1em;}
+/* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
+noscript {display:none;}
+}
+/*}}}*/
+
+
+
<!--{{{-->
+<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
+<div class='headerShadow'>
+<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
+<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
+</div>
+<div class='headerForeground'>
+<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
+<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
+</div>
+</div>
+<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
+<div id='sidebar'>
+<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
+<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
+</div>
+<div id='displayArea'>
+<div id='messageArea'></div>
+<div id='tiddlerDisplay'></div>
+</div>
+<!--}}}-->
+
+
+
<!--{{{-->
+<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
+<div class='title' macro='view title'></div>
+<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
+<div class='tagging' macro='tagging'></div>
+<div class='tagged' macro='tags'></div>
+<div class='viewer' macro='view text wikified'></div>
+<div class='tagClear'></div>
+<!--}}}-->
+
+
+
<!--{{{-->
+<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
+<div class='title' macro='view title'></div>
+<div class='editor' macro='edit title'></div>
+<div macro='annotations'></div>
+<div class='editor' macro='edit text'></div>
+<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser excludeLists'></span></div>
+<!--}}}-->
+
+
+
To get started with this blank TiddlyWiki, you'll need to modify the following tiddlers:
+* SiteTitle & SiteSubtitle: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
+* MainMenu: The menu (usually on the left)
+* DefaultTiddlers: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
+You'll also need to enter your username for signing your edits: <<option txtUserName>>
+
+
+
These InterfaceOptions for customising TiddlyWiki are saved in your browser
+
+Your username for signing your edits. Write it as a WikiWord (eg JoeBloggs)
+
+<<option txtUserName>>
+<<option chkSaveBackups>> SaveBackups
+<<option chkAutoSave>> AutoSave
+<<option chkRegExpSearch>> RegExpSearch
+<<option chkCaseSensitiveSearch>> CaseSensitiveSearch
+<<option chkAnimate>> EnableAnimations
+
+----
+Also see [[AdvancedOptions]]
+
+
+
<<importTiddlers>>
+
+
+ +
+
+ + + + + + + + + + + diff --git a/htmlview.py b/htmlview.py new file mode 100755 index 0000000..c14a461 --- /dev/null +++ b/htmlview.py @@ -0,0 +1,27 @@ +import os +import gtk +import hulahop +from sugar import env +from sugar.activity import activity +from path import path +hulahop.startup(os.path.join(env.get_profile_path(), 'gecko')) + +from hulahop.webview import WebView + +BUNDLEPATH = path(activity.get_bundle_path()) / 'tw' +DATAPATH = path(activity.get_activity_root()) / 'data' +TESTFILE = BUNDLEPATH / 'slides.html' +WORKFILE = 'file://' + DATAPATH / 'slides.html' + +class Htmlview(gtk.VBox): + def __init__(self): + gtk.VBox.__init__(self) + #vbox = gtk.VBox(False, 8) + wv = WebView() + print 'show', WORKFILE, path(WORKFILE).exists() + wv.load_uri(WORKFILE) + wv.show() + self.pack_start(wv, True, True, 0) + #self.add(wv) + self.show_all() + diff --git a/hulahop_test.py b/hulahop_test.py new file mode 100755 index 0000000..24a3067 --- /dev/null +++ b/hulahop_test.py @@ -0,0 +1,22 @@ +import os +import hulahop +from sugar import env +hulahop.startup(os.path.join(env.get_profile_path(), 'gecko')) + +from hulahop.webview import WebView + +import gtk + +win = gtk.Window(gtk.WINDOW_TOPLEVEL) +win.set_size_request(800,600) +win.connect('destroy', gtk.main_quit) +wv = WebView() +#wv.load_uri('file:///home/olpc/Activities/ShowNTell.activity/tw/simple.html') +wv.load_uri('file:///home/olpc/Activities/ShowNTell.activity/tw/slides.html') +wv.show() + +win.add(wv) + +win.show() +gtk.main() + diff --git a/icons/Fileclose-2.0.svg b/icons/Fileclose-2.0.svg new file mode 100755 index 0000000..e06f112 --- /dev/null +++ b/icons/Fileclose-2.0.svg @@ -0,0 +1,124 @@ + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/Filenew.svg b/icons/Filenew.svg new file mode 100755 index 0000000..ba27ce2 --- /dev/null +++ b/icons/Filenew.svg @@ -0,0 +1,151 @@ + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/Icon-move.svg b/icons/Icon-move.svg new file mode 100755 index 0000000..a68267d --- /dev/null +++ b/icons/Icon-move.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + Edit Redo + + + + edit + redo + again + reapply + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/Icon-remove.svg b/icons/Icon-remove.svg new file mode 100755 index 0000000..bb17023 --- /dev/null +++ b/icons/Icon-remove.svg @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Remove + 2006-01-04 + + + Andreas Nilsson + + + http://tango-project.org + + + remove + delete + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/activity-journal.svg b/icons/activity-journal.svg new file mode 100755 index 0000000..8145b48 --- /dev/null +++ b/icons/activity-journal.svg @@ -0,0 +1,31 @@ + + + + + +]> + + + + + + + + + + + + + + diff --git a/icons/black-button.svg b/icons/black-button.svg new file mode 100755 index 0000000..f84b016 --- /dev/null +++ b/icons/black-button.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons/blue-button.svg b/icons/blue-button.svg new file mode 100755 index 0000000..451f0a6 --- /dev/null +++ b/icons/blue-button.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/choose.svg b/icons/choose.svg new file mode 100755 index 0000000..2fc648e --- /dev/null +++ b/icons/choose.svg @@ -0,0 +1,191 @@ + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/done.svg b/icons/done.svg new file mode 100755 index 0000000..14d3b5d --- /dev/null +++ b/icons/done.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/green-button.svg b/icons/green-button.svg new file mode 100755 index 0000000..404a745 --- /dev/null +++ b/icons/green-button.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/locked.svg b/icons/locked.svg new file mode 100755 index 0000000..99c2fd2 --- /dev/null +++ b/icons/locked.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/icons/media-flash-sd-mmc.svg b/icons/media-flash-sd-mmc.svg new file mode 100755 index 0000000..b5d37ad --- /dev/null +++ b/icons/media-flash-sd-mmc.svg @@ -0,0 +1,9 @@ + + +]> + + + + + \ No newline at end of file diff --git a/icons/media-flash-usb.svg b/icons/media-flash-usb.svg new file mode 100755 index 0000000..2fd43a1 --- /dev/null +++ b/icons/media-flash-usb.svg @@ -0,0 +1,9 @@ + + +]> + + + + + \ No newline at end of file diff --git a/icons/new-slideshow.svg b/icons/new-slideshow.svg new file mode 100644 index 0000000..2bda67c --- /dev/null +++ b/icons/new-slideshow.svg @@ -0,0 +1,462 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/new.svg b/icons/new.svg new file mode 100755 index 0000000..f6bc61a --- /dev/null +++ b/icons/new.svg @@ -0,0 +1,214 @@ + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/red-button.svg b/icons/red-button.svg new file mode 100755 index 0000000..430b09a --- /dev/null +++ b/icons/red-button.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/slideshow.svg b/icons/slideshow.svg new file mode 100755 index 0000000..d57facb --- /dev/null +++ b/icons/slideshow.svg @@ -0,0 +1,431 @@ + + +image/svg+xml + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/text.svg b/icons/text.svg new file mode 100755 index 0000000..ddfee40 --- /dev/null +++ b/icons/text.svg @@ -0,0 +1,68 @@ + + +image/svg+xml + + + + \ No newline at end of file diff --git a/icons/tool-brush.svg b/icons/tool-brush.svg new file mode 100755 index 0000000..e888321 --- /dev/null +++ b/icons/tool-brush.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/icons/tool-eraser.svg b/icons/tool-eraser.svg new file mode 100755 index 0000000..41fc143 --- /dev/null +++ b/icons/tool-eraser.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/icons/tool-pencil.svg b/icons/tool-pencil.svg new file mode 100755 index 0000000..c7a1ef9 --- /dev/null +++ b/icons/tool-pencil.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/icons/unlocked.svg b/icons/unlocked.svg new file mode 100755 index 0000000..9e47d87 --- /dev/null +++ b/icons/unlocked.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ink.py b/ink.py new file mode 100755 index 0000000..544bc78 --- /dev/null +++ b/ink.py @@ -0,0 +1,61 @@ +# ink.py +# +# B. Mayton +# +# 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. +# +# -*- mode:python; tab-width:4; indent-tabs-mode:t; -*- + +# 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 random +import logging + +class Path: + + def __init__(self, inkstr=None): + self.__logger = logging.getLogger('Path') + self.points=[] + self.color = (0,0,1.0) + self.pen = 4 + self.uid = random.randint(0, 2147483647) + if inkstr: + try: + i=0 + parts = inkstr.split('#') + if len(parts) > 1: + params = parts[i].split(';') + self.uid = int(params[0]) + colorparts = params[1].split(',') + self.color = (float(colorparts[0]),float(colorparts[1]),float(colorparts[2])) + self.pen = float(params[2]) + i = i + 1 + pathstr = parts[i] + pointstrs = pathstr.split(';') + for pointstr in pointstrs: + pparts = pointstr.split(',') + if len(pparts) == 2: + self.add((int(pparts[0]), int(pparts[1]))) + except Exception, e: + self.__logger.debug('Could not unserialize ink string (old ink?)') + + def add(self, point): + self.points.append(point) + + def __str__(self): + s = str(self.uid) + ";" + s = s + str(self.color[0]) + "," + str(self.color[1]) + "," + str(self.color[2]) + ";" + s = s + str(self.pen) + "#" + for p in self.points: + s = s + str(int(p[0])) + "," + str(int(p[1])) + ";" + return s diff --git a/listview.py b/listview.py new file mode 100644 index 0000000..bf43040 --- /dev/null +++ b/listview.py @@ -0,0 +1,103 @@ +#!/usr/bin/python + +# ZetCode PyGTK tutorial +# +# This example shows a TreeView widget +# in a list view mode +# +# author: jan bodnar +# website: zetcode.com +# last edited: February 2009 + +import sys, os +import gtk +from sugar.datastore import datastore +from path import path + + +class Listview(gtk.VBox): + def __init__(self, activity, deck): + self.activity = activity + gtk.VBox.__init__(self) + vbox = gtk.VBox(False, 8) + sw = gtk.ScrolledWindow() + sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self.pack_start(sw, True, True, 0) + treeView = gtk.TreeView() + treeView.connect("row-activated", self.on_activated) + treeView.set_rules_hint(True) + sw.add(treeView) + self.create_columns(treeView) + self.treeView = treeView + self.deck = deck + self.add(vbox) + self.show_all() + + def create_columns(self, treeView): + + rendererText = gtk.CellRendererText() + column = gtk.TreeViewColumn("Title", rendererText, text=0) + column.set_sort_column_id(0) + treeView.append_column(column) + rendererText = gtk.CellRendererText() + column = gtk.TreeViewColumn("Mime_type", rendererText, text=1) + column.set_sort_column_id(1) + treeView.append_column(column) + rendererText = gtk.CellRendererText() + column = gtk.TreeViewColumn("Date", rendererText, text=2) + column.set_sort_column_id(2) + treeView.append_column(column) + + def get_treeView(self): + return self.treeView + + def set_store(self, mountpoint, pth): + print 'set_store', mountpoint, pth + store = gtk.ListStore(str, str, str) + #get objects from the local datastore + ds_objects, num_objects = datastore.find({'mountpoints':[mountpoint], 'mime_type':['image/jpg', 'image/png', 'image/svg']}) + for f in ds_objects: + try: + title = f.metadata['title'] + except: + title = "" + try: + mime_type = f.metadata['mime_type'] + except: + mime_type = 'unknown' + try: + timestamp = f.metadata['timestamp'] + except: + timestamp = "0" + store.append([title, mime_type, timestamp]) + f.destroy() + return store + + def on_activated(self, widget, row, col): + + model = widget.get_model() + print 'row', model[row][0], model[row][1], model[row][2] + title = model[row][0] + mime_type = model[row][1] + timestamp = model[row][2] + print 'search for', title, mime_type, timestamp + if int(timestamp) > 0: + ds_objects, num_objects = datastore.find({'title':[title], 'timestamp':[timestamp]}) + else: + ds_objects, num_objects = datastore.find({'title':[title], 'mime_type': [mime_type]}) + if num_objects > 0: + object = ds_objects[0] + else: + print 'datastore find failed', f + fn = object.file_path + print 'object filename', path(fn).exists(), fn + self.deck.addSlide(fn) + self.deck.reload() + for object in ds_objects: + object.destroy() + scrn1, scrn2 = self.activity.get_window() + scrn2.hide() + scrn1.show() + + diff --git a/manifest b/manifest new file mode 100755 index 0000000..f58a5b4 --- /dev/null +++ b/manifest @@ -0,0 +1,27 @@ +classroompresenter.py +setup.py +activity/classroompresenter-activity.svg +activity/application-x-classroompresenter.svg +activity/activity.info +activity/mimetypes.xml +toolbars.py +slideshow.py +slideviewer.py +sliderenderer.py +textarea.py +utils.py +sidebar.py +shared.py +sharedslides.py +ink.py +resources/splash.svg +icons/black-button.svg +icons/blue-button.svg +icons/green-button.svg +icons/locked.svg +icons/red-button.svg +icons/text.svg +icons/tool-brush.svg +icons/tool-eraser.svg +icons/tool-pencil.svg +icons/unlocked.svg diff --git a/news b/news new file mode 100755 index 0000000..fbe3682 --- /dev/null +++ b/news @@ -0,0 +1 @@ +empty file for now; sugar complains if it's not here diff --git a/path.py b/path.py new file mode 100755 index 0000000..01c2c04 --- /dev/null +++ b/path.py @@ -0,0 +1,971 @@ +""" path.py - An object representing a path to a file or directory. + +Example: + +from path import path +d = path('/home/guido/bin') +for f in d.files('*.py'): + f.chmod(0755) + +This module requires Python 2.2 or later. + + +URL: http://www.jorendorff.com/articles/python/path +Author: Jason Orendorff (and others - see the url!) +Date: 9 Mar 2007 +""" + + +# TODO +# - Tree-walking functions don't avoid symlink loops. Matt Harrison +# sent me a patch for this. +# - Bug in write_text(). It doesn't support Universal newline mode. +# - Better error message in listdir() when self isn't a +# directory. (On Windows, the error message really sucks.) +# - Make sure everything has a good docstring. +# - Add methods for regex find and replace. +# - guess_content_type() method? +# - Perhaps support arguments to touch(). + +from __future__ import generators + +import sys, warnings, os, fnmatch, glob, shutil, codecs, md5 + +__version__ = '2.2' +__all__ = ['path'] + +# Platform-specific support for path.owner +if os.name == 'nt': + try: + import win32security + except ImportError: + win32security = None +else: + try: + import pwd + except ImportError: + pwd = None + +# Pre-2.3 support. Are unicode filenames supported? +_base = str +_getcwd = os.getcwd +try: + if os.path.supports_unicode_filenames: + _base = unicode + _getcwd = os.getcwdu +except AttributeError: + pass + +# Pre-2.3 workaround for booleans +try: + True, False +except NameError: + True, False = 1, 0 + +# Pre-2.3 workaround for basestring. +try: + basestring +except NameError: + basestring = (str, unicode) + +# Universal newline support +_textmode = 'r' +if hasattr(file, 'newlines'): + _textmode = 'U' + + +class TreeWalkWarning(Warning): + pass + +class path(_base): + """ Represents a filesystem path. + + For documentation on individual methods, consult their + counterparts in os.path. + """ + + # --- Special Python methods. + + def __repr__(self): + return 'path(%s)' % _base.__repr__(self) + + # Adding a path and a string yields a path. + def __add__(self, more): + try: + resultStr = _base.__add__(self, more) + except TypeError: #Python bug + resultStr = NotImplemented + if resultStr is NotImplemented: + return resultStr + return self.__class__(resultStr) + + def __radd__(self, other): + if isinstance(other, basestring): + return self.__class__(other.__add__(self)) + else: + return NotImplemented + + # The / operator joins paths. + def __div__(self, rel): + """ fp.__div__(rel) == fp / rel == fp.joinpath(rel) + + Join two path components, adding a separator character if + needed. + """ + return self.__class__(os.path.join(self, rel)) + + # Make the / operator work even when true division is enabled. + __truediv__ = __div__ + + def getcwd(cls): + """ Return the current working directory as a path object. """ + return cls(_getcwd()) + getcwd = classmethod(getcwd) + + + # --- Operations on path strings. + + isabs = os.path.isabs + def abspath(self): return self.__class__(os.path.abspath(self)) + def normcase(self): return self.__class__(os.path.normcase(self)) + def normpath(self): return self.__class__(os.path.normpath(self)) + def realpath(self): return self.__class__(os.path.realpath(self)) + def expanduser(self): return self.__class__(os.path.expanduser(self)) + def expandvars(self): return self.__class__(os.path.expandvars(self)) + def dirname(self): return self.__class__(os.path.dirname(self)) + basename = os.path.basename + + def expand(self): + """ Clean up a filename by calling expandvars(), + expanduser(), and normpath() on it. + + This is commonly everything needed to clean up a filename + read from a configuration file, for example. + """ + return self.expandvars().expanduser().normpath() + + def _get_namebase(self): + base, ext = os.path.splitext(self.name) + return base + + def _get_ext(self): + f, ext = os.path.splitext(_base(self)) + return ext + + def _get_drive(self): + drive, r = os.path.splitdrive(self) + return self.__class__(drive) + + parent = property( + dirname, None, None, + """ This path's parent directory, as a new path object. + + For example, path('/usr/local/lib/libpython.so').parent == path('/usr/local/lib') + """) + + name = property( + basename, None, None, + """ The name of this file or directory without the full path. + + For example, path('/usr/local/lib/libpython.so').name == 'libpython.so' + """) + + namebase = property( + _get_namebase, None, None, + """ The same as path.name, but with one file extension stripped off. + + For example, path('/home/guido/python.tar.gz').name == 'python.tar.gz', + but path('/home/guido/python.tar.gz').namebase == 'python.tar' + """) + + ext = property( + _get_ext, None, None, + """ The file extension, for example '.py'. """) + + drive = property( + _get_drive, None, None, + """ The drive specifier, for example 'C:'. + This is always empty on systems that don't use drive specifiers. + """) + + def splitpath(self): + """ p.splitpath() -> Return (p.parent, p.name). """ + parent, child = os.path.split(self) + return self.__class__(parent), child + + def splitdrive(self): + """ p.splitdrive() -> Return (p.drive, ). + + Split the drive specifier from this path. If there is + no drive specifier, p.drive is empty, so the return value + is simply (path(''), p). This is always the case on Unix. + """ + drive, rel = os.path.splitdrive(self) + return self.__class__(drive), rel + + def splitext(self): + """ p.splitext() -> Return (p.stripext(), p.ext). + + Split the filename extension from this path and return + the two parts. Either part may be empty. + + The extension is everything from '.' to the end of the + last path segment. This has the property that if + (a, b) == p.splitext(), then a + b == p. + """ + filename, ext = os.path.splitext(self) + return self.__class__(filename), ext + + def stripext(self): + """ p.stripext() -> Remove one file extension from the path. + + For example, path('/home/guido/python.tar.gz').stripext() + returns path('/home/guido/python.tar'). + """ + return self.splitext()[0] + + if hasattr(os.path, 'splitunc'): + def splitunc(self): + unc, rest = os.path.splitunc(self) + return self.__class__(unc), rest + + def _get_uncshare(self): + unc, r = os.path.splitunc(self) + return self.__class__(unc) + + uncshare = property( + _get_uncshare, None, None, + """ The UNC mount point for this path. + This is empty for paths on local drives. """) + + def joinpath(self, *args): + """ Join two or more path components, adding a separator + character (os.sep) if needed. Returns a new path + object. + """ + return self.__class__(os.path.join(self, *args)) + + def splitall(self): + r""" Return a list of the path components in this path. + + The first item in the list will be a path. Its value will be + either os.curdir, os.pardir, empty, or the root directory of + this path (for example, '/' or 'C:\\'). The other items in + the list will be strings. + + path.path.joinpath(*result) will yield the original path. + """ + parts = [] + loc = self + while loc != os.curdir and loc != os.pardir: + prev = loc + loc, child = prev.splitpath() + if loc == prev: + break + parts.append(child) + parts.append(loc) + parts.reverse() + return parts + + def relpath(self): + """ Return this path as a relative path, + based from the current working directory. + """ + cwd = self.__class__(os.getcwd()) + return cwd.relpathto(self) + + def relpathto(self, dest): + """ Return a relative path from self to dest. + + If there is no relative path from self to dest, for example if + they reside on different drives in Windows, then this returns + dest.abspath(). + """ + origin = self.abspath() + dest = self.__class__(dest).abspath() + + orig_list = origin.normcase().splitall() + # Don't normcase dest! We want to preserve the case. + dest_list = dest.splitall() + + if orig_list[0] != os.path.normcase(dest_list[0]): + # Can't get here from there. + return dest + + # Find the location where the two paths start to differ. + i = 0 + for start_seg, dest_seg in zip(orig_list, dest_list): + if start_seg != os.path.normcase(dest_seg): + break + i += 1 + + # Now i is the point where the two paths diverge. + # Need a certain number of "os.pardir"s to work up + # from the origin to the point of divergence. + segments = [os.pardir] * (len(orig_list) - i) + # Need to add the diverging part of dest_list. + segments += dest_list[i:] + if len(segments) == 0: + # If they happen to be identical, use os.curdir. + relpath = os.curdir + else: + relpath = os.path.join(*segments) + return self.__class__(relpath) + + # --- Listing, searching, walking, and matching + + def listdir(self, pattern=None): + """ D.listdir() -> List of items in this directory. + + Use D.files() or D.dirs() instead if you want a listing + of just files or just subdirectories. + + The elements of the list are path objects. + + With the optional 'pattern' argument, this only lists + items whose names match the given pattern. + """ + names = os.listdir(self) + if pattern is not None: + names = fnmatch.filter(names, pattern) + return [self / child for child in names] + + def dirs(self, pattern=None): + """ D.dirs() -> List of this directory's subdirectories. + + The elements of the list are path objects. + This does not walk recursively into subdirectories + (but see path.walkdirs). + + With the optional 'pattern' argument, this only lists + directories whose names match the given pattern. For + example, d.dirs('build-*'). + """ + return [p for p in self.listdir(pattern) if p.isdir()] + + def files(self, pattern=None): + """ D.files() -> List of the files in this directory. + + The elements of the list are path objects. + This does not walk into subdirectories (see path.walkfiles). + + With the optional 'pattern' argument, this only lists files + whose names match the given pattern. For example, + d.files('*.pyc'). + """ + + return [p for p in self.listdir(pattern) if p.isfile()] + + def walk(self, pattern=None, errors='strict'): + """ D.walk() -> iterator over files and subdirs, recursively. + + The iterator yields path objects naming each child item of + this directory and its descendants. This requires that + D.isdir(). + + This performs a depth-first traversal of the directory tree. + Each directory is returned just before all its children. + + The errors= keyword argument controls behavior when an + error occurs. The default is 'strict', which causes an + exception. The other allowed values are 'warn', which + reports the error via warnings.warn(), and 'ignore'. + """ + if errors not in ('strict', 'warn', 'ignore'): + raise ValueError("invalid errors parameter") + + try: + childList = self.listdir() + except Exception: + if errors == 'ignore': + return + elif errors == 'warn': + warnings.warn( + "Unable to list directory '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + return + else: + raise + + for child in childList: + if pattern is None or child.fnmatch(pattern): + yield child + try: + isdir = child.isdir() + except Exception: + if errors == 'ignore': + isdir = False + elif errors == 'warn': + warnings.warn( + "Unable to access '%s': %s" + % (child, sys.exc_info()[1]), + TreeWalkWarning) + isdir = False + else: + raise + + if isdir: + for item in child.walk(pattern, errors): + yield item + + def walkdirs(self, pattern=None, errors='strict'): + """ D.walkdirs() -> iterator over subdirs, recursively. + + With the optional 'pattern' argument, this yields only + directories whose names match the given pattern. For + example, mydir.walkdirs('*test') yields only directories + with names ending in 'test'. + + The errors= keyword argument controls behavior when an + error occurs. The default is 'strict', which causes an + exception. The other allowed values are 'warn', which + reports the error via warnings.warn(), and 'ignore'. + """ + if errors not in ('strict', 'warn', 'ignore'): + raise ValueError("invalid errors parameter") + + try: + dirs = self.dirs() + except Exception: + if errors == 'ignore': + return + elif errors == 'warn': + warnings.warn( + "Unable to list directory '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + return + else: + raise + + for child in dirs: + if pattern is None or child.fnmatch(pattern): + yield child + for subsubdir in child.walkdirs(pattern, errors): + yield subsubdir + + def walkfiles(self, pattern=None, errors='strict'): + """ D.walkfiles() -> iterator over files in D, recursively. + + The optional argument, pattern, limits the results to files + with names that match the pattern. For example, + mydir.walkfiles('*.tmp') yields only files with the .tmp + extension. + """ + if errors not in ('strict', 'warn', 'ignore'): + raise ValueError("invalid errors parameter") + + try: + childList = self.listdir() + except Exception: + if errors == 'ignore': + return + elif errors == 'warn': + warnings.warn( + "Unable to list directory '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + return + else: + raise + + for child in childList: + try: + isfile = child.isfile() + isdir = not isfile and child.isdir() + except: + if errors == 'ignore': + continue + elif errors == 'warn': + warnings.warn( + "Unable to access '%s': %s" + % (self, sys.exc_info()[1]), + TreeWalkWarning) + continue + else: + raise + + if isfile: + if pattern is None or child.fnmatch(pattern): + yield child + elif isdir: + for f in child.walkfiles(pattern, errors): + yield f + + def fnmatch(self, pattern): + """ Return True if self.name matches the given pattern. + + pattern - A filename pattern with wildcards, + for example '*.py'. + """ + return fnmatch.fnmatch(self.name, pattern) + + def glob(self, pattern): + """ Return a list of path objects that match the pattern. + + pattern - a path relative to this directory, with wildcards. + + For example, path('/users').glob('*/bin/*') returns a list + of all the files users have in their bin directories. + """ + cls = self.__class__ + return [cls(s) for s in glob.glob(_base(self / pattern))] + + + # --- Reading or writing an entire file at once. + + def open(self, mode='r'): + """ Open this file. Return a file object. """ + return file(self, mode) + + def bytes(self): + """ Open this file, read all bytes, return them as a string. """ + f = self.open('rb') + try: + return f.read() + finally: + f.close() + + def write_bytes(self, bytes, append=False): + """ Open this file and write the given bytes to it. + + Default behavior is to overwrite any existing file. + Call p.write_bytes(bytes, append=True) to append instead. + """ + if append: + mode = 'ab' + else: + mode = 'wb' + f = self.open(mode) + try: + f.write(bytes) + finally: + f.close() + + def text(self, encoding=None, errors='strict'): + r""" Open this file, read it in, return the content as a string. + + This uses 'U' mode in Python 2.3 and later, so '\r\n' and '\r' + are automatically translated to '\n'. + + Optional arguments: + + encoding - The Unicode encoding (or character set) of + the file. If present, the content of the file is + decoded and returned as a unicode object; otherwise + it is returned as an 8-bit str. + errors - How to handle Unicode errors; see help(str.decode) + for the options. Default is 'strict'. + """ + if encoding is None: + # 8-bit + f = self.open(_textmode) + try: + return f.read() + finally: + f.close() + else: + # Unicode + f = codecs.open(self, 'r', encoding, errors) + # (Note - Can't use 'U' mode here, since codecs.open + # doesn't support 'U' mode, even in Python 2.3.) + try: + t = f.read() + finally: + f.close() + return (t.replace(u'\r\n', u'\n') + .replace(u'\r\x85', u'\n') + .replace(u'\r', u'\n') + .replace(u'\x85', u'\n') + .replace(u'\u2028', u'\n')) + + def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False): + r""" Write the given text to this file. + + The default behavior is to overwrite any existing file; + to append instead, use the 'append=True' keyword argument. + + There are two differences between path.write_text() and + path.write_bytes(): newline handling and Unicode handling. + See below. + + Parameters: + + - text - str/unicode - The text to be written. + + - encoding - str - The Unicode encoding that will be used. + This is ignored if 'text' isn't a Unicode string. + + - errors - str - How to handle Unicode encoding errors. + Default is 'strict'. See help(unicode.encode) for the + options. This is ignored if 'text' isn't a Unicode + string. + + - linesep - keyword argument - str/unicode - The sequence of + characters to be used to mark end-of-line. The default is + os.linesep. You can also specify None; this means to + leave all newlines as they are in 'text'. + + - append - keyword argument - bool - Specifies what to do if + the file already exists (True: append to the end of it; + False: overwrite it.) The default is False. + + + --- Newline handling. + + write_text() converts all standard end-of-line sequences + ('\n', '\r', and '\r\n') to your platform's default end-of-line + sequence (see os.linesep; on Windows, for example, the + end-of-line marker is '\r\n'). + + If you don't like your platform's default, you can override it + using the 'linesep=' keyword argument. If you specifically want + write_text() to preserve the newlines as-is, use 'linesep=None'. + + This applies to Unicode text the same as to 8-bit text, except + there are three additional standard Unicode end-of-line sequences: + u'\x85', u'\r\x85', and u'\u2028'. + + (This is slightly different from when you open a file for + writing with fopen(filename, "w") in C or file(filename, 'w') + in Python.) + + + --- Unicode + + If 'text' isn't Unicode, then apart from newline handling, the + bytes are written verbatim to the file. The 'encoding' and + 'errors' arguments are not used and must be omitted. + + If 'text' is Unicode, it is first converted to bytes using the + specified 'encoding' (or the default encoding if 'encoding' + isn't specified). The 'errors' argument applies only to this + conversion. + + """ + if isinstance(text, unicode): + if linesep is not None: + # Convert all standard end-of-line sequences to + # ordinary newline characters. + text = (text.replace(u'\r\n', u'\n') + .replace(u'\r\x85', u'\n') + .replace(u'\r', u'\n') + .replace(u'\x85', u'\n') + .replace(u'\u2028', u'\n')) + text = text.replace(u'\n', linesep) + if encoding is None: + encoding = sys.getdefaultencoding() + bytes = text.encode(encoding, errors) + else: + # It is an error to specify an encoding if 'text' is + # an 8-bit string. + assert encoding is None + + if linesep is not None: + text = (text.replace('\r\n', '\n') + .replace('\r', '\n')) + bytes = text.replace('\n', linesep) + + self.write_bytes(bytes, append) + + def lines(self, encoding=None, errors='strict', retain=True): + r""" Open this file, read all lines, return them in a list. + + Optional arguments: + encoding - The Unicode encoding (or character set) of + the file. The default is None, meaning the content + of the file is read as 8-bit characters and returned + as a list of (non-Unicode) str objects. + errors - How to handle Unicode errors; see help(str.decode) + for the options. Default is 'strict' + retain - If true, retain newline characters; but all newline + character combinations ('\r', '\n', '\r\n') are + translated to '\n'. If false, newline characters are + stripped off. Default is True. + + This uses 'U' mode in Python 2.3 and later. + """ + if encoding is None and retain: + f = self.open(_textmode) + try: + return f.readlines() + finally: + f.close() + else: + return self.text(encoding, errors).splitlines(retain) + + def write_lines(self, lines, encoding=None, errors='strict', + linesep=os.linesep, append=False): + r""" Write the given lines of text to this file. + + By default this overwrites any existing file at this path. + + This puts a platform-specific newline sequence on every line. + See 'linesep' below. + + lines - A list of strings. + + encoding - A Unicode encoding to use. This applies only if + 'lines' contains any Unicode strings. + + errors - How to handle errors in Unicode encoding. This + also applies only to Unicode strings. + + linesep - The desired line-ending. This line-ending is + applied to every line. If a line already has any + standard line ending ('\r', '\n', '\r\n', u'\x85', + u'\r\x85', u'\u2028'), that will be stripped off and + this will be used instead. The default is os.linesep, + which is platform-dependent ('\r\n' on Windows, '\n' on + Unix, etc.) Specify None to write the lines as-is, + like file.writelines(). + + Use the keyword argument append=True to append lines to the + file. The default is to overwrite the file. Warning: + When you use this with Unicode data, if the encoding of the + existing data in the file is different from the encoding + you specify with the encoding= parameter, the result is + mixed-encoding data, which can really confuse someone trying + to read the file later. + """ + if append: + mode = 'ab' + else: + mode = 'wb' + f = self.open(mode) + try: + for line in lines: + isUnicode = isinstance(line, unicode) + if linesep is not None: + # Strip off any existing line-end and add the + # specified linesep string. + if isUnicode: + if line[-2:] in (u'\r\n', u'\x0d\x85'): + line = line[:-2] + elif line[-1:] in (u'\r', u'\n', + u'\x85', u'\u2028'): + line = line[:-1] + else: + if line[-2:] == '\r\n': + line = line[:-2] + elif line[-1:] in ('\r', '\n'): + line = line[:-1] + line += linesep + if isUnicode: + if encoding is None: + encoding = sys.getdefaultencoding() + line = line.encode(encoding, errors) + f.write(line) + finally: + f.close() + + def read_md5(self): + """ Calculate the md5 hash for this file. + + This reads through the entire file. + """ + f = self.open('rb') + try: + m = md5.new() + while True: + d = f.read(8192) + if not d: + break + m.update(d) + finally: + f.close() + return m.digest() + + # --- Methods for querying the filesystem. + + exists = os.path.exists + isdir = os.path.isdir + isfile = os.path.isfile + islink = os.path.islink + ismount = os.path.ismount + + if hasattr(os.path, 'samefile'): + samefile = os.path.samefile + + getatime = os.path.getatime + atime = property( + getatime, None, None, + """ Last access time of the file. """) + + getmtime = os.path.getmtime + mtime = property( + getmtime, None, None, + """ Last-modified time of the file. """) + + if hasattr(os.path, 'getctime'): + getctime = os.path.getctime + ctime = property( + getctime, None, None, + """ Creation time of the file. """) + + getsize = os.path.getsize + size = property( + getsize, None, None, + """ Size of the file, in bytes. """) + + if hasattr(os, 'access'): + def access(self, mode): + """ Return true if current user has access to this path. + + mode - One of the constants os.F_OK, os.R_OK, os.W_OK, os.X_OK + """ + return os.access(self, mode) + + def stat(self): + """ Perform a stat() system call on this path. """ + return os.stat(self) + + def lstat(self): + """ Like path.stat(), but do not follow symbolic links. """ + return os.lstat(self) + + def get_owner(self): + r""" Return the name of the owner of this file or directory. + + This follows symbolic links. + + On Windows, this returns a name of the form ur'DOMAIN\User Name'. + On Windows, a group can own a file or directory. + """ + if os.name == 'nt': + if win32security is None: + raise Exception("path.owner requires win32all to be installed") + desc = win32security.GetFileSecurity( + self, win32security.OWNER_SECURITY_INFORMATION) + sid = desc.GetSecurityDescriptorOwner() + account, domain, typecode = win32security.LookupAccountSid(None, sid) + return domain + u'\\' + account + else: + if pwd is None: + raise NotImplementedError("path.owner is not implemented on this platform.") + st = self.stat() + return pwd.getpwuid(st.st_uid).pw_name + + owner = property( + get_owner, None, None, + """ Name of the owner of this file or directory. """) + + if hasattr(os, 'statvfs'): + def statvfs(self): + """ Perform a statvfs() system call on this path. """ + return os.statvfs(self) + + if hasattr(os, 'pathconf'): + def pathconf(self, name): + return os.pathconf(self, name) + + + # --- Modifying operations on files and directories + + def utime(self, times): + """ Set the access and modified times of this file. """ + os.utime(self, times) + + def chmod(self, mode): + os.chmod(self, mode) + + if hasattr(os, 'chown'): + def chown(self, uid, gid): + os.chown(self, uid, gid) + + def rename(self, new): + os.rename(self, new) + + def renames(self, new): + os.renames(self, new) + + + # --- Create/delete operations on directories + + def mkdir(self, mode=0777): + os.mkdir(self, mode) + + def makedirs(self, mode=0777): + os.makedirs(self, mode) + + def rmdir(self): + os.rmdir(self) + + def removedirs(self): + os.removedirs(self) + + + # --- Modifying operations on files + + def touch(self): + """ Set the access/modified times of this file to the current time. + Create the file if it does not exist. + """ + fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0666) + os.close(fd) + os.utime(self, None) + + def remove(self): + os.remove(self) + + def unlink(self): + os.unlink(self) + + + # --- Links + + if hasattr(os, 'link'): + def link(self, newpath): + """ Create a hard link at 'newpath', pointing to this file. """ + os.link(self, newpath) + + if hasattr(os, 'symlink'): + def symlink(self, newlink): + """ Create a symbolic link at 'newlink', pointing here. """ + os.symlink(self, newlink) + + if hasattr(os, 'readlink'): + def readlink(self): + """ Return the path to which this symbolic link points. + + The result may be an absolute or a relative path. + """ + return self.__class__(os.readlink(self)) + + def readlinkabs(self): + """ Return the path to which this symbolic link points. + + The result is always an absolute path. + """ + p = self.readlink() + if p.isabs(): + return p + else: + return (self.parent / p).abspath() + + + # --- High-level functions from shutil + + copyfile = shutil.copyfile + copymode = shutil.copymode + copystat = shutil.copystat + copy = shutil.copy + copy2 = shutil.copy2 + copytree = shutil.copytree + if hasattr(shutil, 'move'): + move = shutil.move + rmtree = shutil.rmtree + + + # --- Special stuff from os + + if hasattr(os, 'chroot'): + def chroot(self): + os.chroot(self) + + if hasattr(os, 'startfile'): + def startfile(self): + os.startfile(self) + + diff --git a/readme b/readme new file mode 100755 index 0000000..7cd0899 --- /dev/null +++ b/readme @@ -0,0 +1,192 @@ +Note: 1 - record as slideN.wav in Deck_dir + 2 - add slideN.wav to 'submission' + 3 - play slideN.wav + 4 - test: record clip for three slides, verify each is playable when slide is selected + + +slideshow.py + +class Deck + set_locked_mode + set_is_initiating + getisInitiating + set_title + get_title + reload + save + rebuild_dom + getDeckPath + resizeImage + addSlide + removeSlide + moveSlide + getSlideLayers + getInstructorInk + getSelfInkOrSubmission + setActiveSubmission + getActiveSubmission + getSubmissionList + addSubmission + addInkToSlide + clearInk + clearInstructorInk + removeInstructorPathByUID + removeLocalPathByUID + doSubmit + doBroadcast + getSerializedInkSubmission + getSlideThumb + setSlideThumb + setSlideText + doNewIndex + goToIndex + getIndex + next + previous + isAtBeginning + isAtEnd + getSlideDimensionsFromXML + getSlideCount + +sharedslides.py + +class ReadHTTPRequestHandler + +class ReadHTTPServer + +class sharedSlides + + getStreamTube + handle_download_fail + list_tubes_reply_cb + list_tubes_error_cb + new_tube_cb + download_file + download_result_cb + download_progress_cb + download_error_cb + share_deck + +shared.py + +class Shared + shared_cb + joined_cb + shared_setup + deck_download_complete_cb + student_dl_complete_cb + list_tubes_reply_cb + list_tubes_error_cb + new_tube_cb + participant_change_cb + Slide_Changed + Deck_Download_Complete + Lock_Nav + Push_Initial_State + send_slide_changed_signal + slide_changed_cb + lock_nav_cb + lock_mode_switch + lock_nav + unlock_nav + send_ink_path + Add_Ink_Path + _get_buddy + add_ink_path_cb + submit_ink_cb + Send_Submission + receive_submission_cb + bcast_submission_cb + Bcast_Submission + instr_clear_ink_cb + Instructor_Clear_Ink + recv_instr_clear_ink_cb + instr_remove_ink_cb + Instructor_Remove_Ink + recv_instr_remove_ink_cb + buddy_joined_cb + buddy_left_cb + +ink.py + +class Path + + add + __str__ + +classroompresenter.py + +class ClassroomPresenter + + dl_complete_cb + do_slideview_mode + set_progress_max + do_progress_view + set_progress + read_file + write_file + get_shared_activity + +utils.py + + getFileType + copy_file + run_dialog + +toolbars.py + +class NavToolBar + activity_shared_cb + activity_joined_cb + set_lock_button + next + previous + slide_changed + num_page_activate + +class InkToolBar + instructor_state_cb + set_cur_pen + set_ink_color + set_erase + set_tool_buttons + submit_ink + broadcast_ink + reenable_submissions + undo + redo + update_buttons + +class MakeToolBar + + new + choose + save + done + +textarea.py + +class TextArea + + update_text + text_changed + render_text_area + clear_text + record + play + stop + create_bbox + +sidebar.py + +class SideBar + load_subs + sub_sel_changed + load_thumbs + change_slide + moveslide + removeslide + + + + diff --git a/resources/new.cpxo b/resources/new.cpxo new file mode 100755 index 0000000..1cefb2d --- /dev/null +++ b/resources/new.cpxo Binary files differ diff --git a/resources/splash.svg b/resources/splash.svg new file mode 100644 index 0000000..10b0a14 --- /dev/null +++ b/resources/splash.svg @@ -0,0 +1,205 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + Welcome toClassroom Presenter for the XO Laptop. + This is an empty presentation. Classroom Presenter does not yetsupport slide creation on the XO.To begin using Classroom Presenter, resume an existing slidedeck from the Journal. + Classroom Presenter XO version 1.0 Beta 1University of Washington Computer Science and EngineeringWilliam Burnside, Mathias Klous, Brian Mayton, and Kristofer Plunketthttp://xo.orderedpixels.com/http://classroompresenter.cs.washington.edu/ + + diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..77e788c --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +try: + from sugar.activity import bundlebuilder + bundlebuilder.start("ClassroomPresenter") +except ImportError: + import os + #os.system("find ./ | sed 's,^./,ClassroomPresenter.activity/,g' > MANIFEST") + os.system('rm ClassroomPresenter.xo') + os.chdir('..') + os.system('zip -r ClassroomPresenter.xo ClassroomPresenter.activity') + os.system('mv ClassroomPresenter.xo ./ClassroomPresenter.activity') + os.chdir('ClassroomPresenter.activity') diff --git a/shared.py b/shared.py new file mode 100755 index 0000000..19da1c8 --- /dev/null +++ b/shared.py @@ -0,0 +1,387 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:nil; -*- + +# shared.py +# +# Top-level class responsible for making Classroom Presenter a shared activity. +# Kris Plunkett +# +# 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 +import os +import gobject + +import telepathy +import telepathy.client + +import dbus +from dbus.service import method, signal +from dbus.gobject_service import ExportedGObject + +from sugar.presence import presenceservice +from sugar.presence.tubeconn import TubeConnection + +import utils +from sharedslides import SharedSlides + +SERVICE = "edu.washington.cs.ClassroomPresenterXO" +IFACE = SERVICE +PATH = "/edu/washington/cs/ClassroomPresenterXO" + + +class Shared(ExportedGObject): + + __gsignals__ = { + 'navigation-lock-change' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)), + 'deck-download-complete' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + def __init__(self, activity, deck, work_path): + gobject.GObject.__init__(self) + + self.__activity = activity + self.__deck = deck + self.__logger = logging.getLogger('Shared') + + self.__is_initiating = True # defaults to instructor + self.__shared_slides = None + self.__got_dbus_tube = False + self.__locked = False + self.__pservice = presenceservice.get_instance() + #self.__owner = self.__pservice.get_owner() + + self.__cpxo_path = os.path.join(work_path, 'deck.cpxo') + + self.__activity.connect('shared', self.shared_cb) + self.__activity.connect('joined', self.joined_cb) + + def shared_cb(self, activity): + """ Called when the activity is shared """ + self.__logger.debug('The activity has been shared.') + self.__is_initiating = True + self.__activity.write_file(self.__cpxo_path) + self.__deck.set_is_initiating(is_init=True) + self.shared_setup() + + def joined_cb(self, activity): + """ Called when the activity is joined """ + self.__logger.debug('Joined another activity.') + self.__is_initiating = False + self.__deck.set_is_initiating(is_init=False) + # for showing slide deck download progress + activity.do_progress_view() + activity.set_progress(0.0) + self.shared_setup() + + def shared_setup(self): + """ Called by joined_cb and shared_cb because all of this needs to happen + whether we are sharing or joining the activity """ + + self.__shared_activity = self.__activity.get_shared_activity() + + if self.__shared_activity is None: + self.__logger.error('Failed to share or join activity!') + return + + self.__tubes_chan = self.__shared_activity.telepathy_tubes_chan + self.__iface = self.__tubes_chan[telepathy.CHANNEL_TYPE_TUBES] + + self.__text_chan = self.__shared_activity.telepathy_text_chan + self.__iface_grp = self.__text_chan[telepathy.CHANNEL_INTERFACE_GROUP] + + self.__conn = self.__shared_activity.telepathy_conn + self.__my_handle = self.__conn.GetSelfHandle() + + #self.__shared_activity.connect('buddy-joined', self.buddy_joined_cb) + #self.__shared_activity.connect('buddy-left', self.buddy_left_cb) + + # takes care of downloading (and then sharing) the slide deck over stream tubes + self.__shared_slides = SharedSlides(self.__is_initiating, self.__cpxo_path, + self.__shared_activity, self.__activity.read_file) + self.__shared_slides.connect('deck-download-complete', self.deck_download_complete_cb) + + # now for the dbus tube + self.__iface.connect_to_signal('NewTube', self.new_tube_cb) + + if (self.__is_initiating): + self.__logger.debug("We are sharing, making a dbus tube and setting locked nav mode.") + self.lock_nav() + id = self.__iface.OfferDBusTube(SERVICE, {}) + else: + self.__logger.debug("We are joining, looking for the global dbus tube.") + self.__tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=self.list_tubes_reply_cb, + error_handler=self.list_tubes_error_cb) + + + """ --- START DBUS TUBE CODE --- """ + + def deck_download_complete_cb(self, object): + """ Catches the local deck_download_complete signal and sends the appropriate dbus signal """ + self.__logger.debug("Deck download is complete, sending Deck_Download_Complete dbus signal.") + self.Deck_Download_Complete() + self.emit('deck-download-complete') + + def student_dl_complete_cb(self, sender): + """ Catches the Deck_Download_Complete dbus signal from students, lets us know that they + are ready to have initial state information pushed onto them """ + self.__logger.debug("Got Deck_Download_Complete dbus signal, pushing initial state info to student.") + proxy_object = self.__dbus_tube.get_object(sender, PATH) + proxy_object.Push_Initial_State(self.__locked, self.__deck.getIndex(), + dbus_interface=IFACE) + + 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): + self.__logger.error('ListTubes() failed: %s', e) + + def new_tube_cb(self, tube_id, initiator, type, service, params, state): + self.__logger.debug('New tube: ID=%d initator=%d type=%d service=%s params=%r state=%d', + tube_id, initiator, type, service, params, state) + if (not self.__got_dbus_tube and type == telepathy.TUBE_TYPE_DBUS and service == SERVICE): + if( state == telepathy.TUBE_STATE_LOCAL_PENDING): + self.__iface.AcceptDBusTube(tube_id) + + self.__dbus_tube = TubeConnection(self.__conn, self.__iface, tube_id, + group_iface=self.__iface_grp) + self.__got_dbus_tube = True + self.__logger.debug("Got our dbus tube!") + + # lots of stuff to do once we get our tube + if (self.__is_initiating): + self.__deck.connect('slide-changed', self.send_slide_changed_signal) + self.__deck.connect('local-ink-added', self.send_ink_path) + self.__deck.connect('instructor-ink-cleared', self.instr_clear_ink_cb) + self.__deck.connect('instructor-ink-removed', self.instr_remove_ink_cb) + self.__deck.connect('ink-broadcast', self.bcast_submission_cb) + self.__dbus_tube.add_signal_receiver(self.student_dl_complete_cb, 'Deck_Download_Complete', + IFACE, path=PATH, sender_keyword='sender') + self.__dbus_tube.add_signal_receiver(self.receive_submission_cb, + 'Send_Submission', IFACE, path=PATH) + else: + self.__deck.connect('ink-submitted', self.submit_ink_cb) + self.__dbus_tube.add_signal_receiver(self.slide_changed_cb, 'Slide_Changed', + IFACE, path=PATH) + self.__dbus_tube.add_signal_receiver(self.lock_nav_cb, 'Lock_Nav', + IFACE, path=PATH) + self.__dbus_tube.add_signal_receiver(self.add_ink_path_cb, 'Add_Ink_Path', + IFACE, path=PATH) + self.__dbus_tube.add_signal_receiver(self.recv_instr_clear_ink_cb, 'Instructor_Clear_Ink', + IFACE, path=PATH) + self.__dbus_tube.add_signal_receiver(self.recv_instr_remove_ink_cb, 'Instructor_Remove_Ink', + IFACE, path=PATH) + self.__dbus_tube.add_signal_receiver(self.receive_submission_cb, + 'Bcast_Submission', IFACE, path=PATH) + + #self.__dbus_tube.watch_participants(self.participant_change_cb) + + super(Shared, self).__init__(self.__dbus_tube, PATH) + + def participant_change_cb(self, added, removed): + """ Callback on instructor XO for when someone joins or leaves the tube """ + for handle, bus_name in added: + buddy = self._get_buddy(handle) + if buddy is not None: + if handle != self.__my_handle and self.__is_initiating: + self.__logger.debug("New student joined: %s", buddy.props.nick) + + for handle in removed: + buddy = self._get_buddy(handle) + if buddy is not None: + self.__logger.debug('Buddy %s was removed' % buddy.props.nick) + + @signal(dbus_interface=IFACE, signature='u') + def Slide_Changed(self, slide_num): + """ Signals joiners to move to given slide """ + self.__logger.debug("Sending the Slide_Changed signal with slide num %d.", slide_num) + pass + + @signal(dbus_interface=IFACE, signature='') + def Deck_Download_Complete(self): + """ Signal from the student informing instructor that the deck download has finished """ + self.__logger.debug("Sending Deck_Download_Complete signal, ready for initial state info.") + pass + + @signal(dbus_interface=IFACE, signature='u') + def Lock_Nav(self, lock): + """ Signals joiners to lock or unlock navigation """ + self.__logger.debug("Sending Lock_Nav signal with bool %u", lock) + pass + + @method(dbus_interface=IFACE, in_signature='uu', out_signature='') + def Push_Initial_State(self, locked, slide_idx): + """ Called on student XO to push initial state info """ + # push current slide index and go to that slide + self.__deck.goToIndex(slide_idx, is_local=False) + + # push nav lock information + if locked: + self.lock_nav() + else: + self.unlock_nav() + + def send_slide_changed_signal(self, widget): + """ Arbitrates the sending of the Slide_Changed signal """ + self.__logger.debug("Got the slide-changed signal.") + if self.__locked: + self.__logger.debug("Navigation is locked, sending Slide_Changed to students.") + self.Slide_Changed(self.__deck.getIndex()) + + def slide_changed_cb(self, slide_idx): + """ Called on the joiners when they receive the Slide_Changed signal """ + self.__logger.debug("Received the Slide_Changed signal and changing to slide %d.", + slide_idx) + + self.__deck.goToIndex(slide_idx, is_local=False) + + def lock_nav_cb(self, lock): + """ Called on joiners when they receive the Lock_Nav signal """ + self.__logger.debug("Received the Lock_Nav signal with bool %u", lock) + if (lock): + self.lock_nav() + else: + self.unlock_nav() + + def lock_mode_switch(self, widget=None): + """ Switches the lock mode from locked to unlocked and vice versa """ + # first switch our own lock mode + if (self.__locked): + self.unlock_nav() + else: + self.lock_nav() + + # if we are instructor, tell student XOs to go into our new lock mode + if (self.__is_initiating): + self.Lock_Nav(self.__locked) + + def lock_nav(self): + self.__logger.debug("Locking navigation.") + self.__locked = True + # if we are the instructor, force students to jump to our slide + if self.__got_dbus_tube and self.__is_initiating: + self.Slide_Changed(self.__deck.getIndex()) + self.__deck.set_locked_mode(locked=True) + self.emit('navigation-lock-change', self.__locked) + + def unlock_nav(self): + self.__logger.debug("Unlocking navigation.") + self.__locked = False + self.__deck.set_locked_mode(locked=False) + self.emit('navigation-lock-change', self.__locked) + + def send_ink_path(self, widget, inkstr): + self.__logger.debug("send_ink_path called") + if (self.__is_initiating and self.__got_dbus_tube): + self.Add_Ink_Path(self.__deck.getIndex(), inkstr) + + @signal(dbus_interface=IFACE, signature='us') + def Add_Ink_Path(self, slide_idx, pathstr): + self.__logger.debug("Sending new ink path") + pass + + def _get_buddy(self, cs_handle): + """Get a Buddy from a channel specific handle.""" + self.__logger.debug('Trying to find owner of handle %u...', cs_handle) + my_csh = self.__iface_grp.GetSelfHandle() + self.__logger.debug('My handle in that group is %u', my_csh) + if my_csh == cs_handle: + handle = self.__conn.GetSelfHandle() + self.__logger.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] + self.__logger.debug('CS handle %u belongs to %u', cs_handle, handle) + else: + handle = cs_handle + self.__logger.debug('non-CS handle %u belongs to itself', handle) + # XXX: deal with failure to get the handle owner + assert handle != 0 + return self.__pservice.get_buddy_by_telepathy_handle( + self.__conn.service_name, self.__conn.object_path, handle) + + def add_ink_path_cb(self, idx, inkstr): + self.__logger.debug("Received new ink path") + self.__deck.addInkToSlide(inkstr, islocal=False, n=idx) + + def submit_ink_cb(self, widget, inks, text): + if not self.__is_initiating and self.__got_dbus_tube: + cur_idx = self.__deck.getIndex() + my_csh = self.__iface_grp.GetSelfHandle() + buddy = self._get_buddy(my_csh) + + if buddy is not None: + sender = buddy.props.nick + else: + sender = 'Unknown' + + self.__logger.debug("Sending submission: idx '%d', sender '%s'.", cur_idx, sender) + self.Send_Submission(sender, cur_idx, inks, text) + + @signal(dbus_interface=IFACE, signature='suss') + def Send_Submission(self, sender, slide_idx, inks, text): + pass + + def receive_submission_cb(self, sender, slide_idx, inks, text): + self.__logger.debug("Received submission from '%s'.", sender) + self.__deck.addSubmission(sender, inks, text, slide_idx) + + def bcast_submission_cb(self, widget, whofrom, inks, text): + if self.__is_initiating and self.__got_dbus_tube: + cur_idx = self.__deck.getIndex() + self.Bcast_Submission(whofrom, cur_idx, inks, text) + + @signal(dbus_interface=IFACE, signature='suss') + def Bcast_Submission(self, sender, slide_idx, inks, text): + pass + + def instr_clear_ink_cb(self, widget, idx): + if self.__is_initiating and self.__got_dbus_tube: + self.Instructor_Clear_Ink(idx) + + @signal(dbus_interface=IFACE, signature='u') + def Instructor_Clear_Ink(self, idx): + pass + + def recv_instr_clear_ink_cb(self, idx): + self.__deck.clearInstructorInk(idx) + + def instr_remove_ink_cb(self, widget, uid, idx): + if self.__is_initiating and self.__got_dbus_tube: + self.Instructor_Remove_Ink(uid, idx) + + @signal(dbus_interface=IFACE, signature='uu') + def Instructor_Remove_Ink(self, uid, idx): + pass + + def recv_instr_remove_ink_cb(self, uid, idx): + self.__deck.removeInstructorPathByUID(uid, idx) + + + """ --- END DBUS TUBE CODE --- """ + + # DEPRECATED + def buddy_joined_cb(self, activity, buddy): + """ Called when a buddy joins the activity """ + if self.__is_initiating is True: + utils.run_dialog("Instructor", buddy.props.nick + " has joined!") + + # DEPRECATED + def buddy_left_cb(self, activity, buddy): + """ Called when a buddy leaves the activity """ + if self.__is_initiating is True: + utils.run_dialog("Instructor", buddy.props.nick + " has left!") + +gobject.type_register(Shared) diff --git a/sharedslides.py b/sharedslides.py new file mode 100755 index 0000000..d49c4cd --- /dev/null +++ b/sharedslides.py @@ -0,0 +1,176 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:nil; -*- + +# sharedslides.py +# +# Class that performs all work relating to the sharing of slide decks and ink. +# Kris Plunkett +# +# 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 + +import sys +import os +import time +import random +import gobject + +import telepathy +import telepathy.client + +import dbus +from dbus.service import method, signal +from dbus.gobject_service import ExportedGObject + +from sugar.presence import presenceservice +from sugar import network +from sugar.presence.tubeconn import TubeConnection + +SERVICE = "edu.washington.cs.ClassroomPresenterXO" +IFACE = SERVICE +PATH = "/edu/washington/cs/ClassroomPresenterXO" + + +# Define a simple HTTP server for sharing data. +class ReadHTTPRequestHandler(network.ChunkedGlibHTTPRequestHandler): + def translate_path(self, path): + return self.server._filepath + +class ReadHTTPServer(network.GlibTCPServer): + def __init__(self, server_address, filepath): + self._filepath = filepath + network.GlibTCPServer.__init__(self, server_address, ReadHTTPRequestHandler) + + +class SharedSlides(gobject.GObject): + """ Handles all sharing of slides and ink """ + + __gsignals__ = { + 'deck-download-complete' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + def __init__(self, init, cpxo_path, shared_activity, read_file_cb): + gobject.GObject.__init__(self) + + self.__is_initiating = init + self.__cpxo_path = cpxo_path + self.__shared_activity = shared_activity + self.read_file_cb = read_file_cb + self.__logger = logging.getLogger('SharedSlides') + + self.__tubes_chan = self.__shared_activity.telepathy_tubes_chan + self.__iface = self.__tubes_chan[telepathy.CHANNEL_TYPE_TUBES] + + if (self.__is_initiating): + self.__logger.debug('Hello from SharedSlides (sharer).') + self.__have_deck = True + self.share_deck() + else: + # find a stream tube to download the slide deck from + self.__logger.debug('Hello from SharedSlides (joiner).') + self.__iface.connect_to_signal('NewTube', self.new_tube_cb) + self.__have_deck = False + self.get_stream_tube() + + def get_stream_tube(self): + """ Attempts to download the slide deck from an available stream tube """ + self.__iface.ListTubes( + reply_handler=self.list_tubes_reply_cb, + error_handler=self.list_tubes_error_cb) + + def handle_download_fail(self): + """ If an attempt to download the deck fails, this method takes care of it """ + self.__logger.error('Download failed! Sleeping five seconds and trying again.') + time.sleep(5) + self.get_stream_tube() + + 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): + self.__logger.error('ListTubes() failed: %s', e) + self.handle_download_fail + + def new_tube_cb(self, id, initiator, type, service, params, state): + self.__logger.debug('New tube: ID=%d initiator=%d type=%d service=%s params=%r state=%d', + id, initiator, type, service, params, state) + + if (not self.__have_deck and + type == telepathy.TUBE_TYPE_STREAM and + service == SERVICE and + state == telepathy.TUBE_STATE_LOCAL_PENDING): + addr = self.__iface.AcceptStreamTube(id, + telepathy.SOCKET_ADDRESS_TYPE_IPV4, + telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0, + utf8_strings=True) + self.__logger.debug("Got a stream tube!") + + # sanity checks + assert isinstance(addr, dbus.Struct) + assert len(addr) == 2 + assert isinstance(addr[0], str) + assert isinstance(addr[1], (int, long)) + assert addr[1] > 0 and addr[1] < 65536 + ip_addr = addr[0] + port = int(addr[1]) + + self.__logger.debug("The stream tube is good!") + self.download_file(ip_addr, port, id) + + def download_file(self, ip_addr, port, tube_id): + """ Performs the actual download of the slide deck """ + self.__logger.debug("Downloading from ip %s and port %d.", ip_addr, port) + + getter = network.GlibURLDownloader("http://%s:%d/document" % (ip_addr, port)) + getter.connect("finished", self.download_result_cb, tube_id) + getter.connect("progress", self.download_progress_cb, tube_id) + getter.connect("error", self.download_error_cb, tube_id) + self.__logger.debug("Starting download to %s...", self.__cpxo_path) + getter.start(self.__cpxo_path) + + def download_result_cb(self, getter, tempfile, suggested_name, tube_id): + """ Called when the file download was successful """ + self.__logger.debug("Got file %s (%s) from tube %u", + tempfile, suggested_name, tube_id) + self.emit('deck-download-complete') + self.read_file_cb(self.__cpxo_path) + + def download_progress_cb(self, getter, bytes_downloaded, tube_id): + tmp = True + #self.__logger.debug("Bytes downloaded from tube %u: %u", tube_id, bytes_downloaded) + + def download_error_cb(self, getter, err, tube_id): + self.__logger.error('Download failed on tube %u: %s', tube_id, err) + self.handle_download_fail() + + def share_deck(self): + """ As the instructor XO, or as a student that has completed the deck download + share the deck with others in the activity """ + + # get a somewhat random port number + self.__port = random.randint(1024, 65535) + self.__ip_addr = "127.0.0.1" + + self._fileserver = ReadHTTPServer(("", self.__port), self.__cpxo_path) + self.__logger.debug('Started an HTTP server on port %d', self.__port) + + self.__iface.OfferStreamTube(SERVICE, {}, + telepathy.SOCKET_ADDRESS_TYPE_IPV4, + (self.__ip_addr, dbus.UInt16(self.__port)), + telepathy.SOCKET_ACCESS_CONTROL_LOCALHOST, 0) + self.__logger.debug('Made a stream tube.') + +gobject.type_register(SharedSlides) diff --git a/showntell.py b/showntell.py new file mode 100755 index 0000000..8b37da8 --- /dev/null +++ b/showntell.py @@ -0,0 +1,204 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:t; -*- + +# showntell.py +# +# Derived from: +# +# Classroom Presenter for the XO Laptop +# Main class +# B. Mayton +# +# 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 sugar.activity import activity +from sugar.datastore import datastore +import logging + +import sys, os +import subprocess +import gtk +import zipfile + +import slideviewer +import sidebar +import sliderenderer +import slideshow +import textarea +import toolbars +import utils +import shared +import time +import pdb + +import listview +#import htmlview + + +class ShowNTell(activity.Activity): + + def __init__(self, handle): + #pdb.set_trace() + activity.Activity.__init__(self, handle) + + self.__logger = logging.getLogger('ClassroomPresenter') + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s %(levelname)s %(message)s') + + # Find our instance path + self.__work_path = os.path.join(self.get_activity_root(), 'instance') + self.__deck_dir = os.path.join(self.__work_path, 'deck') + if '-o' not in sys.argv: + try: + os.mkdir(self.__deck_dir) + except Exception, e: + self.__logger.debug("Caught exception and continuing: %s", e) + print 'deck_dir exists' + subprocess.call("rm -rf " + self.__deck_dir + "/*", shell=True) + + self.__rsrc_dir = os.path.join(activity.get_bundle_path(), 'resources') + self.__logger.debug("Found deck directory: %s", self.__deck_dir) + + # Copy the splash screen to the working directory + utils.copy_file(os.path.join(self.__rsrc_dir, 'splash.svg'), + os.path.join(self.__deck_dir, 'splash.svg')) + + # Create a slide deck object + self.__deck = slideshow.Deck(self.__deck_dir) + + # Set up activity sharing + self.__shared = shared.Shared(self, self.__deck, self.__work_path) + + # Create a renderer for slides + self.__renderer = sliderenderer.Renderer(self.__deck) + + # Set up the main canvas + self.__slide_view = gtk.HBox() + self.set_canvas(self.__slide_view) + + # Set up Main Viewer box + self.__main_view_box = gtk.VBox() + self.__slide = slideviewer.SlideViewer(self.__deck, self.__renderer) + self.__text_area = textarea.TextArea(self.__deck) + self.__image_chooser = listview.Listview(self, self.__deck) + #self.__html_viewer = htmlview.Htmlview() + self.__main_view_box.pack_start(self.__slide, True, True, 5) + self.__main_view_box.pack_start(self.__image_chooser, True, True, 5) + #self.__main_view_box.pack_start(self.__html_viewer, True, True, 5) + self.__main_view_box.pack_start(self.__text_area, False, False, 0) + + # Create our toolbars + makeTB = toolbars.MakeToolBar(self, self.__deck) + navTB = toolbars.NavToolBar(self, self.__shared, self.__deck) + inkTB = toolbars.InkToolBar(self.__slide, self.__deck) + + # Create the standard activity toolbox; add our toolbars + toolbox = activity.ActivityToolbox(self) + toolbox.add_toolbar("Slideshow", makeTB) + toolbox.add_toolbar("Navigation",navTB) + toolbox.add_toolbar("Ink", inkTB) + self.set_toolbox(toolbox) + toolbox.show() + + # Set up the side scrollbar widget + self.__side_bar = sidebar.SideBar(self.__deck, self.__renderer) + self.__side_bar.set_size_request(225, 100) + + # Set up a separator for the two widgets + separator = gtk.VSeparator() + + # Pack widgets into main window + self.__slide_view.pack_start(self.__main_view_box, True, True, 0) + self.__slide_view.pack_start(separator, False, False, 5) + self.__slide_view.pack_start(self.__side_bar, False, False, 0) + + # Show all widgets + self.__slide_view.show_all() + self.__main_view_box.show() + self.__slide.show() + self.__text_area.show() + self.__image_chooser.hide() + #self.__html_viewer.hide() + separator.show() + self.__side_bar.show_all() + + # Set up the progress view + self.__progress_max = 1.0 + self.__progress_cur = 0.01 + self.__progress_view = gtk.VBox() + self.__progress_lbl = gtk.Label("Loading slide deck...") + self.__progress_bar = gtk.ProgressBar() + self.__progress_view.pack_start(self.__progress_lbl, True, False, 5) + #self.__progress_view.pack_start(self.__progress_bar, False, False, 5) + self.__progress_bar.set_fraction(self.__progress_cur / self.__progress_max) + + self.__shared.connect('deck-download-complete', self.dl_complete_cb) + + def dl_complete_cb(self, widget): + self.do_slideview_mode() + + def do_slideview_mode(self): + self.set_canvas(self.__slide_view) + self.__slide_view.show_all() + + def set_progress_max(self, maxval): + self.__progress_max = maxval + self.__progress_bar.set_fraction(float(self.__progress_cur) / float(self.__progress_max)) + + def do_progress_view(self): + self.set_canvas(self.__progress_view) + self.__progress_view.show_all() + + def set_progress(self, val): + self.__progress_cur = val + self.__progress_bar.set_fraction(float(self.__progress_cur) / float(self.__progress_max)) + + #resume from journal + def read_file(self, file_path): + self.__logger.debug("read_file " + str(file_path)) + ftype = utils.getFileType(file_path) + z = zipfile.ZipFile(file_path, "r") + for i in z.infolist(): + f = open(os.path.join(self.__deck_dir, i.filename), "wb") + f.write(z.read(i.filename)) + f.close() + z.close() + self.__deck.set_title(self.metadata['title']) + self.__deck.reload() + newindex = 0 + if 'current_index' in self.metadata: + newindex = int(self.metadata.get('current_index', '0')) + self.__deck.goToIndex(newindex, is_local=False) + + #save state in journal for resume + def write_file(self, file_path): + self.__logger.debug("write_file " + str(file_path)) + self.metadata['title'] = self.__deck.get_title() + self.metadata['mime_type'] = "application/x-classroompresenter" + self.metadata['current_index'] = str(self.__deck.getIndex()) + self.__deck.save() + z = zipfile.ZipFile(file_path, "w") + root, dirs, files = os.walk(self.__deck_dir).next() + for f in files: + z.write(os.path.join(root, f), f) + z.close() + + def get_shared_activity(self): + return self._shared_activity + + def get_window(self): + #return (self.__slide, self.__image_chooser, self.__html_viewer) + return (self.__slide, self.__image_chooser) + + diff --git a/sidebar.py b/sidebar.py new file mode 100755 index 0000000..8fb739a --- /dev/null +++ b/sidebar.py @@ -0,0 +1,179 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:t; -*- + +# sidebar.py +# +# Class to handle thumbnail views of the slide on the side of the main viewer +# +# W.Burnside +# B. Mayton +# +# 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 pygtk +import gtk +import slideshow +import slideviewer +import os +import logging +import gobject + +from sugar.graphics import style + +class SideBar(gtk.Notebook): + + def __init__(self, deck, renderer): + gtk.Notebook.__init__(self) + self.__logger = logging.getLogger('SideBar') + self.__deck = deck + self.__renderer = renderer + self.__is_instr = True + + self.set_show_border(False) + self.set_show_tabs(True) + #self.show_tabs = True + #self.show_border = True + + self.slide_context_menu = gtk.Menu() # Don't need to show menus + + # Create the menu items + move_item = gtk.ImageMenuItem('Move') + img = gtk.Image() + img.set_from_file('icons/Icon-move.svg') + move_item.set_image(img) + move_item.connect("activate", self.moveslide) + + remove_item = gtk.ImageMenuItem('remove') + img = gtk.Image() + img.set_from_file('icons/Icon-remove.svg') + remove_item.set_image(img) + remove_item.connect("activate", self.removeslide) + + # Add them to the menu + self.slide_context_menu.append(move_item) + self.slide_context_menu.append(remove_item) + + # We do need to show menu items + move_item.show() + remove_item.show() + + # Create scrolled window for viewing thumbs or subs + # Scrollbar: horizontal if necessary; vertical always + self.__viewing_box = gtk.ScrolledWindow() + self.__viewing_box.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) + slide_label = gtk.Label("Slides") + event_box = gtk.EventBox() + event_box.add(self.__viewing_box) + + self.append_page(event_box, slide_label) + #self.append_page(self.__viewing_box, sub_label) + + self.__sublist_store = gtk.ListStore(str, int) + self.__sub_col = gtk.TreeViewColumn("Versions of this slide:") + self.__sublist = gtk.TreeView(self.__sublist_store) + self.__sublist.append_column(self.__sub_col) + self.__sublist_cell = gtk.CellRendererText() + self.__sub_col.pack_start(self.__sublist_cell, True) + self.__sub_col.add_attribute(self.__sublist_cell, 'text', 0) + + sub_label = gtk.Label("Submissions") + self.append_page(self.__sublist, sub_label) + + #self.__sublist_store.append(["My Ink", -1]) + + self.load_thumbs() + + # show widgets + self.show_all() + + self.__deck.connect('deck-changed', self.load_thumbs) + self.__deck.connect('update-submissions', self.load_subs) + self.__sublist.get_selection().connect('changed', self.sub_sel_changed) + + def load_subs(self, widget=None, def_sub=-1): + self.__logger.debug("Loading submission list") + self.__sublist_store.clear() + sublist = self.__deck.getSubmissionList() + self.__sublist_store.append(["My Ink", -1]) + i = 0 + for submission in sublist: + self.__sublist_store.append([str(submission) + "'s Ink", i]) + i = i + 1 + self.__sublist.get_selection().select_path(def_sub+1) + + def sub_sel_changed(self, widget=None): + (model, itera) = widget.get_selected() + if itera: + newindex = model.get_value(itera, 1) + self.__logger.debug("Submission selection changed to "+ str(newindex)) + self.__deck.setActiveSubmission(newindex) + + # Method to load slides into Scrolling side window + # The method uses a table to organize the slides + def load_thumbs(self, widget=None): + for c in self.__viewing_box.get_children(): + self.__viewing_box.remove(c) + + # create image table for thumbnails + self.image_table = gtk.Table(self.__deck.getSlideCount(), 1, False) + + # Loop to show slides + for i in range(self.__deck.getSlideCount()): + # Create event box for table entry + event_box = gtk.EventBox() + event_box.set_size_request(209, 160) + + # Add navigation to event boxes + event_box.set_above_child(True) + event_box.connect('button_press_event', self.change_slide, i) + + # Create viewer for slide and add to box + slide = slideviewer.ThumbViewer(self.__deck, self.__renderer, i) + event_box.add(slide) + + # Put box in table and show + self.image_table.attach(event_box, 0, 1, i, i+1) + event_box.show() + + # Show each slide + slide.show() + + # show images + self.__viewing_box.add_with_viewport(self.image_table) + self.image_table.show() + self.movemode=False + + + def change_slide(self, widget, event, n): + if event.button == 3: + self.selected_slide = n + self.slide_context_menu.popup( None, None, None, event.button, event.time) + elif self.movemode: + self.movemode=False + self.__deck.moveSlide(self.moved_slide, n) + self.__deck.save() + self.__deck.reload() + else: + self.__deck.goToIndex(n, is_local=True) + + def moveslide(self, params): + self.movemode=True + self.moved_slide = self.selected_slide + + def removeslide(self, params): + self.__deck.removeSlide(self.selected_slide) + self.__deck.save() + self.__deck.reload() + + diff --git a/sliderenderer.py b/sliderenderer.py new file mode 100755 index 0000000..9c89e88 --- /dev/null +++ b/sliderenderer.py @@ -0,0 +1,145 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:t; -*- + +# sliderenderer.py +# +# Class for rendering slides to a surface +# B. Mayton +# +# 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 cairo +import rsvg +import gtk +import os +import utils +import time +import logging + +class Renderer(object): + def __init__(self, deck): + """Constructs a new SlideRenderer that will render slides from deck""" + self.__logger = logging.getLogger('Renderer') + self.__logger.setLevel('error') + self.__deck = deck + + def getSlideDimensionsFromFirstLayer(self, n=None): + """Returns the [width, height] of the first slide layer""" + if n is None: + n = self.__deck.getIndex() + layers = self.__deck.getSlideLayers(n) + + # return some default reasonable value if this is an empty slide + if len(layers) == 0: + return [640.0, 480.0] + + ftype = utils.getFileType(layers[0]) + + # This may be optimizable to avoid having to open the first layer to get its size, + # or at least keeping it around to re-use it when the slide is first rendered + if ftype == "svg": + f = open(layers[0], 'rb') + svg_data = f.read() + f.close() + handle = rsvg.Handle(data=svg_data) + a, b, w, h = handle.get_dimension_data() + return [w,h] + elif ftype == "png": + surface = cairo.ImageSurface.create_from_png(layers[0]) + return [float(surface.get_width()), float(surface.get_height())] + elif ftype == "jpg": + pbuf = gtk.gdk.pixbuf_new_from_file(layers[0]) + return [float(pbuf.get_width()), float(pbuf.get_height())] + else: + return [640.0, 480.0] + + def getSlideDimensions(self, n=None): + """Returns the slide dimensions, using the value in the XML file first, if it exists, and then the size of the first layer""" + if n is None: + n = self.__deck.getIndex() + dims = self.__deck.getSlideDimensionsFromXML(n) + if dims == False: + return self.getSlideDimensionsFromFirstLayer(n) + else: + w, h = dims + return [w, h] + + def renderSlideToSurface(self, surface, n=None): + if n is None: + n = self.__deck.getIndex() + + timerstart = time.time() + + self.__logger.debug("rendering slide " + str(n)) + ctx = gtk.gdk.CairoContext(cairo.Context(surface)) + #ctx = cairo.Context(surface) + + self.__logger.debug("Got context at " + str(time.time() - timerstart)) + + # Get the slide dimensions and set up a Cairo transformation matrix + srcw, srch = self.getSlideDimensions(n) + if srcw > 640: + srcw = 640 + if srch > 480: + srch = 480 + targw = float(surface.get_width()) + targh = float(surface.get_height()) + x_scale = targw/srcw + y_scale = targh/srch + print 'rendering slide', str(n), "w=", targw, srcw, x_scale, "h=", targh, srch, y_scale + + self.__logger.debug("Surface is " + str(targw) + "x" + str(targh)) + + scale = x_scale + if y_scale < x_scale: + scale = y_scale + + if scale < .98 or scale > 1.02: + ctx.transform(cairo.Matrix(scale, 0, 0, scale, 0, 0)) + + self.__logger.debug("Got transformation matrix at " + str(time.time() - timerstart)) + + # Paint the slide background + ctx.set_source_rgb(1.0, 1.0, 1.0) + ctx.rectangle(0, 0, srcw, srch) + ctx.fill() + + self.__logger.debug("Filled background at " + str(time.time() - timerstart)) + + # Paint the layers + layers = self.__deck.getSlideLayers(n) + self.__logger.debug("Got layers at " + str(time.time() - timerstart)) + for layer in layers: + type = utils.getFileType(layer) + self.__logger.debug("Drawing layer " + str(layer) +" " + str(scale) + " at " + str(time.time() - timerstart)) + print 'drawing layer', type, str(layer) + if type == "svg": + f = open(layer, "rb") + svg_data = f.read() + f.close() + handle = rsvg.Handle(data=svg_data) + handle.render_cairo(ctx) + elif type == "png": + png_surface = cairo.ImageSurface.create_from_png(layer) + self.__logger.debug("Got PNG surface at "+ str(time.time() - timerstart)) + ctx.set_source_surface(png_surface, 0, 0) + ctx.rectangle(0, 0, png_surface.get_width(), png_surface.get_height()) + ctx.fill() + elif type == "jpg": + jpg_pixbuf = gtk.gdk.pixbuf_new_from_file(layer) + self.__logger.debug("Got JPG pixbuf at "+ str(time.time() - timerstart)) + ctx.set_source_pixbuf(jpg_pixbuf, 0, 0) + ctx.rectangle(0, 0, jpg_pixbuf.get_width(), jpg_pixbuf.get_height()) + ctx.fill() + self.__logger.debug("Finished drawing layer at "+ str(time.time() - timerstart)) diff --git a/slideshow.py b/slideshow.py new file mode 100755 index 0000000..bc878c6 --- /dev/null +++ b/slideshow.py @@ -0,0 +1,594 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:t; -*- + +# slideshow.py +# +# Classes to represent a deck of slides, and handle things like file I/O and +# formats +# B. Mayton +# +# 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 xml.dom.minidom +import gobject +import logging +from path import path +from sugar.activity import activity + +class Deck(gobject.GObject): + + __gsignals__ = { + 'slide-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'slide-redraw' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'remove-path' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,)), + 'deck-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + 'local-ink-added' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), + 'remote-ink-added' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)), + 'instr-state-propagate' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)), + 'lock-state-propagate' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)), + 'ink-submitted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, gobject.TYPE_STRING)), + 'ink-broadcast' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING)), + 'update-submissions' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,)), + 'instructor-ink-cleared' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT,)), + 'instructor-ink-removed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_INT)), + } + + def __init__(self, base="/nfs/show"): + gobject.GObject.__init__(self) + self.__logger = logging.getLogger('Deck') + self.__base = base + + self.__is_initiating = True + self.__nav_locked = False + self.__active_sub = -1 + self.__self_text = "" + self.__text_tag = None + + # Compute the path to the deck.xml file and read it if it exists; + # otherwise we'll create a new XML Document + self.__xmlpath = os.path.join(base, "deck.xml") + self.reload() + + def set_locked_mode(self, locked): + """ Setter method for the navigation lock flag""" + self.__logger.debug("Lock state: " +str(locked)) + self.__nav_locked = locked + self.emit('lock-state-propagate', locked) + + def set_is_initiating(self, is_init): + """ Setter method for the instructor flag """ + self.__logger.debug("Instructor state: " +str(is_init)) + self.__is_initiating = is_init + self.emit('instr-state-propagate', is_init) + + def getIsInitiating(self): + return self.__is_initiating + + def set_title(self, title): + self.__title = title + + def get_title(self): + print 'get_title', self.__title + if len(self.__title) > 0: + return self.__title + else: + return "no title" + + def reload(self): + self.__logger.debug("Reading deck") + if os.path.exists(self.__xmlpath): + self.__dom = xml.dom.minidom.parse(self.__xmlpath) + else: + self.__dom = xml.dom.minidom.Document() + + # Look for the root deck element; create it if it's not there + decks = self.__dom.getElementsByTagName("deck") + if len(decks) > 0: + self.__deck = decks[0] + nodes = self.__dom.getElementsByTagName("title") + if len(nodes) > 0: + self.__title = nodes[0].firstChild.data + else: + self.__deck = self.__dom.createElement("deck") + self.__dom.appendChild(self.__deck) + self.__title = 'new' + title = self.__dom.createElement("title") + title.appendChild(self.__dom.createTextNode(self.__title)) + self.__deck.appendChild(title) + splash = self.__dom.createElement("slide") + layer = self.__dom.createElement("layer") + layer.appendChild(self.__dom.createTextNode("splash.svg")) + splash.appendChild(layer) + self.__deck.appendChild(splash) + print "Deck.__title=", self.__title + + # Get the slides from the show + self.__slides = self.__deck.getElementsByTagName("slide") + self.__nslides = len(self.__slides) + self.__logger.debug(str(self.__nslides) + " slides in show") + self.goToIndex(0, is_local=False) + self.emit("deck-changed") + + def save(self, path=None): + """Writes the XML DOM in memory out to disk""" + if not path: + path = self.__xmlpath + outfile = open(path, "w") + self.__dom.writexml(outfile) + outfile.close() + + def rebuild_dom(self, title, slides): + dom = xml.dom.minidom.Document() + deck = dom.createElement("deck") + title = dom.createElement("title") + title.appendChild(dom.createTextNode("new")) + deck.appendChild(title) + for slide in slides: + deck.appendChild(slide) + dom.appendChild(deck) + return dom + + def getDeckPath(self): + """Returns the path to the folder that stores this slide deck""" + return self.__base + + def resizeImage(self, inpath, outpath, w, h): + # resize an image + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(inpath, w, h) + #scaled_buf = pixbuf.scale.simple(w, h, gtk.gdk.INTERP_BILINEAR) + pixbuf.save(outpath, "png") + + def addSlide(self,file_path): + + INSTANCE = path(activity.get_activity_root()) / 'instance' + filepath = path(file_path) + print 'addSlide file_path', filepath.exists(), filepath + filename = filepath.name + inpath = INSTANCE / 'deck' / filename + print 'inpath', inpath.exists(), inpath + path.copy(filepath, inpath) + outpath = path(activity.get_activity_root() ) / 'instance' / 'deck' / filename + print 'outpath=', outpath.exists(), outpath + self.resizeImage(inpath, outpath, 640, 480) + print 'outpath=', outpath.exists(), outpath + + print 'get slide dimensions' + dims = self.getSlideDimensionsFromXML(0) + if dims == False: + wf = 640 + hf = 480 + else: + wf, hf = dims + w = str(int(wf)) + h = str(int(hf)) + print 'add slide', w, h + newslide = self.__dom.createElement("slide") + newslide.setAttribute("height", h) + newslide.setAttribute("title", "newslide") + newslide.setAttribute("width", w) + newlayer = self.__dom.createElement("layer") + txt = self.__dom.createTextNode(filename) + newlayer.appendChild(txt) + newslide.appendChild(newlayer) + self.__deck.appendChild(newslide) + print 'added slide', self.__dom.toxml() + self.save() + + def removeSlide(self, n): + del self.__slides[n] + self.__dom = self.rebuild_dom("modified deck", self.__slides) + + def moveSlide(self, f, t): + if f < t: + self.__slides.insert(t, self.__slides[f]) + del self.__slides[f] + elif t < f: + self.__slides.insert(t, self.__slides[f]) + del self.__slides[f+1] + self.__dom = self.rebuild_dom("modified deck", self.__slides) + + def getSlideLayers(self, n=-1): + """Returns a list of the layers that comprise this slide""" + if n == -1: + n = self.__pos + slide = self.__slides[n] + self.__layers = slide.getElementsByTagName("layer") + layers = [] + for l in self.__layers: + p = os.path.join(self.__base, l.firstChild.nodeValue) + layers.append(p) + return layers + + def getInstructorInk(self): + self.__instructor_ink = [] + instr = self.__slide.getElementsByTagName("instructor") + if len(instr) > 0: + self.__instructor_tag = instr[0] + pathtags = self.__instructor_tag.getElementsByTagName("path") + for pathstr in pathtags: + self.__instructor_ink.append(pathstr.firstChild.nodeValue) + return self.__instructor_ink + + def getSelfInkOrSubmission(self): + if self.__active_sub == -1: + return (self.__self_ink, self.__self_text) + subtags = self.__slide.getElementsByTagName("submission") + if self.__active_sub > -1 and self.__active_sub < len(subtags): + active_subtag = subtags[self.__active_sub] + text = "" + texts = active_subtag.getElementsByTagName("text") + if len(texts) > 0: + if texts[0].firstChild: + text = texts[0].firstChild.nodeValue + pathlist = [] + paths = active_subtag.getElementsByTagName("path") + for path in paths: + if path.firstChild: + pathlist.append(path.firstChild.nodeValue) + return (pathlist, text) + return None + + def setActiveSubmission(self, sub): + self.__active_sub = sub + self.emit('slide-redraw') + + def getActiveSubmission(self): + return self.__active_sub + + def getSubmissionList(self, n=None): + if n is None: + n = self.__pos + subtags = self.__slide.getElementsByTagName("submission") + sublist = [] + for subtag in subtags: + sublist.append(subtag.getAttribute("from")) + return sublist + + def addSubmission(self, whofrom, inks, text="", n=None): + if n is None: + n = self.__pos + if n >= 0 and n < self.getSlideCount(): + slide = self.__slides[n] + else: + slide = self.__slides[self.__pos] + newsub = self.__dom.createElement("submission") + newsub.setAttribute("from", whofrom) + substrparts = inks.split("$") + for part in substrparts: + if len(part) > 0: + newpath = self.__dom.createElement("path") + newpath.appendChild(self.__dom.createTextNode(part)) + newsub.appendChild(newpath) + subtext = self.__dom.createElement("text") + subtext.appendChild(self.__dom.createTextNode(text)) + newsub.appendChild(subtext) + subs = slide.getElementsByTagName("submission") + for sub in subs: + if sub.getAttribute("from") == whofrom: + slide.removeChild(sub) + slide.appendChild(newsub) + subs = slide.getElementsByTagName("submission") + if n == self.__pos: + self.emit('update-submissions', len(subs) - 1) + + def addInkToSlide(self, pathstr, islocal, n=None): + """Adds ink to the current slide, or slide n if given. Instructor ink may be added to any slide; + but it only makes sense to add student ink to the current slide (n will be ignored)""" + if n is None: + slide = self.__slide + instr_tag = self.__instructor_tag + if instr_tag == None: + instr_tag = self.__dom.createElement("instructor") + slide.appendChild(instr_tag) + self.__instructor_tag = instr_tag + else: + if n < self.getSlideCount and n >= 0: + slide = self.__slides[n] + else: + slide = self.__slides[self.__pos] + instr_tags = slide.getElementsByTagName("instructor") + if len(instr_tags) > 0: + instr_tag = instr_tags[0] + else: + instr_tag = self.__dom.createElement("instructor") + slide.appendChild(instr_tag) + if not islocal or self.__is_initiating: + self.__instructor_ink.append(pathstr) + path = self.__dom.createElement("path") + path.appendChild(self.__dom.createTextNode(pathstr)) + instr_tag.appendChild(path) + else: + self.__self_ink.append(pathstr) + if not self.__self_ink_tag: + self.__self_ink_tag = self.__dom.createElement("self") + self.__slide.appendChild(self.__self_ink_tag) + path = self.__dom.createElement("path") + path.appendChild(self.__dom.createTextNode(pathstr)) + self.__self_ink_tag.appendChild(path) + if islocal: + self.emit("local-ink-added", pathstr) + else: + if n is None or n == self.__pos: + self.emit("remote-ink-added", pathstr) + + def clearInk(self, n=None): + if n is None: + n = self.__pos + slide = self.__slides[n] + if self.__is_initiating: + self.clearInstructorInk(n) + self.emit('instructor-ink-cleared', n) + self_tags = slide.getElementsByTagName("self") + for self_tag in self_tags: + slide.removeChild(self_tag) + self.__self_ink = [] + self.__self_ink_tag = None + + def clearInstructorInk(self, n=None): + if n is None: + n = self.__pos + slide = self.__slides[n] + instructor_tags = slide.getElementsByTagName("instructor") + for instructor_tag in instructor_tags: + slide.removeChild(instructor_tag) + if n == self.__pos: + self.__instructor_ink = [] + self.__instructor_tag = None + self.emit('slide-redraw') + + def removeInstructorPathByUID(self, uid, n=None): + if n is None: + n = self.__pos + needs_redraw = False + slide = self.__slides[n] + instructor_tags = slide.getElementsByTagName("instructor") + if len(instructor_tags) > 0: + instructor_tag = instructor_tags[0] + else: + return + path_tags = instructor_tag.getElementsByTagName("path") + for path_tag in path_tags: + if path_tag.firstChild: + pathstr = path_tag.firstChild.nodeValue + path_uid = 0 + try: + path_uid = int(pathstr[0:pathstr.find(';')]) + except Exception, e: + pass + if path_uid == uid: + instructor_tag.removeChild(path_tag) + needs_redraw = True + if n == self.__pos and needs_redraw: + self.emit('remove-path', uid) + + def removeLocalPathByUID(self, uid, n=None): + if n is None: + n = self.__pos + slide = self.__slides[n] + if self.__is_initiating: + self.emit('instructor_ink_removed', uid, n) + tags = slide.getElementsByTagName("instructor") + else: + tags = slide.getElementsByTagName("self") + if len(tags) > 0: + tag = tags[0] + else: + return + path_tags = tag.getElementsByTagName("path") + for path_tag in path_tags: + if path_tag.firstChild: + pathstr = path_tag.firstChild.nodeValue + path_uid = 0 + try: + path_uid = int(pathstr[0:pathstr.find(';')]) + except Exception, e: + pass + if path_uid == uid: + tag.removeChild(path_tag) + + def doSubmit(self): + inks, text, whofrom = self.getSerializedInkSubmission() + self.__logger.debug("Submitting ink: " + str(inks) + " text: " + text) + self.emit('ink-submitted', inks, text) + + def doBroadcast(self): + inks, text, whofrom = self.getSerializedInkSubmission() + self.emit('ink-broadcast', whofrom, inks, text) + + def getSerializedInkSubmission(self): + sub = "" + text = "" + if self.__active_sub == -1: + self_tags = self.__slide.getElementsByTagName("self") + if len(self_tags) > 0: + texts = self_tags[0].getElementsByTagName("text") + if len(texts) > 0: + if texts[0].firstChild: + text = texts[0].firstChild.nodeValue + for path in self_tags[0].getElementsByTagName("path"): + sub = sub + path.firstChild.nodeValue + "$" + return sub, text, "myself" + else: + sub = "" + whofrom = "unknown" + subtags = self.__slide.getElementsByTagName("submission") + if self.__active_sub > -1 and self.__active_sub < len(subtags): + active_subtag = subtags[self.__active_sub] + text = "" + whofrom = active_subtag.getAttribute("from") + texts = active_subtag.getElementsByTagName("text") + if len(texts) > 0: + if texts[0].firstChild: + text = texts[0].firstChild.nodeValue + pathlist = [] + paths = active_subtag.getElementsByTagName("path") + for path in paths: + if path.firstChild: + sub = sub + path.firstChild.nodeValue + "$" + return sub, text, whofrom + + def getSlideThumb(self, n=-1): + """Returns the full path to the thumbnail for this slide if it is defined; otherwise False""" + if n == -1: + n = self.__pos + slide = self.__slides[n] + thumbs = slide.getElementsByTagName("thumb") + if len(thumbs) < 1: + return False + return os.path.join(self.__base, thumbs[0].firstChild.nodeValue) + + def setSlideThumb(self, filename, n=-1): + """Sets the thumbnail for this slide to filename (provide a *relative* path!)""" + if n == -1: + n = self.__pos + slide = self.__slides[n] + thumbs = slide.getElementsByTagName("thumb") + for t in thumbs: + slide.removeChild(t) + thumb = self.__dom.createElement("thumb") + thumb.appendChild(self.__dom.createTextNode(filename)) + slide.appendChild(thumb) + + def getSlideClip(self, n=-1): + """Returns the full path to the audio clip for this slide if it is defined; otherwise False""" + if n == -1: + n = self.__pos + slide = self.__slides[n] + clip = slide.getElementsByTagName("clip") + if len(clip) < 1: + return False + return os.path.join(self.__base, clip[0].firstChild.nodeValue) + + def setSlideClip(self, filename, n=-1): + """Sets the clip for this slide to filename (provide a *relative* path!)""" + if n == -1: + n = self.__pos + slide = self.__slides[n] + clips = slide.getElementsByTagName("clip") + for clip in clips: + slide.removeChild(clip) + thumb = self.__dom.createElement("clip") + thumb.appendChild(self.__dom.createTextNode(filename)) + slide.appendChild(thumb) + + def setSlideText(self, textval): + self.__self_text = textval + if self.__text_tag: + if self.__text_tag.firstChild: + self.__text_tag.firstChild.nodeValue = textval + else: + self.__text_tag.appendChild(self.__dom.createTextNode(textval)) + + def doNewIndex(self): + """Updates any necessary state associated with moving to a new slide""" + self.__slide = self.__slides[self.__pos] + self_ink = self.__slide.getElementsByTagName("self") + self.__instructor_tag = None + self.__self_ink_tag = None + self.__instructor_ink = [] + self.__self_ink = [] + self.__self_text = "" + self.__text_tag = None + self.__active_sub = -1 + if len(self_ink) > 0: + self.__self_ink_tag = self_ink[0] + texttags = self.__self_ink_tag.getElementsByTagName("text") + if len(texttags) > 0: + self.__text_tag = texttags[0] + else: + self.__text_tag = self.__dom.createElement(text) + self.__text_tag.appendChild(self.__dom.createTextNode("")) + self.__self_ink_tag.appendChild(text) + pathtags = self.__self_ink_tag.getElementsByTagName("path") + for pathstr in pathtags: + self.__self_ink.append(pathstr.firstChild.nodeValue) + else: + self.__self_ink_tag = self.__dom.createElement("self") + self.__slide.appendChild(self.__self_ink_tag) + self.__text_tag = self.__dom.createElement("text") + self.__text_tag.appendChild(self.__dom.createTextNode("")) + self.__self_ink_tag.appendChild(self.__text_tag) + if self.__text_tag.firstChild: + self.__self_text = self.__text_tag.firstChild.nodeValue + + self.emit("slide-changed") + self.emit("update-submissions", self.__active_sub) + self.emit("slide-redraw") + + def goToIndex(self, index, is_local): + """Jumps to the slide at the given index, if it's valid""" + self.__logger.debug("Trying to change slides: locked? %u, instructor? %u, is_local? %u", + self.__nav_locked, self.__is_initiating, is_local) + + in_range = index < self.__nslides and index >= 0 + + if (self.__is_initiating or not is_local or not self.__nav_locked) and in_range: + self.__logger.debug("Changing slide to index: %u", index) + self.__pos = index + self.doNewIndex() + else: + self.__pos = index + print 'invalid index', index + + def getIndex(self): + """Returns the index of the current slide""" + return self.__pos + + def next(self): + """Moves to the next slide""" + self.goToIndex(self.__pos + 1, is_local=True) + + def previous(self): + """Moves to the previous slide""" + self.goToIndex(self.__pos - 1, is_local=True) + + def isAtBeginning(self): + """Returns true if show is on the first slide in the deck""" + if self.__nslides < 1: + return True + + if self.__pos == 0: + return True + else: + return False + + def isAtEnd(self): + """Returns true if the show is at the last slide in the deck""" + if self.__nslides < 1: + return True + + if self.__pos == self.__nslides - 1: + return True + else: + return False + + def getSlideDimensionsFromXML(self, n=-1): + """Returns the dimensions for the slide at index n, if they're specified""" + if n == -1: + n = self.__pos + slide = self.__slides[n] + wstring = slide.getAttribute("width") + hstring = slide.getAttribute("height") + if wstring != '' and hstring != '': + return [float(wstring), float(hstring)] + return False + + def getSlideCount(self): + return self.__nslides + +gobject.type_register(Deck) diff --git a/slideviewer.py b/slideviewer.py new file mode 100755 index 0000000..5156bbf --- /dev/null +++ b/slideviewer.py @@ -0,0 +1,315 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:t; -*- + +# slideviewer.py +# +# Class for displaying Classroom Presenter SVG slides in a GTK widget +# B. Mayton +# +# 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 cairo +import rsvg +import gtk +import os +import time +import ink +import logging +import gobject + +class SlideViewer(gtk.EventBox): + __gsignals__ = {'button_press_event' : 'override', + 'button_release_event' : 'override', + 'motion_notify_event' : 'override', + 'enter_notify_event' : 'override', + 'undo-redo-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()), + } + + def __init__(self, deck, renderer): + gtk.EventBox.__init__(self) + self.__logger = logging.getLogger('SlideViewer') + self.__logger.setLevel('error') + self.__deck = deck + self.__canvas = SlideViewerCanvas(deck, renderer) + self.add(self.__canvas) + self.__canvas.show() + self.__deck.connect("slide-redraw", self.show_current) + self.__deck.connect("remote-ink-added", self.remote_ink_added) + self.__deck.connect("instr-state-propagate", self.set_is_instructor) + self.__deck.connect("remove-path", self.instr_remove_ink) + self.__cur_path = None + self.__is_instr = True + + # default color-blue and pen-4 + self.set_pen(4) + self.set_color(0.0, 0.0, 1.0) + + def set_is_instructor(self, widget, isInstr): + self.__is_instr = isInstr + self.__canvas.is_instr = isInstr + + def set_color(self, r, g, b): + self.__canvas.cur_color = (r, g, b) + + def get_color(self): + return self.__canvas.get_color() + + def set_pen(self, size): + self.__canvas.cur_pen = size + + def get_pen(self): + return self.__canvas.get_pen() + + def show_current(self, widget): + """Handle a slide-redraw event by showing the current slide.""" + self.show_slide() + + def show_slide(self, n=None): + self.__canvas.show_slide(n) + + def remote_ink_added(self, event, inkstr): + self.__canvas.add_ink_path(ink.Path(inkstr), ink_from_instr=True) + self.__canvas.queue_draw() + + def clear_ink(self): + self.__deck.clearInk() + if self.__is_instr: + self.__canvas.instr_ink = [] + if self.__deck.getActiveSubmission() == -1: + self.__canvas.self_ink = [] + self.__canvas.queue_draw() + + def instr_remove_ink(self, widget, uid): + for path in self.__canvas.instr_ink: + if path.uid == uid: + self.__canvas.instr_ink.remove(path) + self.__canvas.queue_draw() + + def submit_ink(self): + self.__logger.debug("Submit clicked") + self.__deck.doSubmit() + + def broadcast_ink(self): + self.__deck.doBroadcast() + + def can_undo_redo(self): + if self.__deck.getActiveSubmission() == -1 or self.__is_instr: + if self.__is_instr: + return ((len(self.__canvas.instr_ink) > 0), (len(self.__canvas.redo_stack) > 0)) + else: + return ((len(self.__canvas.self_ink) > 0), (len(self.__canvas.redo_stack) > 0)) + else: + return (False, False) + + def undo(self): + if self.__is_instr: + undo_stack = self.__canvas.instr_ink + else: + undo_stack = self.__canvas.self_ink + if len(undo_stack) > 0: + path = undo_stack.pop() + self.__deck.removeLocalPathByUID(path.uid) + self.__canvas.redo_stack.append(path) + self.__canvas.queue_draw() + self.emit('undo-redo-changed') + + def redo(self): + if len(self.__canvas.redo_stack) > 0: + path = self.__canvas.redo_stack.pop() + self.__deck.addInkToSlide(str(path), islocal=True) + if self.__is_instr: + self.__canvas.instr_ink.append(path) + else: + self.__canvas.self_ink.append(path) + self.__canvas.queue_draw() + self.emit('undo-redo-changed') + + def do_button_press_event(self, event): + if self.__deck.getActiveSubmission() == -1 or self.__is_instr: + self.__last_pos = (event.x, event.y) + self.__cur_path = ink.Path() + self.__cur_path.color = self.__canvas.cur_color + self.__cur_path.pen = self.__canvas.cur_pen + self.__cur_path.add((event.x, event.y)); + self.__canvas.add_ink_path(self.__cur_path) + + def do_button_release_event(self, event): + if self.__cur_path: + self.__cur_path.add((event.x, event.y)); + self.__deck.addInkToSlide(str(self.__cur_path), islocal=True) + self.__cur_path = None + self.__canvas.redo_stack = [] + self.emit('undo-redo-changed') + + + def do_motion_notify_event(self, event): + if self.__cur_path: + self.__pos = (event.x, event.y) + if(self.has_moved()): + self.__canvas.draw_ink_seg_immed(self.__last_pos, self.__pos) + self.__cur_path.add((event.x, event.y)); + self.__last_pos = self.__pos + + def has_moved(self): + deltaX = self.__pos[0] - self.__last_pos[0] + deltaY = self.__pos[1] - self.__last_pos[1] + return (deltaX > 3 or deltaX < -3 or deltaY > 3 or deltaY < -3) + + def do_enter_notify_event(self, event): + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.PENCIL)) + +class SlideViewerCanvas(gtk.DrawingArea): + __gsignals__ = {'expose_event' : 'override', + 'configure_event' : 'override', + } + + def __init__ (self, deck, renderer): + gtk.DrawingArea.__init__ (self) + self.__logger = logging.getLogger('SlideViewerCanvas') + self.__logger.setLevel('error') + self.__surface = None + self.__renderer = renderer + self.__deck = deck + self.instr_ink = [] + self.self_ink = [] + self.redo_stack = [] + self.is_instr = True + self.cur_pen = None + self.cur_color = None + + + def show_slide(self, n=None): + timerstart = time.time() + x, y, width, height = self.allocation + self.__surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + self.__renderer.renderSlideToSurface(self.__surface, n) + self.instr_ink = [] + self.self_ink = [] + self.redo_stack = [] + instr = self.__deck.getInstructorInk() + selfink, text = self.__deck.getSelfInkOrSubmission() + for path in instr: + self.instr_ink.append(ink.Path(path)) + for path in selfink: + self.self_ink.append(ink.Path(path)) + self.queue_draw() + self.__logger.debug("Rendering slide took " + str(time.time() - timerstart) + " seconds") + + def add_ink_path(self, path, ink_from_instr=False): + if self.is_instr or ink_from_instr: + self.instr_ink.append(path) + else: + self.self_ink.append(path) + + def draw_ink_seg_immed(self, start, end): + self.__context = self.window.cairo_create() + self.__context.set_source_rgb(self.cur_color[0], self.cur_color[1], self.cur_color[2]) + self.__context.set_line_width(self.cur_pen) + self.__context.move_to(start[0], start[1]) + self.__context.line_to(end[0], end[1]) + self.__context.stroke() + + def do_configure_event(self, event): + """Reload the slide when assigned a new height/width""" + if self.__renderer: + self.show_slide() + + def do_expose_event (self, event): + """Draw the slide surface into the DrawingArea""" + timerstart = time.time() + if self.__surface: + # Draw the (cached) slide + self.__context = self.window.cairo_create() + self.__context.set_source_surface(self.__surface, 0, 0) + self.__context.paint() + self.draw_ink_paths(self.instr_ink) + self.draw_ink_paths(self.self_ink) + + self.__logger.debug("Exposing slide took " + str(time.time() - timerstart) + " seconds") + + def draw_ink_paths(self, paths): + for path in paths: + self.__context.set_line_width(path.pen) + self.__context.set_source_rgb(path.color[0], path.color[1], path.color[2]) + start = True + for point in path.points: + if start: + self.__context.move_to(point[0], point[1]) + start = False + else: + self.__context.line_to(point[0], point[1]) + self.__context.stroke() + + def get_pen(self): + return self.cur_pen + + def get_color(self): + return self.cur_color + + +class ThumbViewer(gtk.DrawingArea): + + __gsignals__ = {'expose_event' : 'override', + } + + def __init__ (self, deck, renderer, n): + gtk.DrawingArea.__init__ (self) + self.__logger = logging.getLogger('ThumbViewer') + self.__logger.setLevel('error') + self.__deck = deck + self.__renderer = renderer + self.__n = n + self.__was_highlighted = False + self.__deck.connect('slide-redraw', self.slide_changed) + + # Load thumbnail from the PNG file, if it exists; otherwise draw from scratch + timerstart = time.time() + thumb = self.__deck.getSlideThumb(n) + if thumb and os.path.exists(thumb): + self.__surface = cairo.ImageSurface.create_from_png(thumb) + else: + self.__surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 200, 150) + self.__renderer.renderSlideToSurface(self.__surface, n) + + # Cache thumbnail + name = "slide" + str(n) + "_thumb.png" + thumb = os.path.join(self.__deck.getDeckPath(), name) + self.__surface.write_to_png(thumb) + self.__deck.setSlideThumb(name, n) + self.__logger.debug("Thumbnail loading/drawing took " + str(time.time() - timerstart) + " seconds") + + + def do_expose_event (self, event): + """Redraws the slide thumbnail view""" + timerstart = time.time() + ctx = self.window.cairo_create() + x, y, width, height = self.allocation + if self.__n == self.__deck.getIndex(): + ctx.set_source_rgb(0, 1.0, 0) + self.__was_highlighted = True + else: + ctx.set_source_rgb(0.7, 0.7, 0.7) + self.__was_highlighted = False + ctx.rectangle(0, 0, width, height) + ctx.fill() + if self.__surface: + ctx.set_source_surface(self.__surface, 0, 0) + ctx.rectangle(5, 5, 200, 150) + ctx.fill() + self.__logger.debug("Exposing slide thumbnail took " + str(time.time() - timerstart) + " seconds") + + def slide_changed(self, widget): + """Updates highlighting, if necessary, when current slide changes""" + if self.__was_highlighted != (self.__n == self.__deck.getIndex()): + self.queue_draw() diff --git a/textarea.py b/textarea.py new file mode 100755 index 0000000..260c887 --- /dev/null +++ b/textarea.py @@ -0,0 +1,172 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:t; -*- + +# textarea.py +# +# Class to show, save, submit text entries +# +# W.Burnside +# +# 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, time +from path import path +import pygtk +import gtk +import logging +import subprocess +from sugar.activity import activity + +import pygst +pygst.require("0.10") +import gst + +class TextArea(gtk.HBox): + + + # Constructor + def __init__(self, deck): + gtk.HBox.__init__(self) + + self.__logger = logging.getLogger('TextArea') + self.__deck = deck + self.__text_area = gtk.Entry() + self.render_text_area() + self.__record=False + self.__play=False + self.__deck.connect('slide-redraw', self.update_text) + self.__text_area.connect('changed', self.text_changed) + self.__logger.debug("Constructed") + + """ + #initialize audio record pipeline + player = gst.Pipeline("player") + #source = gst.element_factory_make("alsasrc", "alsa-source") + source = gst.element_factory_make("filesrc", "file-source") + player.add(source) + parse = gst.element_factory_make("wavparse", "parser") + player.add(parse) + convert = gst.element_factory_make("audioconvert", "converter") + player.add(convert) + enc = gst.element_factory_make("vorbisenc", "vorbis-encoder") + player.add(enc) + create = gst.element_factory_make("oggmux", "ogg-create") + player.add(create) + fileout = gst.element_factory_make("filesink", "sink") + player.add(fileout) + gst.element_link_many(source, parse, convert, enc, create, fileout) + self.__player = player + self.__source = source + self.__fileout = fileout + """ + + def update_text(self, widget): + selfink, text = self.__deck.getSelfInkOrSubmission() + self.__text_area.set_text(text) + if self.__deck.getActiveSubmission() == -1 and not self.__deck.getIsInitiating(): + self.__text_area.set_sensitive(True) + else: + self.__text_area.set_sensitive(False) + + def text_changed(self, entry): + if self.__deck.getActiveSubmission() == -1: + self.__deck.setSlideText(self.__text_area.get_text()) + + def render_text_area(self, widget=None): + + # pack widgets + self.pack_start(self.create_bbox(title="Audio Controls"), False, False, 0) + self.pack_start(self.__text_area, True, True, 0) + + # show widgets + self.__text_area.show() + self.show() + + # Clear text in entry box + def clear_text(self, widget, event): + self.__text_area.set_text("") + + # Start Recording + def record(self, params): + if self.__record: + #we are recording, stop and save clip + subprocess.call("killall -q arecord", shell=True) + #convert to ogg file + pipeline = "filesrc location=/tmp/temp.wav ! wavparse ! audioconvert ! vorbisenc ! oggmux ! filesink location=" + self.__audiofile + print 'pipeline', pipeline + subprocess.call("gst-launch-0.10 " + pipeline, shell=True) + #self.__player.set_state(gst.STATE_PLAYING) + #time.sleep(10) + #self.__player.set_state(gst.STATE_NULL) + self.__record = False + self.__deck.setSlideClip(self.__audiofile, n = self.__deck.getIndex()) + self.__deck.save() + self.__deck.reload() + #reset mic boost (xo) + subprocess.call("amixer cset numid=11 off", shell = True) + print 'recording stopped' + else: + self.__record = True + #what is name of clip? If it exists, rm it + self.__audiofile = self.__deck.getSlideClip() + if path(self.__audiofile).exists(): + subprocess.call("rm -rf " + str(self.__audiofile), shell=True) + else: + self.__audiofile = path(self.__deck.getDeckPath()) / 'slide' + str(self.__deck.getIndex()) + '.ogg' + #turn on mic boost (xo) + print 'turn on mic boost' + subprocess.call("amixer cset numid=11 on", shell=True) + print 'record clip:', self.__audiofile + #self.__fileout.set_property("location", self.__audiofile) + #self.__source.set_property("location", "/tmp/temp.wav") + #self.__player.set_state(gst.STATE_PLAYING) + print 'recording started' + subprocess.call("arecord -f cd -d 10 /tmp/temp.wav", shell=True) + + # Play Audio Clip + def play(self, params): + if self.__play: + #we are playing and need to stop + subprocess.call("killall -q gst-launch-0.10", shell=True) + self.__play = False + else: + #play clip + self.__deck.save() + clip = self.__deck.getSlideClip() + #clip = "/home/olpc/Activities/ShowNTell.activity/resources/test.ogg" + print 'play clip:', clip + if clip: + self.__play = True + subprocess.call("gst-launch-0.10 filesrc location=" + clip + " ! decodebin ! audioconvert ! alsasink", shell = True) + self.__play = False + + # Create buttons for audio controls + def create_bbox(self, title=None, spacing=0, layout=gtk.BUTTONBOX_SPREAD): + frame = gtk.Frame(title) + bbox = gtk.HButtonBox() + bbox.set_border_width(5) + bbox.set_layout(layout) + bbox.set_spacing(spacing) + + button = gtk.Button(stock='gtk-media-record') + button.connect("clicked", self.record) + bbox.pack_start(button, False, False, 0) + + button = gtk.Button(stock='gtk-media-play') + button.connect("clicked", self.play) + bbox.pack_start(button, False, False, 0) + + frame.add(bbox) + return frame diff --git a/toolbars.py b/toolbars.py new file mode 100755 index 0000000..b7c2323 --- /dev/null +++ b/toolbars.py @@ -0,0 +1,464 @@ +# toolbars.py +# +# Classes defining toolbars for Classroom Presenter +# B. Mayton +# +# 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 sugar.activity import activity +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.menuitem import MenuItem +from sugar.graphics.objectchooser import ObjectChooser +from sugar.datastore import datastore + +import gtk +import gobject +import pango +import logging +import threading +import os, sys +import utils +from path import path +import slideshow +import subprocess +import listview + +#import htmlview +#import hulahop +from sugar import env +#hulahop.startup(os.path.join(env.get_profile_path(), 'gecko')) + +#from hulahop.webview import WebView + +DATASTORE = '/home/olpc/.sugar/default/datastore/store' + +class NavToolBar(gtk.Toolbar): + + def __init__(self, activity, shared, deck): + gtk.Toolbar.__init__(self) + + self.__deck = deck + self.__activity = activity + self.__shared = shared + self.__logger = logging.getLogger('Navigation Toolbar') + + self.__is_initiating = True + + self.__prevbtn = ToolButton('go-previous') + self.__prevbtn.set_tooltip("Previous slide") + self.__prevbtn.connect('clicked', self.previous) + self.insert(self.__prevbtn, -1) + self.__prevbtn.show() + + self.__nextbtn = ToolButton('go-next') + self.__nextbtn.set_tooltip("Next slide") + self.__nextbtn.connect('clicked', self.next) + self.insert(self.__nextbtn, -1) + self.__nextbtn.show() + + # page number widget and navigation + self.__num_page_item = gtk.ToolItem() + self.__num_current_page = 1 + + self.__num_page_entry = gtk.Entry() + self.__num_page_entry.set_text(str(self.__num_current_page)) + self.__num_page_entry.set_alignment(1) + self.__num_page_entry.connect('activate', self.num_page_activate) + + self.__num_page_entry.set_width_chars(4) + + self.__num_page_item.add(self.__num_page_entry) + self.__num_page_entry.show() + + self.insert(self.__num_page_item, -1) + self.__num_page_item.show() + + + # total page number widget + self.__total_page_item = gtk.ToolItem() + self.__total_page_label = gtk.Label() + + label_attributes = pango.AttrList() + label_attributes.insert(pango.AttrSize(14000, 0, -1)) + label_attributes.insert(pango.AttrForeground(65535, 65535, 65535, 0, -1)) + self.__total_page_label.set_attributes(label_attributes) + + self.__total_page_label.set_text(' / ' + str(self.__deck.getSlideCount())) + self.__total_page_item.add(self.__total_page_label) + self.__total_page_label.show() + + self.insert(self.__total_page_item, -1) + self.__total_page_item.show() + + # separator between navigation buttons and lock button + separator = gtk.SeparatorToolItem() + separator.set_draw(False) + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + # unlocked button + self.__unlockBtn = ToolButton('unlocked') + self.__unlockBtn.set_tooltip("Student Navigation Unlocked") + + # navigation is unlocked by default, so insert the unlock button + self.insert(self.__unlockBtn, 5) + self.__unlockBtn.show() + + # locked button + self.__lockBtn = ToolButton('locked') + self.__lockBtn.set_tooltip("Student Navigation Locked") + + self.__logger.debug("Connecting to navigation locking and activity sharing signals.") + self.__activity.connect('shared', self.activity_shared_cb) + self.__activity.connect('joined', self.activity_joined_cb) + self.__shared.connect('navigation-lock-change', self.set_lock_button) + + # triggers for when slides are changed + self.__deck.connect("slide-changed", self.slide_changed) + self.__deck.connect("deck-changed", self.slide_changed) + self.slide_changed(self.__deck) + self.show() + + def activity_shared_cb(self, widget): + #Callback for when the activity is shared + # bind the lock button click with switching lock mode + self.__lockBtn.connect('clicked', self.__shared.lock_mode_switch) + self.__unlockBtn.connect('clicked', self.__shared.lock_mode_switch) + + def activity_joined_cb(self, widget): + """ Callback for when the activity is joined """ + self.__is_initiating = False + + def set_lock_button(self, widget, is_locked): + self.__logger.debug("Changing lock button, lock mode %u, init %u", + is_locked, self.__is_initiating) + if is_locked: + new_button = self.__lockBtn + if not self.__is_initiating: + self.__prevbtn.set_sensitive(False) + self.__nextbtn.set_sensitive(False) + else: + new_button = self.__unlockBtn + if not self.__is_initiating: + self.__prevbtn.set_sensitive(True) + self.__nextbtn.set_sensitive(True) + + old = self.get_nth_item(5) + self.remove(old) + self.insert(new_button, 5) + new_button.show() + self.queue_draw() + + def next(self, widget): + self.__deck.next() + + def previous(self, widget): + self.__deck.previous() + + def slide_changed(self, widget): + self.__logger.debug("Changing slides!") + if self.__deck.isAtBeginning(): + self.__prevbtn.set_sensitive(False) + else: + self.__prevbtn.set_sensitive(True) + if self.__deck.isAtEnd(): + self.__nextbtn.set_sensitive(False) + else: + self.__nextbtn.set_sensitive(True) + + self.__num_current_page = self.__deck.getIndex() + self.__num_page_entry.set_text(str(self.__num_current_page + 1)) + self.__total_page_label.set_text(' / ' + str(self.__deck.getSlideCount())) + + def num_page_activate(self, entry): + page_entered = int(entry.get_text()) + + if page_entered < 1: + page_entered = 1 + elif self.__deck.getSlideCount() < page_entered: + page_entered = self.__deck.getSlideCount() + + self.__deck.goToIndex(page_entered - 1, is_local=True) + + +class InkToolBar(gtk.Toolbar): + + # Constructor + def __init__(self, slideviewer, deck): + + gtk.Toolbar.__init__(self) + + self.__slideviewer = slideviewer + self.__cur_color = slideviewer.get_color() + self.__cur_color_str = "blue" + self.__cur_pen = slideviewer.get_pen() + self.__deck = deck + self.__deck.connect('slide-redraw', self.update_buttons) + self.__slideviewer.connect('undo-redo-changed', self.update_buttons) + self.__is_instr = False + + # Red Ink + self.__red = gtk.RadioToolButton() + self.__red.set_icon_name('red-button') + self.insert(self.__red, -1) + self.__red.show() + #self.__red.set_tooltip('Red Ink') + self.__red.connect('clicked', self.set_ink_color, 1.0, 0.0, 0.0, "red") + + # Green Ink + self.__green = gtk.RadioToolButton(group=self.__red) + self.__green.set_icon_name('green-button') + self.insert(self.__green, -1) + self.__green.show() + #self.__green.set_tooltip('Green Ink') + self.__green.connect('clicked', self.set_ink_color, 0.0, 1.0, 0.0, "green") + + # Blue Ink + self.__blue = gtk.RadioToolButton(group=self.__red) + self.__blue.set_icon_name('blue-button') + self.insert(self.__blue, -1) + self.__blue.show() + #self.__blue.set_tooltip('Blue Ink') + self.__blue.connect('clicked', self.set_ink_color, 0.0, 0.0, 1.0, "blue") + + # Black Ink + self.__black = gtk.RadioToolButton(group=self.__red) + self.__black.set_icon_name('black-button') + self.insert(self.__black, -1) + self.__black.show() + #self.__black.set_tooltip('Black Ink') + self.__black.connect('clicked', self.set_ink_color, 0.0, 0.0, 0.0, "black") + + # Separate ink from untensils + separator = gtk.SeparatorToolItem() + separator.set_draw(False) + self.insert(separator, -1) + separator.show() + + # Pencil + self.__pencil = gtk.RadioToolButton() + self.__pencil.set_icon_name('tool-pencil') + self.insert(self.__pencil, -1) + self.__pencil.show() + #self.__pencil.set_tooltip('Pencil') + self.__pencil.connect('clicked', self.set_cur_pen, 4) + + # Brush + self.__brush = gtk.RadioToolButton(self.__pencil) + self.__brush.set_icon_name('tool-brush') + self.insert(self.__brush, -1) + self.__brush.show() + #self.__brush.set_tooltip('Brush') + self.__brush.connect('clicked', self.set_cur_pen, 8) + + # Erase + self.__erase = ToolButton('tool-eraser') + self.insert(self.__erase, -1) + self.__erase.show() + self.__erase.set_tooltip('Erase All Ink') + self.__erase.connect('clicked', self.set_erase) + + """ + # Text + self.__text = ToolButton('text') + self.insert(self.__text, -1) + self.__text.show() + self.__text.set_tooltip('Text') + """ + + # Separate tools from text + separator = gtk.SeparatorToolItem() + separator.set_draw(False) + self.insert(separator, -1) + separator.show() + + # Undo + self.__undo = ToolButton('edit-undo') + self.insert(self.__undo, -1) + self.__undo.show() + self.__undo.set_tooltip('Undo') + self.__undo.connect('clicked', self.undo) + + # Redo + self.__redo = ToolButton('edit-redo') + self.insert(self.__redo, -1) + self.__redo.show() + self.__redo.set_tooltip('Redo') + self.__redo.connect('clicked', self.redo) + + separator = gtk.SeparatorToolItem() + separator.set_draw(False) + separator.set_expand(True) + self.insert(separator, -1) + separator.show() + + self.__submit = ToolButton('dialog-ok') #FIXME (though actually not a terrible icon) + self.insert(self.__submit, -1) + self.__submit.show() + self.__submit.set_tooltip('Broadcast Submission') + self.__submit.connect('clicked', self.submit_ink) + + self.__deck.connect('instr_state_propagate', self.instructor_state_cb) + + self.set_tool_buttons() + self.show() + + def instructor_state_cb(self, widget, is_instr): + self.__is_instr = is_instr + if is_instr: + self.__submit.set_tooltip('Broadcast Submission') + else: + self.__submit.set_tooltip('Submit Ink') + + + def set_cur_pen(self, widget, size): + self.__slideviewer.set_pen(size) + + def set_ink_color(self, widget, r, g, b, color): + self.__slideviewer.set_color(r, g, b) + + + def set_erase(self, widget): + self.__slideviewer.clear_ink() + + def set_tool_buttons(self): + if self.__cur_color == (1.0, 0.0, 0.0): + self.__red.set_active(True) + elif self.__cur_color == (0.0, 1.0, 0.0): + self.__green.set_active(True) + elif self.__cur_color == (0.0, 0.0, 1.0): + self.__blue.set_active(True) + else: + self.__black.set_active(True) + + if self.__cur_pen == 2: + self.__pencil.set_active(True) + elif self.__cur_pen == 5: + self.__brush.set_active(True) + + + def submit_ink(self, widget): + if self.__is_instr: + self.broadcast_ink() + else: + self.__submit.set_sensitive(False) + self.__timer = threading.Timer(3.0, self.reenable_submissions) + self.__timer.start() + self.__slideviewer.submit_ink() + + def broadcast_ink(self): + self.__slideviewer.broadcast_ink() + + def reenable_submissions(self): + self.__submit.set_sensitive(True) + self.__submit.queue_draw() + + def undo(self, widget): + self.__slideviewer.undo() + + def redo(self, widget): + self.__slideviewer.redo() + + def update_buttons(self, widget=None): + can_undo, can_redo = self.__slideviewer.can_undo_redo() + self.__undo.set_sensitive(can_undo) + self.__redo.set_sensitive(can_redo) + if self.__is_instr: + if self.__deck.getActiveSubmission() == -1: + self.__submit.set_sensitive(False) + else: + self.__submit.set_sensitive(True) + +class MakeToolBar(gtk.Toolbar): + + def __init__(self, activity, deck): + gtk.Toolbar.__init__(self) + self.activity = activity + self.deck = deck + + #get mount points + ds_mounts = datastore.mounts() + pendrive = -1 + for i in range(0, len(ds_mounts), 1): + print 'mount', i, ds_mounts[i]['uri'] + if ds_mounts[i]['uri'].find('datastore') > 0: + journal = i + else: + pendrive = i + + + self.__newbtn = ToolButton('new-slideshow') + self.__newbtn.set_tooltip("New slideshow") + self.__newbtn.connect('clicked', self.new) + self.insert(self.__newbtn, -1) + self.__newbtn.show() + + self.__openbtn = ToolButton('slideshow') + self.__openbtn.set_tooltip("Choose slideshow") + self.__openbtn.connect('clicked', self.open) + self.insert(self.__openbtn, -1) + self.__openbtn.show() + + #self.__htmlbutton = ToolButton('new') + #self.__htmlbutton.set_tooltip("test tw") + #self.__htmlbutton.connect('clicked', self.showhtml) + #self.insert(self.__htmlbutton, -1) + #self.__htmlbutton.show() + + self.__journalbtn = ToolButton('activity-journal') + self.__journalbtn.set_tooltip("Choose image") + self.__journalbtn.connect('clicked', self.chooseimage, ds_mounts[journal]['id'], DATASTORE) + self.insert(self.__journalbtn, -1) + self.__journalbtn.show() + + #show pendrive button only if pendrive is mounted + if pendrive > -1: + self.__pendrivebutton = ToolButton('media-flash-usb') + self.__pendrivebutton.set_tooltip("Choose image") + self.__pendrivebutton.connect('clicked', self.chooseimage, ds_mounts[pendrive]['id'], ds_mounts[pendrive]['title']) + self.insert(self.__pendrivebutton, -1) + self.__pendrivebutton.show() + + self.show() + + def new(self, widget): + print 'New slideshow' + #no effect if slideshow is already 'new', e.g. when ShowNTell is opened + #directly not by read_file + #this needs to be changed to show slideshow with html title slide + self.activity.read_file(path(activity.get_bundle_path()) / 'resources' / 'new.cpxo') + + def open(self, widget): + print 'Open slideshow' + #here we need a listview to show existing cpxo bundles (all sources) + #there should be a standard 'open' function for both read_file and open + + def chooseimage(self, widget, source, pth): + scrn1, scrn2 = self.activity.get_window() + treeview = scrn2.get_treeView() + print 'set_store', source, pth + treeview.set_model(scrn2.set_store(source, pth)) + scrn1.hide() + scrn2.show() + + def showhtml(self, widget): + scrn1, scrn2, scrn3 = self.activity.get_window() + scrn1.hide() + scrn2.hide() + #we need to show blank editable tiddly or web page + scrn3.show() + + diff --git a/tw/simple.html b/tw/simple.html new file mode 100644 index 0000000..55bad01 --- /dev/null +++ b/tw/simple.html @@ -0,0 +1,7 @@ + + +Simple + + +

Simple test html page

+ diff --git a/tw/slides.html b/tw/slides.html new file mode 100644 index 0000000..10f4bb7 --- /dev/null +++ b/tw/slides.html @@ -0,0 +1,9428 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
<!--{{{-->
+<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
+<!--}}}-->
+
+
+
Background: #fff
+Foreground: #000
+PrimaryPale: #8cf
+PrimaryLight: #18f
+PrimaryMid: #04b
+PrimaryDark: #014
+SecondaryPale: #ffc
+SecondaryLight: #fe8
+SecondaryMid: #db4
+SecondaryDark: #841
+TertiaryPale: #eee
+TertiaryLight: #ccc
+TertiaryMid: #999
+TertiaryDark: #666
+Error: #f88
+
+
+
/*{{{*/
+body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+
+a {color:[[ColorPalette::PrimaryMid]];}
+a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
+a img {border:0;}
+
+h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
+h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
+h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}
+
+.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
+.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
+.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}
+
+.header {background:[[ColorPalette::PrimaryMid]];}
+.headerShadow {color:[[ColorPalette::Foreground]];}
+.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
+.headerForeground {color:[[ColorPalette::Background]];}
+.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}
+
+.tabSelected{color:[[ColorPalette::PrimaryDark]];
+	background:[[ColorPalette::TertiaryPale]];
+	border-left:1px solid [[ColorPalette::TertiaryLight]];
+	border-top:1px solid [[ColorPalette::TertiaryLight]];
+	border-right:1px solid [[ColorPalette::TertiaryLight]];
+}
+.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
+.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
+.tabContents .button {border:0;}
+
+#sidebar {}
+#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
+#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
+#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}
+
+.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
+.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
+.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
+.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
+	border:1px solid [[ColorPalette::PrimaryMid]];}
+.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
+.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
+.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
+.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
+	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
+.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
+.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
+	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}
+
+.wizard .notChanged {background:transparent;}
+.wizard .changedLocally {background:#80ff80;}
+.wizard .changedServer {background:#8080ff;}
+.wizard .changedBoth {background:#ff8080;}
+.wizard .notFound {background:#ffff80;}
+.wizard .putToServer {background:#ff80ff;}
+.wizard .gotFromServer {background:#80ffff;}
+
+#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
+#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}
+
+.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}
+
+.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
+.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
+.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
+.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
+.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
+.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
+.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}
+
+.tiddler .defaultCommand {font-weight:bold;}
+
+.shadow .title {color:[[ColorPalette::TertiaryDark]];}
+
+.title {color:[[ColorPalette::SecondaryDark]];}
+.subtitle {color:[[ColorPalette::TertiaryDark]];}
+
+.toolbar {color:[[ColorPalette::PrimaryMid]];}
+.toolbar a {color:[[ColorPalette::TertiaryLight]];}
+.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
+.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}
+
+.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
+.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
+.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
+.tagging .button, .tagged .button {border:none;}
+
+.footer {color:[[ColorPalette::TertiaryLight]];}
+.selected .footer {color:[[ColorPalette::TertiaryMid]];}
+
+.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
+.sparktick {background:[[ColorPalette::PrimaryDark]];}
+
+.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
+.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
+.lowlight {background:[[ColorPalette::TertiaryLight]];}
+
+.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}
+
+.imageLink, #displayArea .imageLink {background:transparent;}
+
+.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}
+
+.viewer .listTitle {list-style-type:none; margin-left:-2em;}
+.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
+.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}
+
+.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
+.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
+.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}
+
+.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
+.viewer code {color:[[ColorPalette::SecondaryDark]];}
+.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}
+
+.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}
+
+.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
+.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
+.editorFooter {color:[[ColorPalette::TertiaryMid]];}
+
+#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
+#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
+#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
+#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
+#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
+#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
+#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
+.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
+.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
+#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity:60)';}
+/*}}}*/
+
+
+
/*{{{*/
+* html .tiddler {height:1%;}
+
+body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}
+
+h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
+h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
+h4,h5,h6 {margin-top:1em;}
+h1 {font-size:1.35em;}
+h2 {font-size:1.25em;}
+h3 {font-size:1.1em;}
+h4 {font-size:1em;}
+h5 {font-size:.9em;}
+
+hr {height:1px;}
+
+a {text-decoration:none;}
+
+dt {font-weight:bold;}
+
+ol {list-style-type:decimal;}
+ol ol {list-style-type:lower-alpha;}
+ol ol ol {list-style-type:lower-roman;}
+ol ol ol ol {list-style-type:decimal;}
+ol ol ol ol ol {list-style-type:lower-alpha;}
+ol ol ol ol ol ol {list-style-type:lower-roman;}
+ol ol ol ol ol ol ol {list-style-type:decimal;}
+
+.txtOptionInput {width:11em;}
+
+#contentWrapper .chkOptionInput {border:0;}
+
+.externalLink {text-decoration:underline;}
+
+.indent {margin-left:3em;}
+.outdent {margin-left:3em; text-indent:-3em;}
+code.escaped {white-space:nowrap;}
+
+.tiddlyLinkExisting {font-weight:bold;}
+.tiddlyLinkNonExisting {font-style:italic;}
+
+/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
+a.tiddlyLinkNonExisting.shadow {font-weight:bold;}
+
+#mainMenu .tiddlyLinkExisting,
+	#mainMenu .tiddlyLinkNonExisting,
+	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
+#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}
+
+.header {position:relative;}
+.header a:hover {background:transparent;}
+.headerShadow {position:relative; padding:4.5em 0em 1em 1em; left:-1px; top:-1px;}
+.headerForeground {position:absolute; padding:4.5em 0em 1em 1em; left:0px; top:0px;}
+
+.siteTitle {font-size:3em;}
+.siteSubtitle {font-size:1.2em;}
+
+#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}
+
+#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
+#sidebarOptions {padding-top:0.3em;}
+#sidebarOptions a {margin:0em 0.2em; padding:0.2em 0.3em; display:block;}
+#sidebarOptions input {margin:0.4em 0.5em;}
+#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
+#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
+#sidebarOptions .sliderPanel input {margin:0 0 .3em 0;}
+#sidebarTabs .tabContents {width:15em; overflow:hidden;}
+
+.wizard {padding:0.1em 1em 0em 2em;}
+.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
+.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
+.wizardStep {padding:1em 1em 1em 1em;}
+.wizard .button {margin:0.5em 0em 0em 0em; font-size:1.2em;}
+.wizardFooter {padding:0.8em 0.4em 0.8em 0em;}
+.wizardFooter .status {padding:0em 0.4em 0em 0.4em; margin-left:1em;}
+.wizard .button {padding:0.1em 0.2em 0.1em 0.2em;}
+
+#messageArea {position:fixed; top:2em; right:0em; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
+.messageToolbar {display:block; text-align:right; padding:0.2em 0.2em 0.2em 0.2em;}
+#messageArea a {text-decoration:underline;}
+
+.tiddlerPopupButton {padding:0.2em 0.2em 0.2em 0.2em;}
+.popupTiddler {position: absolute; z-index:300; padding:1em 1em 1em 1em; margin:0;}
+
+.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
+.popup .popupMessage {padding:0.4em;}
+.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0em;}
+.popup li.disabled {padding:0.4em;}
+.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
+.listBreak {font-size:1px; line-height:1px;}
+.listBreak div {margin:2px 0;}
+
+.tabset {padding:1em 0em 0em 0.5em;}
+.tab {margin:0em 0em 0em 0.25em; padding:2px;}
+.tabContents {padding:0.5em;}
+.tabContents ul, .tabContents ol {margin:0; padding:0;}
+.txtMainTab .tabContents li {list-style:none;}
+.tabContents li.listLink { margin-left:.75em;}
+
+#contentWrapper {display:block;}
+#splashScreen {display:none;}
+
+#displayArea {margin:1em 17em 0em 14em;}
+
+.toolbar {text-align:right; font-size:.9em;}
+
+.tiddler {padding:1em 1em 0em 1em;}
+
+.missing .viewer,.missing .title {font-style:italic;}
+
+.title {font-size:1.6em; font-weight:bold;}
+
+.missing .subtitle {display:none;}
+.subtitle {font-size:1.1em;}
+
+.tiddler .button {padding:0.2em 0.4em;}
+
+.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
+.isTag .tagging {display:block;}
+.tagged {margin:0.5em; float:right;}
+.tagging, .tagged {font-size:0.9em; padding:0.25em;}
+.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
+.tagClear {clear:both;}
+
+.footer {font-size:.9em;}
+.footer li {display:inline;}
+
+.annotation {padding:0.5em; margin:0.5em;}
+
+* html .viewer pre {width:99%; padding:0 0 1em 0;}
+.viewer {line-height:1.4em; padding-top:0.5em;}
+.viewer .button {margin:0em 0.25em; padding:0em 0.25em;}
+.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
+.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}
+
+.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
+.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
+table.listView {font-size:0.85em; margin:0.8em 1.0em;}
+table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}
+
+.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
+.viewer code {font-size:1.2em; line-height:1.4em;}
+
+.editor {font-size:1.1em;}
+.editor input, .editor textarea {display:block; width:100%; font:inherit;}
+.editorFooter {padding:0.25em 0em; font-size:.9em;}
+.editorFooter .button {padding-top:0px; padding-bottom:0px;}
+
+.fieldsetFix {border:0; padding:0; margin:1px 0px 1px 0px;}
+
+.sparkline {line-height:1em;}
+.sparktick {outline:0;}
+
+.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
+.zoomer div {padding:1em;}
+
+* html #backstage {width:99%;}
+* html #backstageArea {width:99%;}
+#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em 0.3em 0.5em;}
+#backstageToolbar {position:relative;}
+#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em 0.3em 0.5em;}
+#backstageButton {display:none; position:absolute; z-index:175; top:0em; right:0em;}
+#backstageButton a {padding:0.1em 0.4em 0.1em 0.4em; margin:0.1em 0.1em 0.1em 0.1em;}
+#backstage {position:relative; width:100%; z-index:50;}
+#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin:0em 3em 0em 3em; padding:1em 1em 1em 1em;}
+.backstagePanelFooter {padding-top:0.2em; float:right;}
+.backstagePanelFooter a {padding:0.2em 0.4em 0.2em 0.4em;}
+#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}
+
+.whenBackstage {display:none;}
+.backstageVisible .whenBackstage {display:block;}
+/*}}}*/
+
+
+
/***
+StyleSheet for use when a translation requires any css style changes.
+This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
+***/
+/*{{{*/
+body {font-size:0.8em;}
+#sidebarOptions {font-size:1.05em;}
+#sidebarOptions a {font-style:normal;}
+#sidebarOptions .sliderPanel {font-size:0.95em;}
+.subtitle {font-size:0.8em;}
+.viewer table.listView {font-size:0.95em;}
+/*}}}*/
+
+
+
/*{{{*/
+@media print {
+#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none ! important;}
+#displayArea {margin: 1em 1em 0em 1em;}
+/* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
+noscript {display:none;}
+}
+/*}}}*/
+
+
+
<!--{{{-->
+<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
+<div class='headerShadow'>
+<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
+<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
+</div>
+<div class='headerForeground'>
+<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
+<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
+</div>
+</div>
+<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
+<div id='sidebar'>
+<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
+<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
+</div>
+<div id='displayArea'>
+<div id='messageArea'></div>
+<div id='tiddlerDisplay'></div>
+</div>
+<!--}}}-->
+
+
+
<!--{{{-->
+<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
+<div class='title' macro='view title'></div>
+<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
+<div class='tagging' macro='tagging'></div>
+<div class='tagged' macro='tags'></div>
+<div class='viewer' macro='view text wikified'></div>
+<div class='tagClear'></div>
+<!--}}}-->
+
+
+
<!--{{{-->
+<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
+<div class='title' macro='view title'></div>
+<div class='editor' macro='edit title'></div>
+<div macro='annotations'></div>
+<div class='editor' macro='edit text'></div>
+<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser excludeLists'></span></div>
+<!--}}}-->
+
+
+
To get started with this blank TiddlyWiki, you'll need to modify the following tiddlers:
+* SiteTitle & SiteSubtitle: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
+* MainMenu: The menu (usually on the left)
+* DefaultTiddlers: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
+You'll also need to enter your username for signing your edits: <<option txtUserName>>
+
+
+
These InterfaceOptions for customising TiddlyWiki are saved in your browser
+
+Your username for signing your edits. Write it as a WikiWord (eg JoeBloggs)
+
+<<option txtUserName>>
+<<option chkSaveBackups>> SaveBackups
+<<option chkAutoSave>> AutoSave
+<<option chkRegExpSearch>> RegExpSearch
+<<option chkCaseSensitiveSearch>> CaseSensitiveSearch
+<<option chkAnimate>> EnableAnimations
+
+----
+Also see [[AdvancedOptions]]
+
+
+
<<importTiddlers>>
+
+
+ +
+
+ + + + + + + + + + + diff --git a/utils.py b/utils.py new file mode 100755 index 0000000..0fb2026 --- /dev/null +++ b/utils.py @@ -0,0 +1,32 @@ +# -*- mode:python; tab-width:4; indent-tabs-mode:t; -*- + +import os +import gtk + +def getFileType(filename): + return os.path.basename(filename).split('.').pop() + +def copy_file(src, dest): + f1 = open(src, "rb") + data = f1.read() + f1.close() + f2 = open(dest, "wb") + f2.write(data) + f2.close() + +def run_dialog(header,msg): + """Pops up a blocking dialog box with 'msg'""" + dialog = gtk.Dialog(str(header), None, gtk.DIALOG_MODAL, + (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + + hbox = gtk.HBox(False, 12) + hbox.set_border_width(12) + dialog.vbox.pack_start(hbox, True, True, 0) + hbox.show() + + label = gtk.Label(str(msg)) + hbox.pack_start(label, False, False, 0) + label.show() + + dialog.run() + dialog.destroy() -- cgit v0.9.1