Bladeren bron

Merge pull request #14 from chamilo/1.9.x

1.9.x
ycastillo 11 jaren geleden
bovenliggende
commit
03c2bb8c09
100 gewijzigde bestanden met toevoegingen van 7765 en 358 verwijderingen
  1. 2 1
      404.php
  2. 10 6
      main/admin/course_edit.php
  3. 1 1
      main/admin/statistics/statistics.lib.php
  4. 1 1
      main/admin/sub_language.class.php
  5. 4 1
      main/admin/sub_language_ajax.inc.php
  6. 5 0
      main/admin/user_add.php
  7. 36 45
      main/admin/user_list.php
  8. 1 1
      main/attendance/attendance_add.php
  9. 5 4
      main/auth/external_login/ldap.inc.php
  10. 2 7
      main/calendar/agenda.inc.php
  11. 39 6
      main/calendar/agenda.lib.php
  12. 7 2
      main/calendar/agenda_js.php
  13. 19 1
      main/calendar/agenda_list.php
  14. 36 67
      main/course_info/infocours.php
  15. 10 4
      main/coursecopy/classes/CourseBuilder.class.php
  16. 10 7
      main/coursecopy/classes/CourseRecycler.class.php
  17. 53 2
      main/cron/import_csv.php
  18. 45 13
      main/document/document.php
  19. 11 0
      main/document/jcapture/.settings/org.eclipse.jdt.core.prefs
  20. 23 0
      main/document/jcapture/JCaptureApplet.html
  21. 48 0
      main/document/jcapture/action.php
  22. 99 0
      main/document/jcapture/applet.php
  23. BIN
      main/document/jcapture/camera.png
  24. 21 0
      main/document/jcapture/jcapture.jnlp
  25. BIN
      main/document/jcapture/lib/apache-mime4j-0.6.jar
  26. BIN
      main/document/jcapture/lib/commons-codec-1.3.jar
  27. BIN
      main/document/jcapture/lib/commons-logging-1.1.1.jar
  28. BIN
      main/document/jcapture/lib/httpclient-4.0.1.jar
  29. BIN
      main/document/jcapture/lib/httpcore-4.0.1.jar
  30. BIN
      main/document/jcapture/lib/httpmime-4.0.1.jar
  31. BIN
      main/document/jcapture/lib/jcapture.jar
  32. BIN
      main/document/jcapture/lib/transform-3.0.2.jar
  33. 1 0
      main/document/jcapture/license.txt
  34. 19 0
      main/document/jcapture/script.js
  35. 1 0
      main/document/jcapture/src/META-INF/services/com.hammurapi.jcapture.VideoEncoder
  36. 17 0
      main/document/jcapture/src/com/hammurapi/jcapture/AWTUtilitiesTranslucener.java
  37. 262 0
      main/document/jcapture/src/com/hammurapi/jcapture/AbstractCaptureApplet.java
  38. 85 0
      main/document/jcapture/src/com/hammurapi/jcapture/ButtonManager.java
  39. 370 0
      main/document/jcapture/src/com/hammurapi/jcapture/CaptureConfig.java
  40. 405 0
      main/document/jcapture/src/com/hammurapi/jcapture/CaptureFrame.java
  41. 606 0
      main/document/jcapture/src/com/hammurapi/jcapture/CaptureOptionsDialog.java
  42. 361 0
      main/document/jcapture/src/com/hammurapi/jcapture/ComponentMover.java
  43. 446 0
      main/document/jcapture/src/com/hammurapi/jcapture/ComponentResizer.java
  44. 28 0
      main/document/jcapture/src/com/hammurapi/jcapture/FragmentImpl.java
  45. 57 0
      main/document/jcapture/src/com/hammurapi/jcapture/FrameImpl.java
  46. 21 0
      main/document/jcapture/src/com/hammurapi/jcapture/GraphicsDeviceTranslucener.java
  47. 80 0
      main/document/jcapture/src/com/hammurapi/jcapture/ImageToolTip.java
  48. 92 0
      main/document/jcapture/src/com/hammurapi/jcapture/JCaptureApplet.java
  49. 121 0
      main/document/jcapture/src/com/hammurapi/jcapture/MappedImage.java
  50. 55 0
      main/document/jcapture/src/com/hammurapi/jcapture/Movie.java
  51. 1124 0
      main/document/jcapture/src/com/hammurapi/jcapture/MovieEditorDialog.java
  52. 321 0
      main/document/jcapture/src/com/hammurapi/jcapture/RecordingControlsFrame.java
  53. 221 0
      main/document/jcapture/src/com/hammurapi/jcapture/Region.java
  54. 312 0
      main/document/jcapture/src/com/hammurapi/jcapture/ScreenRecorder.java
  55. 251 0
      main/document/jcapture/src/com/hammurapi/jcapture/ScreenShot.java
  56. 75 0
      main/document/jcapture/src/com/hammurapi/jcapture/ShapeImpl.java
  57. 353 0
      main/document/jcapture/src/com/hammurapi/jcapture/SwfEncoder.java
  58. 17 0
      main/document/jcapture/src/com/hammurapi/jcapture/Translucener.java
  59. 164 0
      main/document/jcapture/src/com/hammurapi/jcapture/VideoEncoder.java
  60. 787 0
      main/document/jcapture/src/com/hammurapi/jcapture/WavFile.java
  61. 26 0
      main/document/jcapture/src/com/hammurapi/jcapture/WavFileException.java
  62. BIN
      main/document/jcapture/src/com/hammurapi/jcapture/camera.png
  63. BIN
      main/document/jcapture/src/com/hammurapi/jcapture/mouse.png
  64. BIN
      main/document/jcapture/src/com/hammurapi/jcapture/toolbar_buttons.swf
  65. 24 30
      main/document/upload.php
  66. 4 2
      main/forum/forumfunction.inc.php
  67. BIN
      main/img/capture.png
  68. BIN
      main/img/icons/128/login_as.png
  69. BIN
      main/img/icons/22/login_as.png
  70. BIN
      main/img/icons/22/login_as_na.png
  71. BIN
      main/img/icons/32/import_html.png
  72. BIN
      main/img/icons/32/login_as.png
  73. BIN
      main/img/icons/48/capture.png
  74. BIN
      main/img/icons/48/import_html.png
  75. BIN
      main/img/icons/64/login_as.png
  76. 50 41
      main/inc/ajax/document.ajax.php
  77. 2 0
      main/inc/lib/add_course.lib.inc.php
  78. 1 1
      main/inc/lib/attendance.lib.php
  79. 106 13
      main/inc/lib/course.lib.php
  80. 143 48
      main/inc/lib/document.lib.php
  81. 0 36
      main/inc/lib/events.lib.inc.php
  82. 17 5
      main/inc/lib/export.lib.inc.php
  83. 29 13
      main/inc/lib/fileUpload.lib.php
  84. 11 0
      main/inc/lib/jcapture/.settings/org.eclipse.jdt.core.prefs
  85. 23 0
      main/inc/lib/jcapture/JCaptureApplet.html
  86. 48 0
      main/inc/lib/jcapture/action.php
  87. 99 0
      main/inc/lib/jcapture/applet.php
  88. BIN
      main/inc/lib/jcapture/camera.png
  89. 21 0
      main/inc/lib/jcapture/jcapture.jnlp
  90. BIN
      main/inc/lib/jcapture/lib/apache-mime4j-0.6.jar
  91. BIN
      main/inc/lib/jcapture/lib/commons-codec-1.3.jar
  92. BIN
      main/inc/lib/jcapture/lib/commons-logging-1.1.1.jar
  93. BIN
      main/inc/lib/jcapture/lib/httpclient-4.0.1.jar
  94. BIN
      main/inc/lib/jcapture/lib/httpcore-4.0.1.jar
  95. BIN
      main/inc/lib/jcapture/lib/httpmime-4.0.1.jar
  96. BIN
      main/inc/lib/jcapture/lib/jcapture.jar
  97. BIN
      main/inc/lib/jcapture/lib/transform-3.0.2.jar
  98. 1 0
      main/inc/lib/jcapture/license.txt
  99. 19 0
      main/inc/lib/jcapture/script.js
  100. 1 0
      main/inc/lib/jcapture/src/META-INF/services/com.hammurapi.jcapture.VideoEncoder

+ 2 - 1
404.php

@@ -9,4 +9,5 @@ require_once '../main/inc/global.inc.php';
 $msg = get_lang('FileNotFound');
 Display::display_header($msg);
 Display::display_error_message($msg);
-Display::display_footer();
+Display::display_footer();
+

+ 10 - 6
main/admin/course_edit.php

@@ -7,6 +7,7 @@
 // name of the language file that needs to be included
 $language_file = 'admin';
 $cidReset = true;
+
 require_once '../inc/global.inc.php';
 $this_section = SECTION_PLATFORM_ADMIN;
 
@@ -33,7 +34,9 @@ if (Database::num_rows($result) != 1) {
     header('Location: course_list.php');
     exit();
 }
-$course = Database::fetch_array($result,'ASSOC');
+$course = Database::fetch_array($result, 'ASSOC');
+
+$course_info = api_get_course_info($course_code);
 
 // Get course teachers
 $table_course_user = Database :: get_main_table(TABLE_MAIN_COURSE_USER);
@@ -86,6 +89,7 @@ if (count($course_teachers) == 0) {
 
 // Build the form
 $form = new FormValidator('update_course');
+$form->addElement('header', get_lang('Course').'  #'.$course_info['real_id'].' '.$course_code);
 $form->addElement('hidden', 'code', $course_code);
 
 //title
@@ -343,12 +347,8 @@ if ($form->validate()) {
 
             }
         }
-
     }
 
-
-
-
 	$sql = "INSERT IGNORE INTO ".$course_user_table . " SET
 				course_code = '".Database::escape_string($course_code). "',
 				user_id = '".$tutor_id . "',
@@ -359,7 +359,6 @@ if ($form->validate()) {
 				user_course_cat='0'";
 	Database::query($sql);
 
-	$course_info = api_get_course_info($course_code);
 
     if (array_key_exists('add_teachers_to_sessions_courses', $course_info)) {
         $sql = "UPDATE $course_table SET add_teachers_to_sessions_courses = '$addTeacherToSessionCourses'
@@ -381,6 +380,11 @@ if ($form->validate()) {
 
 Display::display_header($tool_name);
 
+echo '<div class="actions">';
+echo Display::url(Display::return_icon('back.png', get_lang('Back')), api_get_path(WEB_CODE_PATH).'admin/course_list.php');
+echo Display::url(Display::return_icon('course_home.png', get_lang('CourseHome')), $course_info['course_public_url'], array('target' => '_blank'));
+echo '</div>';
+
 echo "<script>
 function moveItem(origin , destination) {
 	for (var i = 0 ; i<origin.options.length ; i++) {

+ 1 - 1
main/admin/statistics/statistics.lib.php

@@ -206,7 +206,7 @@ class Statistics {
         		$row['default_date'] = '-';
         	}
             if (!empty($row[4])) { //user ID
-                $row[3] = Display::url($row[3],api_get_path(WEB_CODE_PATH).'admin/user_information?user_id='.$row[5], array('title' => get_lang('UserInfo')));
+                $row[3] = Display::url($row[3],api_get_path(WEB_CODE_PATH).'admin/user_information?user_id='.$row[4], array('title' => get_lang('UserInfo')));
 
                 $row[4] = TrackingUserLog::get_ip_from_user_event($row[4],$row[5],true);
                 if (empty($row[4])) {

+ 1 - 1
main/admin/sub_language.class.php

@@ -116,7 +116,7 @@ class SubLanguageManager {
      * @param String The path file (/var/www/chamilo/main/lang/spanish/gradebook.inc.php)
      * @param String The new sub-language
      * @param String The language variable
-     * @return void()
+     * @return Boolean True on success, False on error
      */
     public static function write_data_in_file($path_file, $new_term, $new_variable) {
         $return_value = false;

+ 4 - 1
main/admin/sub_language_ajax.inc.php

@@ -31,7 +31,10 @@ if (isset($new_language) && isset($language_variable) && isset($file_id)) {
     $return_value            = SubLanguageManager::add_file_in_language_directory($path_folder);
 
     //update variable language
+    // Replace double quotes to avoid parse errors
     $new_language = str_replace('"', '\"',$new_language);
+    // Replace new line signs to avoid parse errors - see #6773
+    $new_language = str_replace("\n","\\n",$new_language);
     $all_file_of_directory[$language_variable]="\"".api_convert_encoding($new_language, api_get_system_encoding(),'UTF-8')."\";";
     $result_array = array();
 
@@ -40,7 +43,7 @@ if (isset($new_language) && isset($language_variable) && isset($file_id)) {
     }
     $variables_with_problems = '';
     if (!empty($result_array)) {
-        foreach($result_array as $key =>$result) {
+        foreach ($result_array as $key => $result) {
             if ($result == false) {
                 $variables_with_problems .=$key.' <br />';
             }

+ 5 - 0
main/admin/user_add.php

@@ -51,6 +51,11 @@ if (isset($_configuration['allow_strength_pass_checker']) && $_configuration['al
 
 $htmlHeadXtra[] = '
 <script>
+$("#status_select").ready(function() {
+    if ($(this).attr("value") != '.STUDENT.') {
+        $("#id_platform_admin").hide();
+    }
+});
 function enable_expiration_date() { //v2.0
 	document.user_add.radio_expiration_date[0].checked=false;
 	document.user_add.radio_expiration_date[1].checked=true;

+ 36 - 45
main/admin/user_list.php

@@ -15,6 +15,8 @@ global $_configuration;
 
 $current_access_url_id = api_get_current_access_url_id();
 
+$action = isset($_REQUEST["action"]) ? $_REQUEST["action"] : null;
+
 // Blocks the possibility to delete a user
 $delete_user_available = true;
 if (isset($_configuration['deny_delete_users']) &&  $_configuration['deny_delete_users']) {
@@ -102,13 +104,13 @@ function clear_session_list (div_session) {
 }
 
 function display_advanced_search_form () {
-        if ($("#advanced_search_form").css("display") == "none") {
-                $("#advanced_search_form").css("display","block");
-                $("#img_plus_and_minus").html(\'&nbsp;'.Display::return_icon('div_hide.gif',get_lang('Hide'),array('style'=>'vertical-align:middle')).'&nbsp;'.get_lang('AdvancedSearch').'\');
-        } else {
-                $("#advanced_search_form").css("display","none");
-                $("#img_plus_and_minus").html(\'&nbsp;'.Display::return_icon('div_show.gif',get_lang('Show'),array('style'=>'vertical-align:middle')).'&nbsp;'.get_lang('AdvancedSearch').'\');
-        }
+    if ($("#advanced_search_form").css("display") == "none") {
+            $("#advanced_search_form").css("display","block");
+            $("#img_plus_and_minus").html(\'&nbsp;'.Display::return_icon('div_hide.gif',get_lang('Hide'),array('style'=>'vertical-align:middle')).'&nbsp;'.get_lang('AdvancedSearch').'\');
+    } else {
+            $("#advanced_search_form").css("display","none");
+            $("#img_plus_and_minus").html(\'&nbsp;'.Display::return_icon('div_show.gif',get_lang('Show'),array('style'=>'vertical-align:middle')).'&nbsp;'.get_lang('AdvancedSearch').'\');
+    }
 }
 
 $(document).ready(function() {
@@ -125,7 +127,6 @@ $(document).ready(function() {
         }
     }
 
-
     $(".agenda_opener").live("click", function() {
         var url = this.href;
         var dialog = $("#dialog");
@@ -135,12 +136,12 @@ $(document).ready(function() {
         }
         // load remote content
         dialog.load(
-                url,
-                {},
-                function(responseText, textStatus, XMLHttpRequest) {
-                    dialog.dialog({width:720, height:550, modal:true});
-                }
-            );
+            url,
+            {},
+            function(responseText, textStatus, XMLHttpRequest) {
+                dialog.dialog({width:720, height:550, modal:true});
+            }
+        );
         //prevent the browser to follow the link
         return false;
     });
@@ -155,6 +156,17 @@ function load_calendar(user_id, month, year) {
 
 $this_section = SECTION_PLATFORM_ADMIN;
 
+if ($action == 'login_as') {
+    $check = Security::check_token('get');
+
+    if (isset($_GET['user_id']) && api_can_login_as($_GET['user_id']) && $check) {
+        login_user($_GET['user_id']);
+    } else {
+        api_not_allowed(true);
+    }
+    Security::clear_token();
+}
+
 api_protect_admin_script(true);
 
 /**
@@ -168,21 +180,17 @@ api_protect_admin_script(true);
 */
 function login_user($user_id) {
     $user_id = intval($user_id);
+    $user_info = api_get_user_info($user_id);
 
-    if (empty($user_id)) {
-        return false;
-    }
-    if ($user_id != strval(intval($user_id))) {
-    	return false;
-    }
+    // Check if the user is allowed to 'login_as'
+    $can_login_as = api_can_login_as($user_id);
 
-    //Only superadmins can login to admin accounts
-    if (!api_global_admin_can_edit_admin($user_id)) {
+    if (!$can_login_as) {
         return false;
     }
 
     //Load $_user to be sure we clean it before logging in
-	global $uidReset, $loginFailed, $_configuration, $_user;
+	global $uidReset, $loginFailed, $_user;
 
 	$main_user_table      = Database::get_main_table(TABLE_MAIN_USER);
 	$main_admin_table     = Database::get_main_table(TABLE_MAIN_ADMIN);
@@ -190,11 +198,6 @@ function login_user($user_id) {
 
 	unset($_user['user_id']); // uid not in session ? prevent any hacking
 
-    $user_info = api_get_user_info($user_id);
-
-    // check if the user is allowed to 'login_as'
-    $can_login_as = (api_is_platform_admin() OR (api_is_session_admin() && $user_info['status'] == 5 ));
-    if (!$can_login_as) { return false; }
 
 	$firstname  = $user_info['firstname'];
 	$lastname   = $user_info['lastname'];
@@ -261,7 +264,6 @@ function login_user($user_id) {
 			$_SESSION['login_as']               = true; // will be useful later to know if the user is actually an admin or not (example reporting)s
 
 			$target_url = api_get_path(WEB_PATH)."user_portal.php";
-			//$message .= "<br/>Login successful. Go to <a href=\"$target_url\">$target_url</a>";
 			$message .= '<br />'.sprintf(get_lang('LoginSuccessfulGoToX'),'<a href="'.$target_url.'">'.$target_url.'</a>');
 			Display :: display_header(get_lang('UserList'));
 			Display :: display_normal_message($message,false);
@@ -561,16 +563,15 @@ function modify_filter($user_id, $url_params, $row) {
     if (api_is_platform_admin() || (api_is_session_admin() && $current_user_status_label == $statusname[STUDENT])) {
     	if (!$user_is_anonymous) {
             if (api_global_admin_can_edit_admin($user_id)) {
-                $result .= '<a href="user_list.php?action=login_as&amp;user_id='.$user_id.'&amp;sec_token='.$_SESSION['sec_token'].'">'.Display::return_icon('login_as.gif', get_lang('LoginAs')).'</a>&nbsp;&nbsp;';
+                $result .= '<a href="user_list.php?action=login_as&amp;user_id='.$user_id.'&amp;sec_token='.$_SESSION['sec_token'].'">'.Display::return_icon('login_as.png', get_lang('LoginAs')).'</a>&nbsp;&nbsp;';
             } else {
-                $result .= Display::return_icon('login_as_na.gif', get_lang('LoginAs')).'&nbsp;&nbsp;';
+                $result .= Display::return_icon('login_as_na.png', get_lang('LoginAs')).'&nbsp;&nbsp;';
             }
-            //$result .= '<a href="user_list.php?action=login_as&amp;user_id='.$user_id.'&amp;sec_token='.$_SESSION['sec_token'].'">'.Display::return_icon('login_as.gif', get_lang('LoginAs')).'</a>&nbsp;&nbsp;';
     	} else {
-    		$result .= Display::return_icon('login_as_na.gif', get_lang('LoginAs')).'&nbsp;&nbsp;';
+    		$result .= Display::return_icon('login_as_na.png', get_lang('LoginAs')).'&nbsp;&nbsp;';
     	}
     } else {
-    	$result .= Display::return_icon('login_as_na.gif', get_lang('LoginAs')).'&nbsp;&nbsp;';
+    	$result .= Display::return_icon('login_as_na.png', get_lang('LoginAs')).'&nbsp;&nbsp;';
     }
 
 	if ($current_user_status_label != $statusname[STUDENT]) {
@@ -588,14 +589,12 @@ function modify_filter($user_id, $url_params, $row) {
 		}
 	}
 
-
 	if ($is_admin) {
 		$result .= Display::return_icon('admin_star.png', get_lang('IsAdministrator'),array('width'=> ICON_SIZE_SMALL, 'heigth'=> ICON_SIZE_SMALL));
 	} else {
 		$result .= Display::return_icon('admin_star_na.png', get_lang('IsNotAdministrator'));
 	}
 
-
 	// actions for assigning sessions, courses or users
 	if (api_is_session_admin()) {
 		/*if ($row[0] == api_get_user_id()) {
@@ -673,8 +672,6 @@ function status_filter($status) {
 	return $statusname[$status];
 }
 
-$action = isset($_REQUEST["action"]) ? $_REQUEST["action"] : null;
-
 if (isset($_GET['keyword']) || isset($_GET['keyword_firstname'])) {
     $interbreadcrumb[] = array ("url" => 'index.php', "name" => get_lang('PlatformAdmin'));
     $interbreadcrumb[] = array ("url" => 'user_list.php', "name" => get_lang('UserList'));
@@ -687,7 +684,7 @@ if (isset($_GET['keyword']) || isset($_GET['keyword_firstname'])) {
 $message = '';
 
 if (!empty($action)) {
-	$check = Security::check_token('get');
+    $check = Security::check_token('get');
 	if ($check) {
 		switch ($action) {
             case 'add_user_to_my_url':
@@ -698,12 +695,6 @@ if (!empty($action)) {
                     $message = get_lang('UserAdded').' '.$user_info['firstname'].' '.$user_info['lastname'].' ('.$user_info['username'].')';
                     $message  = Display::return_message($message, 'confirmation');
                 }
-                break;
-            case 'login_as':
-                $login_as_user_id = $_GET["user_id"];
-                if (isset ($login_as_user_id)) {
-                    login_user($login_as_user_id);
-                }
                 break;
 			case 'show_message' :
                 if (!empty($_GET['message'])) {

+ 1 - 1
main/attendance/attendance_add.php

@@ -35,7 +35,7 @@ $form->add_html_editor('description', get_lang('Description'), false, false, arr
 // Adavanced Parameters
 
 $advanced = '<a href="javascript://" class = "advanced_parameters" ><span id="img_plus_and_minus">&nbsp;'.Display::return_icon('div_show.gif',get_lang('Show'),array('style'=>'vertical-align:middle')).' '.get_lang('AdvancedParameters').'</span></a>';
-if (Gradebook::is_active()) {  
+if ((api_get_session_id() != 0 && Gradebook::is_active()) || api_get_session_id() == 0) {   
     $form -> addElement('advanced_settings',$advanced);
 
     $form->addElement('html','<div id="id_qualify" style="display:none">');

+ 5 - 4
main/auth/external_login/ldap.inc.php

@@ -1,6 +1,6 @@
 <?php
 
-// External login module : LDAP 
+// External login module : LDAP
 /**
  * This files is included by newUser.ldap.php and login.ldap.php
  * It implements the functions nedded by both files
@@ -307,15 +307,16 @@ function extldap_add_user_by_array($data, $update_if_exists = true)
     $password        = $data[$passwordKey][0];
 
     // Structure
-    $structure       = $data['edupersonprimaryorgunitdn'][0];
+  /*  $structure       = $data['edupersonprimaryorgunitdn'][0];
     $array_structure = explode(",", $structure);
     $array_val       = explode("=", $array_structure[0]);
     $etape           = $array_val[1];
     $array_val       = explode("=", $array_structure[1]);
     $annee           = $array_val[1];
-
+*/
     // To ease management, we add the step-year (etape-annee) code
-    $official_code = $etape."-".$annee;
+    //$official_code = $etape."-".$annee;
+    $official_code = api_convert_encoding($data[$extldap_user_correspondance['official_code']][0], api_get_system_encoding(), 'UTF-8');
     $auth_source   = 'ldap';
 
     // No expiration date for students (recover from LDAP's shadow expiry)

+ 2 - 7
main/calendar/agenda.inc.php

@@ -1549,11 +1549,7 @@ function change_visibility($tool, $id, $visibility)
     $TABLE_ITEM_PROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
     $tool = Database::escape_string($tool);
     $id = Database::escape_string($id);
-    /*
-      $sql="SELECT * FROM $TABLE_ITEM_PROPERTY WHERE tool='".TOOL_CALENDAR_EVENT."' AND ref='$id'";
-      $result=Database::query($sql) or die (Database::error());
-      $row=Database::fetch_array($result);
-     */
+
     if ($visibility == 0) {
         $sql_visibility = "UPDATE $TABLE_ITEM_PROPERTY SET visibility='0' WHERE tool='$tool' AND ref='$id'";
         api_item_property_update($_course, TOOL_CALENDAR_EVENT, $id, "invisible", api_get_user_id());
@@ -1584,8 +1580,7 @@ function display_courseadmin_links($filter = 0)
         $actions = "<a href='agenda_js.php?type=course&".api_get_cidreq()."'>".Display::return_icon('calendar.png', get_lang('Agenda'), '', ICON_SIZE_MEDIUM)."</a>";
     }
 
-    // $actions .= "<a href='agenda_list.php?type=course&".api_get_cidreq()."'>".Display::return_icon('list.png', get_lang('Agenda'), '', ICON_SIZE_MEDIUM)."</a>";
-
+    $actions .= "<a href='agenda_list.php?type=course&".api_get_cidreq()."'>".Display::return_icon('week.png', get_lang('Agenda'), '', ICON_SIZE_MEDIUM)."</a>";
     $actions .= "<a href='agenda.php?".api_get_cidreq()."&amp;sort=asc&amp;toolgroup=".api_get_group_id()."&action=add'>".Display::return_icon('new_event.png', get_lang('AgendaAdd'), '', ICON_SIZE_MEDIUM)."</a>";
     $actions .= "<a href='agenda.php?".api_get_cidreq()."&action=importical'>".Display::return_icon('import_calendar.png', get_lang('ICalFileImport'), '', ICON_SIZE_MEDIUM)."</a>";
     $actions .= $form;

+ 39 - 6
main/calendar/agenda.lib.php

@@ -9,6 +9,9 @@ class Agenda
     public $events = array();
     public $type = 'personal'; // personal, admin or course
 
+    /**
+     *
+     */
     public function __construct()
     {
         //Table definitions
@@ -556,22 +559,23 @@ class Agenda
 
                     GROUP BY id";
         } else {
+            $visibilityCondition = "ip.visibility='1' AND";
             if (api_is_allowed_to_edit()) {
                 if ($user_id == 0) {
                     $where_condition = "";
                 } else {
                     $where_condition = "( ip.to_user_id=".$user_id. ") AND ";
                 }
+                $visibilityCondition = " (ip.visibility IN ('1', '0')) AND ";
             } else {
                 $where_condition = "( ip.to_user_id=$user_id OR ip.to_group_id='0') AND ";
             }
-
             $sql = "SELECT DISTINCT agenda.*, ip.visibility, ip.to_group_id, ip.insert_user_id, ip.ref, to_user_id
                     FROM ".$tlb_course_agenda." agenda, ".$tbl_property." ip
                     WHERE   agenda.id = ip.ref AND
                             ip.tool='".TOOL_CALENDAR_EVENT."' AND
                             $where_condition
-                            ip.visibility='1' AND
+                            $visibilityCondition
                             agenda.c_id = $course_id AND
                             ip.c_id = $course_id AND
                             agenda.session_id = $session_id AND
@@ -659,13 +663,12 @@ class Agenda
                 }
 
                 $event['sent_to'] = '';
-                //$event['type']    = $this->type;
                 $event['type'] = 'course';
                 if ($row['session_id'] != 0) {
                     $event['type'] = 'session';
                 }
 
-                //Event Sent to a group?
+                // Event Sent to a group?
                 if (isset($row['to_group_id']) && !empty($row['to_group_id'])) {
                     $sent_to = array();
                     if (!empty($group_to_array)) {
@@ -685,7 +688,7 @@ class Agenda
                     if (!empty($user_to_array)) {
                         foreach ($user_to_array as $item) {
                             $user_info = api_get_user_info($item);
-                            // add username as tooltip for $event['sent_to'] - ref #4226
+                            // Add username as tooltip for $event['sent_to'] - ref #4226
                             $username = api_htmlentities(sprintf(get_lang('LoginX'), $user_info['username']), ENT_QUOTES);
                             $sent_to[] = "<span title='".$username."'>".$user_info['complete_name']."</span>";
                         }
@@ -701,6 +704,8 @@ class Agenda
                 }
 
                 $event['description'] = $row['content'];
+                $event['visibility'] = $row['visibility'];
+                $event['real_id'] = $row['id'];
                 $event['allDay'] = isset($row['all_day']) && $row['all_day'] == 1 ? $row['all_day'] : 0;
 
                 $this->events[] = $event;
@@ -764,7 +769,9 @@ class Agenda
 
     /**
      * Format needed for the Fullcalendar js lib
-     *  @param string UTC time
+     *
+     * @param string $utc_time
+     * @return bool|string
      */
     function format_event_date($utc_time)
     {
@@ -968,6 +975,10 @@ class Agenda
         $form->display();
     }
 
+    /**
+     * @param FormValidator $form
+     * @param $to_already_selected
+     */
     static function show_to_form($form, $to_already_selected)
     {
         $order = 'lastname';
@@ -979,4 +990,26 @@ class Agenda
 
         self::construct_not_selected_select_form_validator($form, $group_list, $user_list, $to_already_selected);
     }
+
+    /**
+     * @param int $id
+     * @param int $visibility 0= invisible, 1 visible
+     * @param array $courseInfo
+     * @param int $userId
+     */
+    public static function changeVisibility($id, $visibility, $courseInfo, $userId = null)
+    {
+        $id = Database::escape_string($id);
+        if (empty($userId)) {
+            $userId = api_get_user_id();
+        } else {
+            $userId = intval($userId);
+        }
+
+        if ($visibility == 0) {
+            api_item_property_update($courseInfo, TOOL_CALENDAR_EVENT, $id, "invisible", $userId);
+        } else {
+            api_item_property_update($courseInfo, TOOL_CALENDAR_EVENT, $id, "visible", $userId);
+        }
+    }
 }

+ 7 - 2
main/calendar/agenda_js.php

@@ -12,7 +12,7 @@ $language_file = array('agenda', 'group', 'announcements');
 // use anonymous mode when accessing this course tool
 $use_anonymous = true;
 
-//Calendar type
+// Calendar type
 $type = isset($_REQUEST['type']) && in_array($_REQUEST['type'], array('personal', 'course', 'admin')) ? $_REQUEST['type'] : 'personal';
 
 if ($type == 'personal') {
@@ -144,9 +144,14 @@ if (api_is_allowed_to_edit(false, true) OR
         }
         $actions = display_courseadmin_links($filter);
     }
-    $tpl->assign('actions', $actions);
+} else {
+    if ($type == 'course') {
+        $actions = "<a href='agenda_list.php?type=course&".api_get_cidreq()."'>".Display::return_icon('week.png', get_lang('Agenda'), '', ICON_SIZE_MEDIUM)."</a>";
+    }
 }
 
+$tpl->assign('actions', $actions);
+
 //Calendar Type : course, admin, personal
 $tpl->assign('type', $type);
 

+ 19 - 1
main/calendar/agenda_list.php

@@ -13,12 +13,30 @@ require_once '../inc/global.inc.php';
 require_once 'agenda.lib.php';
 require_once 'agenda.inc.php';
 
-$tpl = new Template(get_lang('Agenda'));
+$interbreadcrumb[] = array("url" => api_get_path(WEB_CODE_PATH)."calendar/agenda_js.php?".api_get_cidreq(), "name" => get_lang('Agenda'));
+
+$tpl = new Template(get_lang('Events'));
+
 $agenda = new Agenda();
 $agenda->type = 'course'; //course,admin or personal
 
 $events = $agenda->get_events(null, null, api_get_course_int_id(), api_get_group_id(), null, 'array');
+$url = api_get_path(WEB_CODE_PATH).'calendar/agenda_list.php?'.api_get_cidreq();
+$tpl->assign('url', $url);
 $tpl->assign('agenda_events', $events);
+$actions = '<a href="'.api_get_path(WEB_CODE_PATH).'calendar/agenda_js.php?type=course&'.api_get_cidreq().'">'.
+            Display::return_icon('calendar.png', get_lang('Agenda'), '', ICON_SIZE_MEDIUM)."</a>";
+$tpl->assign('actions', $actions);
+$tpl->assign('is_allowed_to_edit', api_is_allowed_to_edit());
+
+if (api_is_allowed_to_edit()) {
+    if (isset($_GET['action']) && $_GET['action'] == 'change_visibility') {
+        $courseInfo = api_get_course_info();
+        $agenda->changeVisibility($_GET['id'], $_GET['visibility'], $courseInfo);
+        header('Location: '.$url);
+        exit;
+    }
+}
 
 // Loading Agenda template
 $content = $tpl->fetch('default/agenda/event_list.tpl');

+ 36 - 67
main/course_info/infocours.php

@@ -49,7 +49,7 @@ function is_settings_editable() {
 
 /* MAIN CODE */
 if (!$is_allowedToEdit) {
-	api_not_allowed(true);
+    api_not_allowed(true);
 }
 
 $show_delete_watermark_text_message = false;
@@ -66,7 +66,9 @@ $tbl_course_user       = Database :: get_main_table(TABLE_MAIN_COURSE_USER);
 $tbl_course            = Database :: get_main_table(TABLE_MAIN_COURSE);
 
 // Get all course categories
-$sql = "SELECT code,name FROM ".$table_course_category." WHERE auth_course_child ='TRUE'  OR code = '".Database::escape_string($_course['categoryCode'])."'  ORDER BY tree_pos";
+$sql = "SELECT code,name FROM ".$table_course_category."
+        WHERE auth_course_child ='TRUE'  OR code = '".Database::escape_string($_course['categoryCode'])."'
+        ORDER BY tree_pos";
 $res = Database::query($sql);
 
 $s_select_course_tutor_name = "SELECT tutor_name FROM $tbl_course WHERE code='$course_code'";
@@ -74,11 +76,15 @@ $q_tutor = Database::query($s_select_course_tutor_name);
 $s_tutor = Database::result($q_tutor, 0, 'tutor_name');
 
 $target_name = api_sort_by_first_name() ? 'firstname' : 'lastname';
-$s_sql_course_titular = "SELECT DISTINCT username, lastname, firstname FROM $tbl_user as user, $tbl_course_user as course_rel_user WHERE (course_rel_user.status='1') AND user.user_id=course_rel_user.user_id AND course_code='".$course_code."' ORDER BY ".$target_name." ASC";
+$s_sql_course_titular = "SELECT DISTINCT username, lastname, firstname
+    FROM $tbl_user as user, $tbl_course_user as course_rel_user
+    WHERE (course_rel_user.status='1') AND user.user_id=course_rel_user.user_id AND course_code='".$course_code."'
+    ORDER BY ".$target_name." ASC";
 $q_result_titulars = Database::query($s_sql_course_titular);
 
 if (Database::num_rows($q_result_titulars) == 0) {
-	$sql = "SELECT username, lastname, firstname FROM $tbl_user as user, $tbl_admin as admin WHERE admin.user_id=user.user_id ORDER BY ".$target_name." ASC";
+    $sql = "SELECT username, lastname, firstname FROM $tbl_user as user, $tbl_admin as admin
+            WHERE admin.user_id=user.user_id ORDER BY ".$target_name." ASC";
 	$q_result_titulars = Database::query($sql);
 }
 
@@ -127,7 +133,7 @@ if ($form->validate() && is_settings_editable()) {
 $course_path = api_get_path(SYS_COURSE_PATH).$currentCourseRepository;   // course path
 if (file_exists($course_path.'/course-pic85x85.png')) {
     $course_web_path = api_get_path(WEB_COURSE_PATH).$currentCourseRepository;   // course web path
-    $course_medium_image = $course_web_path.'/course-pic85x85.png?'.rand(1,1000); // redimensioned image 85x85
+    $course_medium_image = $course_web_path.'/course-pic85x85.png?'.rand(1, 1000); // redimensioned image 85x85
     $image_html =  '<div class="row"><div class="formw"><img src="'.$course_medium_image.'" /></div></div>';
 }
 $form->addElement('html', $image_html);
@@ -137,19 +143,6 @@ $form->add_textfield('title', get_lang('Title'), true, array('class' => 'span6')
 $form->applyFilter('title', 'html_filter');
 $form->applyFilter('title', 'trim');
 
-//$form->add_textfield('tutor_name', get_lang('Professors'), true, array ('size' => '60'));
-
-//the teacher doesn't need to change the trainer only the admin can do that
-/*
-$prof = &$form->addElement('select', 'tutor_name', get_lang('Teacher'), $a_profs, array('style'=>'width:350px', 'class'=>'chzn-select', 'id'=>'tutor_name'));
-$form->applyFilter('tutor_name', 'html_filter');
-$prof -> setSelected($s_selected_tutor);
- * /
-
-/*$visual_code=$form->addElement('text', 'visual_code', get_lang('Code'));
-$visual_code->freeze();
-$form->applyFilter('visual_code', 'strtoupper');*/
-
 $form->addElement('select', 'category_code', get_lang('Fac'), $categories, array('style'=>'width:350px', 'class'=>'chzn-select', 'id'=>'category_code'));
 $form->addElement('select_language', 'course_language', array(get_lang('Ln'), get_lang('TipLang')));
 
@@ -159,8 +152,6 @@ $form->applyFilter('department_name', 'trim');
 
 $form->add_textfield('department_url', get_lang('DepartmentUrl'), false, array('class' => 'span5'));
 $form->applyFilter('department_url', 'html_filter');
-//$form->addRule('tutor_name', get_lang('ThisFieldIsRequired'), 'required');
-
 
 // Picture
 $form->addElement('file', 'picture', get_lang('AddPicture'));
@@ -182,7 +173,7 @@ $group = array();
 $group[]=$form->createElement('select_theme', 'course_theme', null, array('class'=>' ', 'id'=>'course_theme_id'));
 $form->addGroup($group, '', array(get_lang("Stylesheets")), '');
 
-$form->addElement('label', get_lang('DocumentQuota'),  format_file_size(DocumentManager::get_course_quota()));
+$form->addElement('label', get_lang('DocumentQuota'), format_file_size(DocumentManager::get_course_quota()));
 
 $form->addElement('style_submit_button', null, get_lang('SaveSettings'), 'class="save"');
 $form->addElement('html', '</div></div>');
@@ -224,6 +215,21 @@ $form->addElement('textarea', 'legal', get_lang('CourseLegalAgreement'), array('
 $form->addElement('style_submit_button', null, get_lang('SaveSettings'), 'class="save"');
 $form->addElement('html', '</div></div>');
 
+// Documents
+if (api_get_setting('documents_default_visibility_defined_in_course') == 'true') {
+    $form->addElement('html', '<div> <h3>'.Display::return_icon('folder.png', Security::remove_XSS(get_lang('Documents')),'',ICON_SIZE_SMALL).' '.Security::remove_XSS(get_lang('Documents')).'</h3><div>');
+
+    $group = array(
+        $form->createElement('radio', 'documents_default_visibility', null, get_lang('Visible'), 'visible'),
+        $form->createElement('radio', 'documents_default_visibility', null, get_lang('Invisible'), 'invisible')
+    );
+    $form->addGroup($group, '', array(get_lang("DocumentsDefaultVisibility")), '');
+
+    $form->addElement('style_submit_button', null, get_lang('SaveSettings'), 'class="save"');
+
+    $form->addElement('html', '</div></div>');
+}
+
 // EMAIL NOTIFICATIONS
 $form->addElement('html', '<div> <h3>'.Display::return_icon('mail.png', Security::remove_XSS(get_lang('EmailNotifications')),'',ICON_SIZE_SMALL).' '.Security::remove_XSS(get_lang('EmailNotifications')).'</h3><div>');
 
@@ -243,7 +249,6 @@ $group[]=$form->createElement('radio', 'email_alert_manager_on_new_doc', get_lan
 $group[]=$form->createElement('radio', 'email_alert_manager_on_new_doc', null, get_lang('WorkEmailAlertDeactivate'), 0);
 $form->addGroup($group, '', array(get_lang("WorkEmailAlert")), '');
 
-
 $group = array();
 $group[]=$form->createElement('radio', 'email_alert_on_new_doc_dropbox', get_lang('DropboxEmailAlert'), get_lang('DropboxEmailAlertActivate'), 1);
 $group[]=$form->createElement('radio', 'email_alert_on_new_doc_dropbox', null, get_lang('DropboxEmailAlertDeactivate'), 0);
@@ -253,8 +258,6 @@ $group = array();
 $group[]=$form->createElement('radio', 'email_alert_manager_on_new_quiz', get_lang('QuizEmailAlert'), get_lang('QuizEmailAlertActivate'), 1);
 $group[]=$form->createElement('radio', 'email_alert_manager_on_new_quiz', null, get_lang('QuizEmailAlertDeactivate'), 0);
 $form->addGroup($group, '', array(get_lang("QuizEmailAlert")), '');
-
-
 $form->addElement('style_submit_button', null, get_lang('SaveSettings'), 'class="save"');
 
 $form->addElement('html', '</div></div>');
@@ -285,7 +288,6 @@ $form->addGroup($group, '', array(get_lang("AllowUserViewUserList")), '');
 $form->addElement('style_submit_button', null, get_lang('SaveSettings'), 'class="save"');
 $form->addElement('html', '</div></div>');
 
-
 // CHAT SETTINGS
 $form->addElement('html', '<div><h3>'.Display::return_icon('chat.png', Security::remove_XSS(get_lang('ConfigChat')),'',ICON_SIZE_SMALL).' '.Security::remove_XSS(get_lang('ConfigChat')).'</h3><div>');
 
@@ -297,7 +299,6 @@ $form->addGroup($group, '', array(get_lang("AllowOpenchatWindow")), '');
 $form->addElement('style_submit_button', null, get_lang('SaveSettings'), 'class="save"');
 $form->addElement('html', '</div></div>');
 
-
 // LEARNING PATH
 $form->addElement('html', '<div><h3>'.Display::return_icon('scorms.png', get_lang('ConfigLearnpath'),'',ICON_SIZE_SMALL).' '.Security::remove_XSS(get_lang('ConfigLearnpath')).'</h3><div>');
 
@@ -314,7 +315,6 @@ if (api_get_setting('allow_course_theme') == 'true') {
     $group[]=$form->createElement('radio', 'allow_learning_path_theme', get_lang('AllowLearningPathTheme'), get_lang('AllowLearningPathThemeAllow'), 1);
     $group[]=$form->createElement('radio', 'allow_learning_path_theme', null, get_lang('AllowLearningPathThemeDisallow'), 0);
     $form->addGroup($group, '', array(get_lang("AllowLearningPathTheme")), '');
-
 }
 
 if (is_settings_editable()) {
@@ -339,12 +339,9 @@ $group[]=$form->createElement('radio', 'display_info_advance_inside_homecourse',
 $form->addGroup($group, '', array(get_lang("InfoAboutAdvanceInsideHomeCourse")), '');
 
 $form->addElement('style_submit_button', null, get_lang('SaveSettings'), 'class="save"');
-
 $form->addElement('html', '</div></div>');
 
-
 // Certificate settings
-
 if (api_get_setting('allow_public_certificates')=='true') {
     $form->addElement('html', '<div><h3>'.Display::return_icon('certificate.png', Security::remove_XSS(get_lang('Certificates')),'',ICON_SIZE_SMALL).' '.Security::remove_XSS(get_lang('Certificates')).'</h3><div>');
     $group = array();
@@ -356,7 +353,6 @@ if (api_get_setting('allow_public_certificates')=='true') {
     $form->addElement('html', '</div></div>');
 }
 
-
 // Plugin course settings
 
 $app_plugin = new AppPlugin();
@@ -379,39 +375,13 @@ $values['visibility']                   = $_course['visibility'];
 $values['subscribe']                    = $course_access_settings['subscribe'];
 $values['unsubscribe']                  = $course_access_settings['unsubscribe'];
 $values['course_registration_password'] = $all_course_information['registration_code'];
-
 $values['legal']                        = $all_course_information['legal'];
 $values['activate_legal']               = $all_course_information['activate_legal'];
 
-// Get send_mail_setting (auth)from table
-$values['email_alert_to_teacher_on_new_user_in_course']= api_get_course_setting('email_alert_to_teacher_on_new_user_in_course');
-// Get send_mail_setting (work)from table
-$values['email_alert_manager_on_new_doc']           = api_get_course_setting('email_alert_manager_on_new_doc');
-// Get send_mail_setting (dropbox) from table
-$values['email_alert_on_new_doc_dropbox']           = api_get_course_setting('email_alert_on_new_doc_dropbox');
-// Get send_mail_setting (work)from table
-$values['email_alert_manager_on_new_quiz']          = api_get_course_setting('email_alert_manager_on_new_quiz');
-// Get allow_user_edit_agenda from table
-$values['allow_user_edit_agenda']                   = api_get_course_setting('allow_user_edit_agenda');
-// Get allow_user_edit_announcement from table
-$values['allow_user_edit_announcement']             = api_get_course_setting('allow_user_edit_announcement');
-// Get allow_user_image_forum from table
-$values['allow_user_image_forum']                   = api_get_course_setting('allow_user_image_forum');
-// Get allow_open_chat_window from table
-$values['allow_open_chat_window']                   = api_get_course_setting('allow_open_chat_window');
-// Get course_theme from table
-$values['course_theme']                             = api_get_course_setting('course_theme');
-// Get allow_learning_path_theme from table
-$values['allow_learning_path_theme']                = api_get_course_setting('allow_learning_path_theme');
-//Get allow show user list
-$values['allow_user_view_user_list']                = api_get_course_setting('allow_user_view_user_list');
-//Get allow show user list
-$values['display_info_advance_inside_homecourse']   = api_get_course_setting('display_info_advance_inside_homecourse');
-$values['email_alert_students_on_new_homework']     = api_get_course_setting('email_alert_students_on_new_homework');
-
-$values['enable_lp_auto_launch']                    = api_get_course_setting('enable_lp_auto_launch');
-$values['pdf_export_watermark_text']                = api_get_course_setting('pdf_export_watermark_text');
-$values['allow_public_certificates']                = api_get_course_setting('allow_public_certificates');
+$courseSettings = CourseManager::getCourseSettingVariables();
+foreach ($courseSettings as $setting) {
+    $values[$setting] = api_get_course_setting($setting);
+}
 
 $app_plugin->set_course_settings_defaults($values);
 
@@ -452,14 +422,13 @@ if ($form->validate() && is_settings_editable()) {
         WHERE code = '".$course_code."'";
     Database::query($sql);
 
-    // Update course_settings table - this assumes those records exist, otherwise triggers an error
-    $table_course_setting = Database::get_course_table(TABLE_COURSE_SETTING);
-    foreach ($update_values as $key =>$value) {
-        //We do not update variables that were already saved in the TABLE_MAIN_COURSE table
-        if (!in_array($key, $update_in_course_table)) {
-            Database::update($table_course_setting, array('value' => $update_values[$key]), array('variable = ? AND c_id = ?' => array($key, api_get_course_int_id())));
+    // Insert/Updates course_settings table
+    foreach ($courseSettings as $setting) {
+        if (isset($update_values[$setting])) {
+            CourseManager::saveCourseConfigurationSetting($setting, $update_values[$setting], api_get_course_int_id());
         }
     }
+
     $app_plugin->save_course_settings($update_values);
     $cidReset = true;
     $cidReq = $course_code;

+ 10 - 4
main/coursecopy/classes/CourseBuilder.class.php

@@ -491,8 +491,9 @@ class CourseBuilder
         // Building a fictional test for collecting orphan questions.
         $build_orphan_questions = !empty($_POST['recycle_option']); // When a course is emptied this option should be activated (true).
 
-        //1st union gets the orphan questions from deleted exercises
-        //2nd union gets the orphan questions from question that were deleted in a exercise.
+        // 1st union gets the orphan questions from deleted exercises
+        // 2nd union gets the orphan questions from question that were deleted in a exercise.
+
         $sql = " (
                     SELECT question_id, q.* FROM $table_que q INNER JOIN $table_rel r
                     ON (q.c_id = r.c_id AND q.id = r.question_id)
@@ -521,8 +522,13 @@ class CourseBuilder
             $build_orphan_questions = true;
             $orphanQuestionIds = array();
             while ($obj = Database::fetch_object($db_result)) {
-                //Avoid adding the same question twice
-                $obj->id = $obj->question_id;
+
+                // Orphan questions
+                if (!empty($obj->question_id)) {
+                    $obj->id = $obj->question_id;
+                }
+
+                // Avoid adding the same question twice
                 if (!isset($this->course->resources[$obj->id])) {
                     $question = new QuizQuestion($obj->id, $obj->question, $obj->description, $obj->ponderation, $obj->type, $obj->position, $obj->picture,$obj->level, $obj->extra);
                     $sql = "SELECT * FROM $table_ans WHERE c_id = $course_id AND question_id = ".$obj->id;

+ 10 - 7
main/coursecopy/classes/CourseRecycler.class.php

@@ -362,13 +362,16 @@ class CourseRecycler
                     while ($obj = Database::fetch_object($db_result)) {
                         $orphan_ids[] = $obj->id;
                     }
-                    $orphan_ids = implode(',', $orphan_ids);
-                    $sql = "DELETE FROM ".$table_rel." WHERE c_id = ".$this->course_id." AND question_id IN(".$orphan_ids.")";
-                    Database::query($sql);
-                    $sql = "DELETE FROM ".$table_qui_ans." WHERE c_id = ".$this->course_id." AND question_id IN(".$orphan_ids.")";
-                    Database::query($sql);
-                    $sql = "DELETE FROM ".$table_qui_que." WHERE c_id = ".$this->course_id." AND id IN(".$orphan_ids.")";
-                    Database::query($sql);
+                    if (!empty($orphan_ids)) {
+                        $orphan_ids = implode(',', $orphan_ids);
+
+                        $sql = "DELETE FROM ".$table_rel." WHERE c_id = ".$this->course_id." AND question_id IN(".$orphan_ids.")";
+                        Database::query($sql);
+                        $sql = "DELETE FROM ".$table_qui_ans." WHERE c_id = ".$this->course_id." AND question_id IN(".$orphan_ids.")";
+                        Database::query($sql);
+                        $sql = "DELETE FROM ".$table_qui_que." WHERE c_id = ".$this->course_id." AND id IN(".$orphan_ids.")";
+                        Database::query($sql);
+                    }
                 }
                 // Also delete questions categories and options
                 $sql = "DELETE FROM $table_qui_que_rel_cat WHERE c_id = ".$this->course_id;

+ 53 - 2
main/cron/import_csv.php

@@ -95,7 +95,9 @@ class ImportCsv
                 if ($fileInfo['extension'] == 'csv') {
                     // teachers_yyyymmdd.csv, courses_yyyymmdd.csv, students_yyyymmdd.csv and sessions_yyyymmdd.csv
                     $parts = explode('_', $fileInfo['filename']);
-                    $method = 'import'.ucwords($parts[1]);
+                    $preMethod = ucwords($parts[1]);
+                    $preMethod = str_replace('-static', 'Static', $preMethod);
+                    $method = 'import'.$preMethod;
 
                     if (method_exists($this, $method)) {
                          $fileToProcess[$parts[1]][] = array(
@@ -105,6 +107,8 @@ class ImportCsv
                         //$this->$method($path.$fileInfo['basename']);
                     } else {
                         echo "Error - This file '$file' can't be processed.".PHP_EOL;
+                        echo "Trying to call $method".PHP_EOL;
+
                         echo "The file have to has this format:".PHP_EOL;
                         echo "prefix_students_ddmmyyyy.csv, prefix_teachers_ddmmyyyy.csv, prefix_courses_ddmmyyyy.csv, prefix_sessions_ddmmyyyy.csv ".PHP_EOL;
                         exit;
@@ -117,7 +121,7 @@ class ImportCsv
                 exit;
             }
 
-            $sections = array('students', 'teachers', 'courses', 'sessions');
+            $sections = array('students', 'teachers', 'courses', 'sessions', 'unsubscribe-static');
 
             $this->prepareImport();
 
@@ -138,6 +142,8 @@ class ImportCsv
 
                 }
             }
+
+
         }
     }
 
@@ -602,6 +608,51 @@ class ImportCsv
         $this->moveFile($file);
     }
 
+    /**
+     * @param string $file
+     */
+    private function importUnsubscribeStatic($file)
+    {
+        $data = Import::csv_to_array($file);
+
+        //$language = $this->defaultLanguage;
+
+        if (!empty($data)) {
+            $this->logger->addInfo(count($data)." records found.");
+            foreach ($data as $row) {
+                $chamiloUsername = $row['UserName'];
+                $chamiloCourseCode = $row['CourseCode'];
+                $systemSessionId= $row['SessionID'];
+
+                $sessionId = SessionManager::get_session_id_from_original_id($systemSessionId, $this->extraFieldIdNameList['session']);
+
+                if (empty($sessionId)) {
+                    $this->logger->addError('Session does not exists: '.$systemSessionId);
+                    continue;
+                }
+
+                $courseInfo = api_get_course_info($chamiloCourseCode);
+                if (empty($courseInfo)) {
+                    $this->logger->addError('Course does not exists: '.$courseInfo);
+                    continue;
+                }
+
+                $userId = Usermanager::get_user_id_from_username($chamiloUsername);
+
+                if (empty($userId)) {
+                    $this->logger->addError('User does not exists: '.$chamiloUsername);
+                    continue;
+                }
+
+                CourseManager::unsubscribe_user($userId, $courseInfo['code'], $sessionId);
+                $this->logger->addError("User '$chamiloUsername' was removed from session: #$sessionId, Course: ".$courseInfo['code']);
+            }
+        }
+    }
+
+    /**
+     *  Dump database tables
+     */
     private function dumpDatabaseTables()
     {
         echo 'Dumping tables'.PHP_EOL;

+ 45 - 13
main/document/document.php

@@ -54,25 +54,46 @@ if (isset($_SESSION['temp_audio_nanogong']) && !empty($_SESSION['temp_audio_nano
 if (isset($_SESSION['temp_realpath_image']) && !empty($_SESSION['temp_realpath_image']) && is_file($_SESSION['temp_realpath_image'])) {
     unlink($_SESSION['temp_realpath_image']);
 }
-
+$course_info = api_get_course_info();
+$course_dir = $course_info['path'].'/document';
+$sys_course_path = api_get_path(SYS_COURSE_PATH);
+$base_work_dir = $sys_course_path.$course_dir;
+$http_www = api_get_path(WEB_COURSE_PATH).$_course['path'].'/document';
+$document_path = $base_work_dir;
 //Removing sessions
 unset($_SESSION['draw_dir']);
 unset($_SESSION['paint_dir']);
 unset($_SESSION['temp_audio_nanogong']);
 
+$htmlHeadXtra[] = '<script>
+function startApplet() {
+    appletsource = "<applet code=\"com.hammurapi.jcapture.JCaptureApplet.class\" archive=\"jcapture/lib/jcapture.jar\" width=\"0\" height=\"0\">";
+    appletsource += "<param name=\"outputDir\" value=\"'.$base_work_dir.'\">";
+    appletsource += "</applet>";
+    document.getElementById("appletplace").innerHTML=appletsource;
+}
+$(function() {
+    $("#jcapture").click(function(){
+        startApplet();
+    });
+});
+</script>';
+
 // Create directory certificates
 DocumentManager::create_directory_certificate_in_course(api_get_course_id());
 
-$course_info = api_get_course_info();
+
 
 if (empty($course_info)) {
     api_not_allowed(true);
 }
 
-$course_dir = $course_info['path'].'/document';
-$sys_course_path = api_get_path(SYS_COURSE_PATH);
-$base_work_dir = $sys_course_path.$course_dir;
-$http_www = api_get_path(WEB_COURSE_PATH).$_course['path'].'/document';
+
+
+?>
+
+<div id="appletplace"></div>
+<?php
 
 $dbl_click_id = 0; // Used for avoiding double-click
 
@@ -710,6 +731,10 @@ if ($is_allowed_to_edit || $group_member_with_upload_rights || is_my_shared_fold
                             null,
                             $session_id
                         )) {
+                            // Update visibility of the document in all sessions
+                            if (empty($session_id)) {
+                                DocumentManager::updateVisibilityFromAllSessions($_course, $documentId, $visibilityCommand, api_get_user_id());
+                            }
                             Display::display_confirmation_message(get_lang('VisibilityChanged').': '.$data['path']);
                         } else {
                             Display::display_error_message(get_lang('ViModProb'));
@@ -729,13 +754,16 @@ if ($is_allowed_to_edit || $group_member_with_upload_rights || is_my_shared_fold
                             null,
                             $session_id
                         )) {
+                            // Update visibility of the document in all sessions
+                            if (empty($session_id)) {
+                                DocumentManager::updateVisibilityFromAllSessions($_course, $documentId, $visibilityCommand, api_get_user_id());
+                            }
                             Display::display_confirmation_message(get_lang('VisibilityChanged').': '.$data['path']);
                         } else {
                             Display::display_error_message(get_lang('ViModProb'));
                         }
                         break;
                     case 'delete':
-
                         foreach ($files as $path) {
                             if (!$is_allowed_to_edit) {
                                 if (DocumentManager::check_readonly($_course, api_get_user_id(), $path)) {
@@ -783,9 +811,6 @@ if ($is_allowed_to_edit || $group_member_with_upload_rights || is_my_shared_fold
 
                 if ($created_dir) {
                     Display::display_confirmation_message('<span title="'.$created_dir.'">'.get_lang('DirCr').'</span>', false);
-                    // Uncomment if you want to enter the created dir
-                    //$curdirpath = $created_dir;
-                    //$curdirpathurl = urlencode($curdirpath);
                 } else {
                     Display::display_error_message(get_lang('CannotCreateDir'));
                 }
@@ -828,6 +853,11 @@ if ($is_allowed_to_edit) {
         // Update item_property to change visibility
         if (api_item_property_update($_course, TOOL_DOCUMENT, $update_id, $visibility_command, api_get_user_id(), null, null, null, null, $session_id)) {
             Display::display_confirmation_message(get_lang('VisibilityChanged')); //don't use ViMod because firt is load ViMdod (Gradebook). VisibilityChanged (trad4all)
+
+            if (empty($session_id)) {
+                DocumentManager::updateVisibilityFromAllSessions($_course, $update_id, $visibility_command, api_get_user_id());
+            }
+
         } else {
             Display::display_error_message(get_lang('ViModProb'));
         }
@@ -937,8 +967,6 @@ if (api_get_group_id() != 0) {
     $folders = DocumentManager::get_all_document_folders($_course, api_get_group_id(), $is_allowed_to_edit || $group_member_with_upload_rights);
 }
 
-
-//$folders = DocumentManager::get_all_document_folders($_course, $to_group_id, $is_allowed_to_edit || $group_member_with_upload_rights);
 if ($folders === false) {
     $folders = array();
 }
@@ -1054,6 +1082,10 @@ if ($is_allowed_to_edit || $group_member_with_upload_rights || is_my_shared_fold
         echo '<a href="upload.php?'.api_get_cidreq().'&id='.$current_folder_id.'">';
         echo Display::display_icon('upload_file.png', get_lang('UplUploadDocument'), '', ICON_SIZE_MEDIUM).'</a>';
     }
+    
+    echo '<a href="#" style="margin-top:-5px;" id="jcapture">';
+    echo Display::display_icon('capture.png', get_lang('CatchScreenCasts'), '', ICON_SIZE_MEDIUM).'</a>';
+
     // Create directory
     if (!$is_certificate_mode) {
         ?>
@@ -1127,7 +1159,7 @@ if (isset($docs_and_folders) && is_array($docs_and_folders)) {
                 $count++;
             }
 
-            // Validacion when belongs to a session
+            // Validation when belongs to a session
             $session_img = api_get_session_image($document_data['session_id'], $_user['status']);
 
             // Document title with link

+ 11 - 0
main/document/jcapture/.settings/org.eclipse.jdt.core.prefs

@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6

+ 23 - 0
main/document/jcapture/JCaptureApplet.html

@@ -0,0 +1,23 @@
+<!-- WARNING: Eclipse auto-generated file.
+              Any modifications will be overwritten. -->
+<html>
+    <body>
+    
+<script>
+    function llamarAplett()
+    {
+        document.write("Prueba de sonido");
+        
+        alert("Prueba de sonido;")
+    }
+</script>    
+    
+    <input type="button" onclick="llamarAplett();"/>   
+    
+<!--     
+    <applet code='com.hammurapi.jcapture.JCaptureApplet.class' archive='lib\jcapture.jar' width='20' height='20'>
+        <param name='outputDir' value='C:\_temp'>
+    </applet>
+-->
+    </body>
+</html>

+ 48 - 0
main/document/jcapture/action.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * JCapture plugin
+ *
+ * @author Pavel Vlasov
+ */
+ 
+if (!defined('DOKU_INC')) die();
+if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
+require_once (DOKU_PLUGIN . 'action.php');
+ 
+class action_plugin_jcapture extends DokuWiki_Action_Plugin {
+ 
+  /**
+   * return some info
+   */
+  function getInfo(){
+    return array(
+                 'author' => 'Pavel Vlasov',
+                 'email'  => 'Pavel.Vlasov@hammurapi.com',
+                 'name'   => 'JCapture',
+                 'desc'   => 'Plugin for making screen captures.',
+                 'url'    => 'http://www.hammurapi.com/dokuwiki/doku.php/products:jcapture:start',
+                 );
+  }
+
+    /**
+     * Register the eventhandlers
+     */
+    function register(&$controller) {
+        $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'insert_button', array ());
+    }
+ 
+    /**
+     * Inserts the toolbar button
+     */
+    function insert_button(& $event, $param) {
+        $event->data[] = array (
+            'type' => 'JCapture',
+            'title' => 'Screen capture',
+            'icon' => '../../plugins/jcapture/camera.png',
+            'open' => '<abutton>',
+            'close' => '</abutton>',
+        );
+    }
+ 
+}
+

+ 99 - 0
main/document/jcapture/applet.php

@@ -0,0 +1,99 @@
+<?php 
+
+	//fix for Opera XMLHttpRequests
+	if(!count($_POST) && $HTTP_RAW_POST_DATA){
+	  parse_str($HTTP_RAW_POST_DATA, $_POST);
+	}
+	
+	if(!defined('DOKU_INC')) define('DOKU_INC',dirname(__FILE__).'/../../../');
+	require_once(DOKU_INC.'inc/init.php');
+	require_once(DOKU_INC.'inc/common.php');
+	require_once(DOKU_INC.'inc/pageutils.php');
+	require_once(DOKU_INC.'inc/auth.php');
+	//close sesseion
+	session_write_close();
+	
+	header('Content-Type: text/html; charset=utf-8');
+	
+	$hostName = "http".($_SERVER['HTTPS'] ? 's' : null).'://'.$_SERVER['HTTP_HOST']; 
+	
+	$imageFormat = "PNG";
+
+	$cookies;
+	foreach (array_keys($_COOKIE) as $cookieName) {
+		$cookies.=bin2hex($cookieName)."=".bin2hex($_COOKIE[$cookieName]).";";
+	} 
+	
+	$pageName = $_GET["pageName"];
+	$edid = $_GET["edid"];
+?>
+
+<script language="JavaScript" type="text/javascript"><!--
+    var _info = navigator.userAgent;
+    var _ns = false;
+    var _ns6 = false;
+    var _ie = (_info.indexOf("MSIE") > 0 && _info.indexOf("Win") > 0 && _info.indexOf("Windows 3.1") < 0);
+//--></script>
+    <comment>
+        <script language="JavaScript" type="text/javascript"><!--
+        var _ns = (navigator.appName.indexOf("Netscape") >= 0 && ((_info.indexOf("Win") > 0 && _info.indexOf("Win16") < 0 && java.lang.System.getProperty("os.version").indexOf("3.5") < 0) || (_info.indexOf("Sun") > 0) || (_info.indexOf("Linux") > 0) || (_info.indexOf("AIX") > 0) || (_info.indexOf("OS/2") > 0) || (_info.indexOf("IRIX") > 0)));
+        var _ns6 = ((_ns == true) && (_info.indexOf("Mozilla/5") >= 0));
+//--></script>
+    </comment>
+
+<script language="JavaScript" type="text/javascript"><!--
+    if (_ie == true) document.writeln('<object classid="clsid:CAFEEFAC-0017-0000-0020-ABCDEFFEDCBA" NAME = "jCapture"  WIDTH = "1" HEIGHT = "1" codebase="http://java.sun.com/update/1.7.0/jinstall-1_7_0-windows-i586.cab#Version=7,0,0,0"><xmp>');
+    else if (_ns == true && _ns6 == false) document.writeln('<embed ' +
+	    'type="application/x-java-applet;jpi-version=1.7.0" \
+            ID = "jCaptureApplet" \
+            scriptable = "true" \
+            mayscript = "true" \
+            WIDTH = "1"
+            JAVA_CODEBASE = "/somenonexistingcodebase" \
+            HEIGHT = "1"
+            CODE = "com.hammurapi.jcapture.JCaptureApplet.class" \
+            ARCHIVE = "<?php echo DOKU_BASE; ?>lib/plugins/jcapture/lib/jcapture.jar" \
+            NAME = "jCapture" \
+            dokuBase ="<?php echo bin2hex(DOKU_BASE); ?>" \
+            sectok ="<?php echo getSecurityToken(); ?>" \
+            cookies ="<?php echo $cookies; ?>" \
+            authtok = "<?php echo auth_createToken(); ?>" \
+            pageName = "<?php echo $pageName; ?>" \
+            edid = "<?php echo $edid; ?>" \
+            host ="<?php echo $hostName; ?>" ' +
+	    'scriptable=false ' +
+	    'pluginspage="http://java.sun.com/products/plugin/index.html#download"><xmp>');
+//--></script>
+<applet id="jCaptureApplet" CODE = "com.hammurapi.jcapture.JCaptureApplet.class" WIDTH="1" HEIGHT="1" ARCHIVE = "<?php echo DOKU_BASE; ?>lib/plugins/jcapture/lib/jcapture.jar" NAME = "jCapture"></xmp>
+    <PARAM NAME = CODE VALUE = "com.hammurapi.jcapture.JCaptureApplet.class" >
+    <PARAM NAME = ARCHIVE VALUE = "<?php echo DOKU_BASE; ?>lib/plugins/jcapture/lib/jcapture.jar" >
+    <PARAM NAME = NAME VALUE = "jCapture" >
+    <param name="type" value="application/x-java-applet;jpi-version=1.7.0">
+    <param name="scriptable" value="true">
+    <param name="mayscript" value="true">
+    <PARAM NAME = "dokuBase" VALUE="<?php echo bin2hex(DOKU_BASE); ?>">
+    <PARAM NAME = "sectok" VALUE="<?php echo getSecurityToken(); ?>">
+    <PARAM NAME = "cookies" VALUE="<?php echo $cookies; ?>">
+    <PARAM NAME = "host" VALUE="<?php echo $hostName; ?>">
+    <PARAM NAME = "pageName" VALUE="<?php echo $pageName; ?>">
+    <PARAM NAME = "edid" VALUE="<?php echo $edid; ?>">
+    <PARAM NAME = CODEBASE VALUE = "/somenonexistingcodebase" >    
+    <PARAM NAME = "authtok" VALUE="<?php echo auth_createToken(); ?>">
+
+Java 2 Standard Edition v 1.7 or above is required for this applet.<br/>
+		Download it from <a href="http://java.sun.com">http://java.sun.com</a>.
+</applet>
+</embed>
+</object>
+
+<!--
+<APPLET CODE = "com.hammurapi.jcapture.JCaptureApplet.class" ARCHIVE = "<?php echo DOKU_BASE; ?>lib/plugins/jcapture/lib/jcapture.jar" NAME = "jCapture">
+<PARAM NAME = "dokuBase" VALUE="<?php echo bin2hex(DOKU_BASE); ?>">
+<PARAM NAME = "sectok" VALUE="<?php echo getSecurityToken(); ?>">
+<PARAM NAME = "cookies" VALUE="<?php echo $cookies; ?>">
+<PARAM NAME = "host" VALUE="<?php echo $hostName; ?>">
+Java 2 Standard Edition v 1.7 or above is required for this applet.<br/>
+		Download it from <a href="http://java.sun.com">http://java.sun.com</a>.
+
+</APPLET>
+-->

BIN
main/document/jcapture/camera.png


+ 21 - 0
main/document/jcapture/jcapture.jnlp

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jnlp spec="1.0+" codebase="" href="">
+    <information>
+        <title>jCapture</title>
+        <vendor>Hammurapi Group</vendor>
+    </information>
+    
+    <resources>
+        <!-- Application Resources -->
+        <j2se version="1.7+" href="http://java.sun.com/products/autodl/j2se" />
+        <jar href="lib/jcapture.jar" main="true" />
+    </resources>
+    
+    <applet-desc 
+         name="jCapture Applet"
+         main-class="com.hammurapi.jcapture.JCaptureApplet"
+         width="300"
+         height="300">
+     </applet-desc>
+     <update check="background"/>
+</jnlp>                                   

BIN
main/document/jcapture/lib/apache-mime4j-0.6.jar


BIN
main/document/jcapture/lib/commons-codec-1.3.jar


BIN
main/document/jcapture/lib/commons-logging-1.1.1.jar


BIN
main/document/jcapture/lib/httpclient-4.0.1.jar


BIN
main/document/jcapture/lib/httpcore-4.0.1.jar


BIN
main/document/jcapture/lib/httpmime-4.0.1.jar


BIN
main/document/jcapture/lib/jcapture.jar


BIN
main/document/jcapture/lib/transform-3.0.2.jar


+ 1 - 0
main/document/jcapture/license.txt

@@ -0,0 +1 @@
+GPL v 2

+ 19 - 0
main/document/jcapture/script.js

@@ -0,0 +1,19 @@
+function addBtnActionJCapture($btn, props, edid) {
+	$btn.click(function() {
+		var appletDiv = document.getElementById("jCaptureAppletDiv");
+		if (appletDiv==null) {
+	        	var oNewDiv = document.createElement("div");
+		        oNewDiv.id="jCaptureAppletDiv";
+		        //oNewDiv.style.display='none';
+		        document.body.appendChild(oNewDiv);
+			jQuery("#jCaptureAppletDiv").load(DOKU_BASE+"lib/plugins/jcapture/applet.php?edid="+edid+"&pageName="+document.forms['dw__editform'].elements['id'].value);
+		} else {
+	    		document.getElementById("jCaptureApplet").showCaptureFrame();
+		}	    
+        	return false;
+	});
+ 
+	return true;
+}
+
+

+ 1 - 0
main/document/jcapture/src/META-INF/services/com.hammurapi.jcapture.VideoEncoder

@@ -0,0 +1 @@
+com.hammurapi.jcapture.SwfEncoder

+ 17 - 0
main/document/jcapture/src/com/hammurapi/jcapture/AWTUtilitiesTranslucener.java

@@ -0,0 +1,17 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Frame;
+
+import com.sun.awt.AWTUtilities;
+import com.sun.awt.AWTUtilities.Translucency;
+
+public class AWTUtilitiesTranslucener extends Translucener {
+
+	@Override
+	protected void makeTranslucent(Frame frame) {
+        if (AWTUtilities.isTranslucencySupported(Translucency.TRANSLUCENT)) {
+        	AWTUtilities.setWindowOpacity(frame, 0.7f);	
+        }		
+	}
+
+}

+ 262 - 0
main/document/jcapture/src/com/hammurapi/jcapture/AbstractCaptureApplet.java

@@ -0,0 +1,262 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Component;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.ProxySelector;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import javax.swing.JApplet;
+import javax.swing.JOptionPane;
+import javax.swing.ProgressMonitorInputStream;
+import javax.swing.SwingUtilities;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.net.URLCodec;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.mime.content.InputStreamBody;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
+
+/**
+ * Base class for capture applets.
+ * @author Pavel
+ *
+ */
+public abstract class AbstractCaptureApplet extends JApplet {
+	
+	private static final String OUTPUT_DIR_PARAMETER = "outputDir";
+	
+	private CaptureFrame captureFrame;
+
+	@Override
+	public void stop() {
+		if (captureFrame!=null) {
+			captureFrame.dispose();
+			captureFrame = null;
+		}
+		backgroundProcessor.shutdown();
+		synchronized (closeables) {
+			Iterator<Closeable> cit = closeables.iterator();
+			while (cit.hasNext()) {
+				try {
+					cit.next().close();
+				} catch (Exception e) {
+					e.printStackTrace();
+				}
+				cit.remove();
+			}
+		}
+		super.stop();
+	}
+	
+	/**
+	 * Adds closeable to close in applet stop() method.
+	 * @param closeable
+	 */
+	public void addCloseable(Closeable closeable) {
+		synchronized (closeables) {
+			closeables.add(closeable);
+		}
+	}
+	
+	private Collection<Closeable> closeables = new ArrayList<Closeable>();
+
+	public void showCaptureFrame() {
+		if (captureFrame==null) {
+			createCaptureFrame();
+		}
+		captureFrame.setVisible(true);
+	}
+	
+	private ExecutorService backgroundProcessor;
+	
+	@Override
+	public void start() {		
+		super.start();
+		
+		ThreadFactory threadFactory =  new ThreadFactory() {
+			
+			@Override
+			public Thread newThread(Runnable r) {
+				Thread th=new Thread(r, "Background processor");
+				th.setPriority(Thread.NORM_PRIORITY);
+				return th;
+			}
+		};
+		backgroundProcessor = Executors.newSingleThreadExecutor(threadFactory);
+		
+		SwingUtilities.invokeLater(new Runnable() {
+	
+			public void run() {
+				createCaptureFrame();
+			}
+		});
+		
+		try {
+			// Proxy configuration - requires java.net.NetPermission getProxySelector
+			proxySelector = ProxySelector.getDefault();
+		} catch (Exception e) {
+			System.err.println("Can't obtain proxy information: "+e);
+			e.printStackTrace();				
+		}
+	}
+
+	public ExecutorService getBackgroundProcessor() {
+		return backgroundProcessor;
+	}
+	
+	protected void createCaptureFrame() {
+		try {
+			captureFrame = new CaptureFrame(this);
+			captureFrame.setVisible(true);
+		} catch (Exception e) {
+			JOptionPane.showMessageDialog(
+					null,
+					"Error: "+e, 
+					"Cannot create capture window",
+					JOptionPane.ERROR_MESSAGE);
+			e.printStackTrace();
+		}
+	}
+	
+	public static String formatByteSize(long bytes) {
+		if (bytes<1024) {
+			return bytes + "bytes";
+		}
+		if (bytes<1024*1024) {
+			return MessageFormat.format("{0,number,0.0} Kb", new Object[] {(double) bytes/1024.0});
+		}
+		if (bytes<1024*1024*1024) {
+			return MessageFormat.format("{0,number,0.00} Mb", new Object[] {(double) bytes/(double) (1024.0*1024.0)});
+		}
+		return MessageFormat.format("{0,number,0.00} Gb", new Object[] {(double) bytes/(double) (1024.0*1024.0*1024.0)});
+	}
+	
+	
+	protected File preferencesFile = new File(System.getProperty("user.home")+File.separator+"."+getClass().getName()+".properties");
+	 
+	public Properties loadConfig() {
+		try {
+			if (preferencesFile.isFile()) {
+				InputStream configStream = new FileInputStream(preferencesFile);				
+				Properties ret = new Properties();
+				ret.load(configStream);
+				configStream.close();
+				return ret;
+			}
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+	
+	public void storeConfig(Properties properties) {
+		try {
+			FileOutputStream out = new FileOutputStream(preferencesFile);
+			properties.store(out, "Config");
+			out.close();
+		} catch (Exception e) {
+			e.printStackTrace();
+		}		
+	}
+					
+	protected String getCookies() throws DecoderException {
+		String cookiesStr = getParameter("cookies");
+		if (cookiesStr==null) {
+			return null;
+		}
+		
+		StringBuilder ret = new StringBuilder();
+		StringTokenizer st = new StringTokenizer(cookiesStr, ";");
+		while (st.hasMoreTokens()) {
+			String tok = st.nextToken();
+			int idx = tok.indexOf("=");
+			ret.append(hex2urlEncoded(tok.substring(0, idx)));
+			ret.append("=");
+			ret.append(hex2urlEncoded(tok.substring(idx+1)));
+			if (st.hasMoreElements()) {
+				ret.append(";");
+			}
+		}
+		
+		return ret.toString();
+	}
+	
+	private String hex2urlEncoded(String hexStr) throws DecoderException {
+		return new String(URLCodec.encodeUrl(null, Hex.decodeHex(hexStr.toCharArray())));
+	}
+				
+	protected ProxySelector proxySelector;
+	
+	/**
+	 * Posts capture/recording to the web site.
+	 * @param parentComponent Parent component for the progress bar.
+	 * @param content Content - file or byte array.
+	 * @param fileName File name.
+	 * @param mimeType Mime type.
+	 * @return
+	 * @throws Exception
+	 */
+	public HttpResponse post(
+			Component parentComponent, 
+			final InputStream content,
+			final long contentLength,
+			String fileName, 
+			String mimeType) throws Exception {
+		
+		
+		System.out.println("jCapture applet, build @@@time@@@");
+		
+		/**
+		 * Debugging - save to file.
+		 */
+		if (getParameter(OUTPUT_DIR_PARAMETER)!=null) {
+			OutputStream out = new FileOutputStream(new File(getParameter(OUTPUT_DIR_PARAMETER)+File.separator+fileName));
+			byte[] buf=new byte[4096];
+			int l;
+			while ((l=content.read(buf))!=-1) {
+				out.write(buf, 0, l);
+			}
+			out.close();
+			content.close();
+			return null;
+		}
+		
+	    ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(parentComponent, "Uploading "+ fileName + " ("+formatByteSize(contentLength)+")", content);	    
+		InputStreamBody bin = new InputStreamBody(pmis, mimeType, bodyName(fileName)) {
+	    	
+	    	@Override
+	    	public long getContentLength() {
+	    		return contentLength;
+	    	}
+	    };		    
+	    
+		DefaultHttpClient httpClient = new DefaultHttpClient();
+	    if (proxySelector!=null) {
+	    	ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
+		        httpClient.getConnectionManager().getSchemeRegistry(),
+		        proxySelector);
+	    	httpClient.setRoutePlanner(routePlanner);
+	    }
+		return httpClient.execute(createRequest(fileName, bin));			
+	}
+
+	protected abstract HttpUriRequest createRequest(String fileName, InputStreamBody bin) throws Exception;
+	
+	protected abstract String bodyName(String fileName);
+}

+ 85 - 0
main/document/jcapture/src/com/hammurapi/jcapture/ButtonManager.java

@@ -0,0 +1,85 @@
+package com.hammurapi.jcapture;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.zip.DataFormatException;
+
+import com.flagstone.transform.DefineTag;
+import com.flagstone.transform.Movie;
+import com.flagstone.transform.MovieTag;
+import com.flagstone.transform.Place2;
+import com.flagstone.transform.datatype.CoordTransform;
+
+
+public class ButtonManager {
+
+	private final Map<String, Place2> buttons;
+	private final List<DefineTag> definitions;
+
+	public ButtonManager() {
+		buttons = new HashMap<String, Place2>();
+		definitions = new ArrayList<DefineTag>();
+	}
+
+	public void loadLibrary(URL libUrl) throws IOException, DataFormatException {
+		Movie movie = new Movie();
+		movie.decodeFromUrl(libUrl);
+		findDefinitions(movie, definitions);
+		findButtons(movie, buttons);
+	}
+
+	public int maxIdentifier() {
+		int identifier = 0;
+		DefineTag object;
+		for (Iterator<DefineTag>iter = definitions.iterator(); iter.hasNext();) {
+			object = iter.next();
+			if (object.getIdentifier() > identifier) {
+				identifier = object.getIdentifier();
+			}
+		}
+		return identifier;
+	}
+
+	public List<DefineTag> getDefinitions() {
+		List<DefineTag> list = new ArrayList<DefineTag>(definitions.size());
+		for (Iterator<DefineTag>iter = definitions.iterator(); iter.hasNext();) {
+			list.add((DefineTag) iter.next().copy());
+		}
+		return list;
+	}
+
+	public Place2 getButton(final String name, final int layer, final int xpos, final int ypos) {
+		Place2 place = (Place2)buttons.get(name).copy();
+		place.setLayer(layer);
+		place.setTransform(new CoordTransform(1, 1, 0, 0, xpos, ypos));
+		return place;
+	}
+
+	private void findDefinitions(final Movie movie, final List<DefineTag> list) {
+		MovieTag object;
+		for (Iterator<MovieTag> iter = movie.getObjects().iterator(); iter.hasNext();) {
+			object = iter.next();
+			if (object instanceof DefineTag) {
+				list.add((DefineTag)object);
+			}
+		}
+	}
+
+	private void findButtons(final Movie movie, final Map<String, Place2> list) {
+		MovieTag object;
+		Place2 place;
+		for (Iterator<MovieTag>iter = movie.getObjects().iterator(); iter.hasNext();) {
+			object = iter.next();
+			if (object instanceof Place2) {
+				place = (Place2)object;
+				if (place.getName() != null) {
+					list.put(place.getName(), place);
+				}
+			}
+		}
+	}
+}

+ 370 - 0
main/document/jcapture/src/com/hammurapi/jcapture/CaptureConfig.java

@@ -0,0 +1,370 @@
+package com.hammurapi.jcapture;
+
+import java.awt.AWTException;
+import java.awt.Component;
+import java.awt.MouseInfo;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Robot;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Properties;
+import java.util.ServiceLoader;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import javax.sound.sampled.AudioFormat;
+
+public class CaptureConfig implements VideoEncoder.Config {
+	
+	private static final String MP3_COMMAND_PROPERTY = "mp3command";
+	private static final String TOOL_BAR_PROPERTY = "toolBar";
+	private static final String SPEED_SCALE_PROPERTY = "speedScale";
+	private static final String SOUND_PROPERTY = "sound";
+	private static final String SCREEN_SCALE_PROPERTY = "screenScale";
+	private static final String REMOVE_INACTIVITY_PROPERTY = "removeInactivity";
+	private static final String PLAY_PROPERTY = "play";
+	private static final String MOUSE_PROPERTY = "mouse";
+	private static final String MIXER_NAME_PROPERTY = "mixerName";
+	private static final String LOOP_PROPERTY = "loop";
+	private static final String INACTIVITY_INTERVAL_PROPERTY = "inactivityInterval";
+	private static final String IMAGE_FORMAT_PROPERTY = "imageFormat";
+	private static final String FRAMES_PER_SECOND_PROPERTY = "framesPerSecond";
+	private static final String BORDER_PROPERTY = "border";
+	private static final String RECORDING_RECTANGLE_PROPERTY = "recordingRectangle";
+	private static final String ENCODER_NAME_PROPERTY = "encoderName";
+	private static final String AUDIO_FORMAT_SAMPLE_SIZE_PROPERTY = "audioFormat.sampleSize";
+	private static final String AUDIO_FORMAT_SAMPLE_RATE_PROPERTY = "audioFormat.sampleRate";
+	private static final String AUDIO_FORMAT_CHANNELS_PROPERTY = "audioFormat.channels";
+	private AudioFormat audioFormat =  new AudioFormat(22050.0F, 16, 1, true, false);;
+	private String mixerName;
+	private float framesPerSecond = 10.0f;
+	private double screenScale = 1.0;
+	private float speedScale = 1.0f;
+	private boolean removeInactivity;
+	private double inactivityInterval = 0.7;
+	private Component parentComponent;
+	private Rectangle recordingRectangle;	
+	private boolean border = true;
+	private boolean toolBar = true;	
+	private	Robot robot;
+	private String imageFormat = "PNG";		
+	private boolean sound = true;
+	private boolean mouse = true;
+	private boolean loop = true;
+	private boolean play = false;
+	private VideoEncoder encoder;
+	private int grabRange = 3;
+	private ExecutorService backgroundProcessor;
+	private String mp3command;
+		
+	public String getMp3command() {
+		return mp3command;
+	}
+
+	public void setMp3command(String mp3command) {
+		this.mp3command = mp3command;
+	}
+
+	public int getGrabRange() {
+		return grabRange;
+	}
+
+	public ExecutorService getBackgroundProcessor() {
+		return backgroundProcessor;
+	}
+
+	public void setBackgroundProcessor(ExecutorService backgroundProcessor) {
+		this.backgroundProcessor = backgroundProcessor;
+	}
+
+	public void setGrabRange(int grabRange) {
+		this.grabRange = grabRange;
+	}
+
+	public VideoEncoder getEncoder() {
+		return encoder;
+	}
+
+	public void setEncoder(VideoEncoder encoder) {
+		this.encoder = encoder;
+	}
+
+	public boolean isLoop() {
+		return loop;
+	}
+
+	public void setLoop(boolean loop) {
+		this.loop = loop;
+	}
+
+	public boolean isPlay() {
+		return play;
+	}
+
+	public void setPlay(boolean play) {
+		this.play = play;
+	}
+
+	public boolean isSound() {
+		return sound;
+	}
+
+	public void setSound(boolean sound) {
+		this.sound = sound;
+	}
+
+	public boolean isMouse() {
+		return mouse;
+	}
+
+	public void setMouse(boolean mouse) {
+		this.mouse = mouse;
+	}
+
+	public String getImageFormat() {
+		return imageFormat;
+	}
+
+	public void setImageFormat(String imageFormat) {
+		this.imageFormat = imageFormat;
+	}
+
+	public CaptureConfig() throws AWTException {
+		robot = new Robot();
+		
+		ServiceLoader<VideoEncoder> sl = ServiceLoader.load(VideoEncoder.class);
+		List<VideoEncoder> accumulator = new ArrayList<VideoEncoder>();
+		Iterator<VideoEncoder> vit = sl.iterator();
+		while (vit.hasNext()) {
+			accumulator.add(vit.next());
+		}
+		
+		Collections.sort(accumulator, new Comparator<VideoEncoder>() {
+
+			@Override
+			public int compare(VideoEncoder o1, VideoEncoder o2) {
+				return o1.toString().compareTo(o2.toString());
+			}
+			
+		});
+		
+		encoders = Collections.unmodifiableList(accumulator);
+		if (encoder==null && !encoders.isEmpty()) {
+			encoder = encoders.get(0);
+		}
+		
+	}
+	
+	/**
+	 * Submits screenshot for processing in a background thread.
+	 * @param task
+	 * @return
+	 */
+	public Future<ScreenShot> submit(ScreenShot task) {
+		return backgroundProcessor.submit(task);
+	}	
+	
+	public Robot getRobot() {
+		return robot;
+	}
+	
+	public ScreenShot createScreenShot(ScreenShot prev, FileChannel imageChannel) throws IOException {		
+		BufferedImage image = robot.createScreenCapture(recordingRectangle);
+   		Point mouseLocation = MouseInfo.getPointerInfo().getLocation();
+   		if (mouse && recordingRectangle.contains(mouseLocation)) {
+   	   		mouseLocation.move(mouseLocation.x-recordingRectangle.x, mouseLocation.y-recordingRectangle.y);   			
+   		} else {
+   			mouseLocation = null;
+   		}
+   		return new ScreenShot(
+   				image, 
+   				mouseLocation, 
+   				prev, 
+   				System.currentTimeMillis(), 
+   				grabRange, 
+   				isTransparencySupported(),
+   				border, 
+   				getScreenScale(),
+   				imageChannel,
+   				getImageFormat());
+	}
+	
+	public boolean isTransparencySupported() {
+		return !"jpeg".equalsIgnoreCase(getImageFormat())
+				&& !"jpg".equalsIgnoreCase(getImageFormat());				
+	}
+	
+	public boolean isToolBar() {
+		return toolBar;
+	}
+	public void setToolBar(boolean toolBar) {
+		this.toolBar = toolBar;
+	}
+	public boolean isBorder() {
+		return border;
+	}
+	public void setBorder(boolean border) {
+		this.border = border;
+	}
+	public Rectangle getRecordingRectangle() {
+		return recordingRectangle;
+	}
+	public Properties setRecordingRectangle(Rectangle recordingRectangle) {
+		Rectangle oldValue = this.recordingRectangle;
+		this.recordingRectangle = recordingRectangle;
+		if (this.recordingRectangle!=null && !this.recordingRectangle.equals(oldValue)) {
+			return store();
+		}
+		return null;
+	}
+	public AudioFormat getAudioFormat() {
+		return audioFormat;
+	}
+	public void setAudioFormat(AudioFormat audioFormat) {
+		this.audioFormat = audioFormat;
+	}
+	public String getMixerName() {
+		return mixerName;
+	}
+	public void setMixerName(String mixerName) {
+		this.mixerName = mixerName;
+	}
+	public float getFramesPerSecond() {
+		return framesPerSecond;
+	}
+	public void setFramesPerSecond(float framesPerSecond) {
+		this.framesPerSecond = framesPerSecond;
+	}
+	public double getScreenScale() {
+		return screenScale;
+	}
+	public void setScreenScale(double screenScale) {
+		this.screenScale = screenScale;
+	}
+	public float getSpeedScale() {
+		return speedScale;
+	}
+	public void setSpeedScale(float speedScale) {
+		this.speedScale = speedScale;
+	}
+	public boolean isRemoveInactivity() {
+		return removeInactivity;
+	}
+	public void setRemoveInactivity(boolean removeInactivity) {
+		this.removeInactivity = removeInactivity;
+	}
+	public double getInactivityInterval() {
+		return inactivityInterval;
+	}
+	public void setInactivityInterval(double inactivityInterval) {
+		this.inactivityInterval = inactivityInterval;
+	}
+	public Component getParentComponent() {
+		return parentComponent;
+	}
+	public void setParentComponent(Component parentComponent) {
+		this.parentComponent = parentComponent;
+	}
+			
+    void load(Properties properties) {    	
+    	if (properties!=null) {
+    		try {
+    			if (properties.containsKey(AUDIO_FORMAT_CHANNELS_PROPERTY)) {
+    				audioFormat = new AudioFormat(
+    						Float.parseFloat(properties.getProperty(AUDIO_FORMAT_SAMPLE_RATE_PROPERTY, String.valueOf(audioFormat.getSampleRate()))),
+    						Integer.parseInt(properties.getProperty(AUDIO_FORMAT_SAMPLE_SIZE_PROPERTY, String.valueOf(audioFormat.getSampleSizeInBits()))),
+    						Integer.parseInt(properties.getProperty(AUDIO_FORMAT_CHANNELS_PROPERTY, String.valueOf(audioFormat.getChannels()))),
+    						true, false);
+    			}
+        		
+    			border=Boolean.parseBoolean(properties.getProperty(BORDER_PROPERTY, String.valueOf(border)));    				
+    			framesPerSecond=Float.parseFloat(properties.getProperty(FRAMES_PER_SECOND_PROPERTY, String.valueOf(framesPerSecond)));    				
+    			imageFormat=properties.getProperty(IMAGE_FORMAT_PROPERTY, String.valueOf(imageFormat));    				
+    			inactivityInterval=Double.parseDouble(properties.getProperty(INACTIVITY_INTERVAL_PROPERTY, String.valueOf(inactivityInterval)));    				
+    			loop=Boolean.parseBoolean(properties.getProperty(LOOP_PROPERTY, String.valueOf(loop)));    				
+    			mixerName=properties.getProperty(MIXER_NAME_PROPERTY, String.valueOf(mixerName));    				
+    			mouse=Boolean.parseBoolean(properties.getProperty(MOUSE_PROPERTY, String.valueOf(mouse)));    				
+    			play=Boolean.parseBoolean(properties.getProperty(PLAY_PROPERTY, String.valueOf(play)));    				
+    			removeInactivity=Boolean.parseBoolean(properties.getProperty(REMOVE_INACTIVITY_PROPERTY, String.valueOf(removeInactivity)));    				
+    			screenScale=Double.parseDouble(properties.getProperty(SCREEN_SCALE_PROPERTY, String.valueOf(screenScale)));    				
+    			sound=Boolean.parseBoolean(properties.getProperty(SOUND_PROPERTY, String.valueOf(sound)));    				
+    			speedScale=Float.parseFloat(properties.getProperty(SPEED_SCALE_PROPERTY, String.valueOf(speedScale)));    				
+    			toolBar=Boolean.parseBoolean(properties.getProperty(TOOL_BAR_PROPERTY, String.valueOf(toolBar)));    
+    			mp3command=properties.getProperty(MP3_COMMAND_PROPERTY);
+    			encoder = null;
+    			String encoderName = properties.getProperty(ENCODER_NAME_PROPERTY);
+    			if (encoderName!=null) {
+    				for (VideoEncoder candidate: getEncoders()) {
+    					if (encoderName.equals(candidate.toString())) {
+    						encoder = candidate;
+    						break;
+    					}
+    				}
+    			}
+    			if (encoder==null && !getEncoders().isEmpty()) {
+    				encoder = getEncoders().get(0);
+    			}
+    			
+    			String rr = properties.getProperty(RECORDING_RECTANGLE_PROPERTY);
+    			if (rr!=null && rr.trim().length()>0) {
+    				String[] dims = rr.split(";");
+    				recordingRectangle = new Rectangle(Integer.parseInt(dims[0]), Integer.parseInt(dims[1]), Integer.parseInt(dims[2]), Integer.parseInt(dims[3]));
+    			}
+    				
+    		} catch (Exception e) {
+    			e.printStackTrace();
+    		}
+    	}
+    }
+    
+    private List<VideoEncoder> encoders;
+    
+    /**
+     * @return array of available encoders.
+     */
+    public List<VideoEncoder> getEncoders() {
+		return encoders;
+	}
+
+	Properties store() {
+    	Properties properties = new Properties();
+		if (audioFormat!=null) {
+			properties.setProperty(AUDIO_FORMAT_CHANNELS_PROPERTY, String.valueOf(audioFormat.getChannels()));
+			properties.setProperty(AUDIO_FORMAT_SAMPLE_RATE_PROPERTY, String.valueOf(audioFormat.getSampleRate()));
+			properties.setProperty(AUDIO_FORMAT_SAMPLE_SIZE_PROPERTY, String.valueOf(audioFormat.getSampleSizeInBits()));    				
+		}
+		properties.setProperty(BORDER_PROPERTY, String.valueOf(border));    				
+		properties.setProperty(FRAMES_PER_SECOND_PROPERTY, String.valueOf(framesPerSecond));    				
+		properties.setProperty(IMAGE_FORMAT_PROPERTY, String.valueOf(imageFormat));    				
+		properties.setProperty(INACTIVITY_INTERVAL_PROPERTY, String.valueOf(inactivityInterval));    				
+		properties.setProperty(LOOP_PROPERTY, String.valueOf(loop));    				
+		properties.setProperty(MIXER_NAME_PROPERTY, String.valueOf(mixerName));    				
+		properties.setProperty(MOUSE_PROPERTY, String.valueOf(mouse));    				
+		properties.setProperty(PLAY_PROPERTY, String.valueOf(play));    				
+		properties.setProperty(REMOVE_INACTIVITY_PROPERTY, String.valueOf(removeInactivity));    				
+		properties.setProperty(SCREEN_SCALE_PROPERTY, String.valueOf(screenScale));    				
+		properties.setProperty(SOUND_PROPERTY, String.valueOf(sound));    				
+		properties.setProperty(SPEED_SCALE_PROPERTY, String.valueOf(speedScale));    				
+		properties.setProperty(TOOL_BAR_PROPERTY, String.valueOf(toolBar));
+		if (recordingRectangle!=null) {
+			properties.setProperty(RECORDING_RECTANGLE_PROPERTY, recordingRectangle.x+";"+recordingRectangle.y+";"+recordingRectangle.width+";"+recordingRectangle.height);
+		}
+		if (mp3command!=null) {
+			properties.setProperty(MP3_COMMAND_PROPERTY, mp3command);
+		}
+		if (encoder!=null) {
+			properties.setProperty(ENCODER_NAME_PROPERTY, encoder.toString());
+		}
+    	
+    	return properties;
+    }
+
+	
+}

+ 405 - 0
main/document/jcapture/src/com/hammurapi/jcapture/CaptureFrame.java

@@ -0,0 +1,405 @@
+package com.hammurapi.jcapture;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.imageio.ImageIO;
+import javax.swing.AbstractAction;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+import javax.swing.SwingWorker;
+import javax.swing.border.LineBorder;
+
+import netscape.javascript.JSObject;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+
+/**
+ * License: LGPL.
+ * @author Pavel Vlasov.
+ *
+ */
+public class CaptureFrame extends javax.swing.JFrame {
+	private JPanel capturePanel;	
+	private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss-SSS");
+	private int counter;
+	private CaptureConfig captureConfig;
+	private AbstractCaptureApplet applet;
+	private JButton recordButton;
+	
+	public CaptureConfig getCaptureConfig() {
+		return captureConfig;
+	}
+
+	public CaptureFrame(final AbstractCaptureApplet applet) throws Exception {				
+		super("Screen capture");
+		setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("camera.png")));
+
+    	setUndecorated(true);
+    	
+    	Translucener.makeFrameTranslucent(this);
+    	
+		setAlwaysOnTop(true);
+		this.applet = applet;
+		captureConfig = new CaptureConfig();
+		captureConfig.load(applet.loadConfig());
+		captureConfig.setBackgroundProcessor(applet.getBackgroundProcessor());
+		
+		//--- GUI construction ---
+		
+		capturePanel = new JPanel();
+
+		final JLabel dimensionsLabel = new JLabel("");
+		capturePanel.add(dimensionsLabel, BorderLayout.CENTER);		
+		
+		capturePanel.addComponentListener(new ComponentAdapter() {
+			
+			@Override
+			public void componentResized(ComponentEvent e) {				
+				super.componentResized(e);
+				dimensionsLabel.setText(e.getComponent().getWidth()+" x "+e.getComponent().getHeight());
+			}
+		});
+		
+		JButton captureButton = new JButton(new AbstractAction() {
+
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				Rectangle bounds = capturePanel.getBounds();
+				Point loc = bounds.getLocation();
+				SwingUtilities.convertPointToScreen(loc, capturePanel);
+				bounds.setLocation(loc);
+				Properties props = captureConfig.setRecordingRectangle(bounds);
+				if (props!=null) {
+					getApplet().storeConfig(props);
+				}
+				capturing.set(true);
+				setVisible(false);
+			}
+			
+		});
+		captureButton.setText("Capture");
+		captureButton.setToolTipText("Create a snapshot of the screen");
+		capturePanel.add(captureButton, BorderLayout.CENTER);		
+		
+		recordButton = new JButton(new AbstractAction() {
+
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				Rectangle bounds = capturePanel.getBounds();
+				Point loc = bounds.getLocation();
+				SwingUtilities.convertPointToScreen(loc, capturePanel);
+				bounds.setLocation(loc);
+				Properties props = captureConfig.setRecordingRectangle(bounds);
+				if (props!=null) {
+					getApplet().storeConfig(props);
+				}
+				recording.set(true);
+				setVisible(false);
+			}
+			
+		});
+		recordButton.setText("Record");
+		setRecordButtonState();
+		capturePanel.add(recordButton, BorderLayout.CENTER);
+		
+		JButton optionsButton = new JButton(new AbstractAction() {
+
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				new CaptureOptionsDialog(CaptureFrame.this).setVisible(true);
+			}
+			
+		});
+		optionsButton.setText("Options");
+		capturePanel.add(optionsButton, BorderLayout.CENTER);
+		
+		JButton cancelButton = new JButton(new AbstractAction() {
+
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				CaptureFrame.this.setVisible(false);
+			}
+			
+		});
+		cancelButton.setText("Cancel");
+		capturePanel.add(cancelButton, BorderLayout.CENTER);
+		
+		getContentPane().add(capturePanel, BorderLayout.CENTER);
+				
+		capturePanel.setBorder(new LineBorder(new java.awt.Color(0,0,0), 1, false));
+		
+		if (captureConfig.getRecordingRectangle()==null) {
+			setSize(400, 300);
+			setLocationRelativeTo(null);
+		} else {
+			setBounds(captureConfig.getRecordingRectangle());
+		}
+		    	
+		Insets dragInsets = new Insets(5, 5, 5, 5);
+    	new ComponentResizer(dragInsets, this);
+    	
+    	ComponentMover cm = new ComponentMover();
+    	cm.registerComponent(this);
+    	cm.setDragInsets(dragInsets);
+    	
+    	addComponentListener(new ComponentListener() {
+			
+			@Override
+			public void componentShown(ComponentEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+			
+			@Override
+			public void componentResized(ComponentEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+			
+			@Override
+			public void componentMoved(ComponentEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+			
+			@Override
+			public void componentHidden(ComponentEvent e) {
+				if (capturing.get()) {
+					capturing.set(false);
+					try {
+						capture();
+					} catch (Exception ex) {
+						ex.printStackTrace();
+					}							
+				} else if (recording.get()) {
+					recording.set(false);
+					record();
+				}
+			}
+		});
+		
+	}
+
+	void setRecordButtonState() {
+		if (captureConfig.getEncoder()==null) {
+			recordButton.setEnabled(false);
+			recordButton.setToolTipText("Video format not selected. Use Options dialog to select video format.");			
+		} else {
+			recordButton.setEnabled(true);
+			recordButton.setToolTipText("Record screen activity and audio");
+		}
+	}
+	
+	public AbstractCaptureApplet getApplet() {
+		return applet;
+	}
+	
+	protected void capture() throws Exception {
+		try {
+			Thread.sleep(200); // For Ubuntu.
+		} catch (InterruptedException ie) {
+			// Ignore
+		}
+		
+		BufferedImage screenShot = captureConfig.createScreenShot(null, null).call().getRegions().get(0).getImage().getImage();
+		
+		String prefix = getDatePrefix();
+		
+		String defaultImageFormat = applet.getParameter("imageFormat");
+		if (defaultImageFormat==null || defaultImageFormat.trim().length()==0) {
+			defaultImageFormat = "PNG";
+		}
+		final String defaultFileExtension=defaultImageFormat.toLowerCase();
+		
+		final String fileName = JOptionPane.showInputDialog(CaptureFrame.this, "Upload as", applet.getParameter("pageName")+"-capture-"+prefix+"-" + nextCounter() +"."+defaultFileExtension);
+		if (fileName!=null) {
+			try {
+				ByteArrayOutputStream baos = new ByteArrayOutputStream();
+				int idx = fileName.lastIndexOf('.');
+				String imageFormat = idx==-1 ? defaultImageFormat : fileName.substring(idx+1).toUpperCase();
+				ImageIO.write(screenShot, imageFormat, baos);
+				final byte[] imageBytes = baos.toByteArray();
+				System.out.println("Image size: "+imageBytes.length);
+				// Uploading
+				SwingWorker<Boolean, Long> task = new SwingWorker<Boolean, Long>() {
+
+					@Override
+					protected Boolean doInBackground() throws Exception {
+						
+						System.out.println("Uploading in background");
+						try {									
+							HttpResponse iResponse = applet.post(
+									CaptureFrame.this, 
+									new ByteArrayInputStream(imageBytes),
+									imageBytes.length,
+									fileName, 
+									"application/octet-stream");
+							
+							System.out.println("Response status line: "+iResponse.getStatusLine());
+							if (iResponse.getStatusLine().getStatusCode()!=HttpStatus.SC_OK) {
+						    	errorMessage = iResponse.getStatusLine();
+						    	errorTitle = "Error saving image";
+						    	return false;
+							}
+							return true;
+						} catch (Error e) {										
+							errorMessage=e.toString();
+							errorTitle = "Upload error";
+							e.printStackTrace();
+							return false;
+						}
+					}
+					
+					private Object errorMessage;
+					private String errorTitle;
+					
+					protected void done() {
+						try {
+							if (get()) {
+								JSObject window = JSObject.getWindow(applet);
+								String toEval = "insertAtCarret('"+applet.getParameter("edid")+"','{{:"+fileName+"|}}')";
+								System.out.println("Evaluating: "+toEval);
+								window.eval(toEval);
+								CaptureFrame.this.setVisible(false);																																			
+							} else {
+						    	JOptionPane.showMessageDialog(
+						    			CaptureFrame.this,
+										errorMessage, 
+										errorTitle,
+										JOptionPane.ERROR_MESSAGE);																			
+							}
+						} catch (Exception e) {
+							e.printStackTrace();
+					    	JOptionPane.showMessageDialog(
+					    			CaptureFrame.this,
+									e.toString(), 
+									"Exception",
+									JOptionPane.ERROR_MESSAGE);																			
+						}
+					};
+					
+				};
+				
+				task.execute();
+			} catch (IOException ex) {
+		    	JOptionPane.showMessageDialog(
+		    			applet,
+						ex.toString(), 
+						"Error saving image",
+						JOptionPane.ERROR_MESSAGE);									
+			}
+		}
+	}
+
+	public int nextCounter() {
+		return counter++;
+	}
+
+	public String getDatePrefix() {
+		return dateFormat.format(new Date());
+	}
+
+	protected void record() {
+		try {
+			Thread.sleep(200); // For Ubuntu.
+		} catch (InterruptedException ie) {
+			// Ignore
+		}
+		
+		int borderWidth = 1;
+		JFrame[] borderFrames = new JFrame[4];
+		
+		Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+		
+		Rectangle rr = captureConfig.getRecordingRectangle();
+		Color borderColor = Color.RED;
+		if (rr.x>=borderWidth) {
+			// West border
+			borderFrames[0] = new JFrame();
+			borderFrames[0].setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+			borderFrames[0].setSize(borderWidth, rr.height+borderWidth*2);
+			borderFrames[0].setLocation(rr.x-borderWidth, rr.y-borderWidth);
+			borderFrames[0].setUndecorated(true);
+			borderFrames[0].setAlwaysOnTop(true);
+			borderFrames[0].setFocusableWindowState(false);			
+			borderFrames[0].getContentPane().setBackground(borderColor);
+		}
+		if (rr.x+rr.width<dim.width-borderWidth) {
+			// East border
+			borderFrames[1] = new JFrame();
+			borderFrames[1].setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+			borderFrames[1].setSize(borderWidth, rr.height+borderWidth*2);
+			borderFrames[1].setLocation(rr.x+rr.width, rr.y-borderWidth);
+			borderFrames[1].setUndecorated(true);
+			borderFrames[1].setAlwaysOnTop(true);
+			borderFrames[1].setFocusableWindowState(false);			
+			borderFrames[1].getContentPane().setBackground(borderColor);
+		}
+		if (rr.y>=borderWidth) {
+			// North border
+			borderFrames[2] = new JFrame();
+			borderFrames[2].setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+			borderFrames[2].setSize(rr.width, borderWidth);
+			borderFrames[2].setLocation(rr.x, rr.y-borderWidth);
+			borderFrames[2].setUndecorated(true);
+			borderFrames[2].setAlwaysOnTop(true);
+			borderFrames[2].setFocusableWindowState(false);			
+			borderFrames[2].getContentPane().setBackground(borderColor);
+		}
+		if (rr.y+rr.height<dim.height-borderWidth) {
+			// South border
+			borderFrames[3] = new JFrame();
+			borderFrames[3].setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+			borderFrames[3].setSize(rr.width, borderWidth);
+			borderFrames[3].setLocation(rr.x, rr.y+rr.height);
+			borderFrames[3].setUndecorated(true);
+			borderFrames[3].setAlwaysOnTop(true);
+			borderFrames[3].setFocusableWindowState(false);			
+			borderFrames[3].getContentPane().setBackground(borderColor);
+		}
+		
+		RecordingControlsFrame inst = new RecordingControlsFrame(this, borderFrames);		
+		int x = getLocation().x + getWidth() - inst.getWidth();
+		if (x+inst.getWidth()>dim.getWidth()) {
+			x = dim.width-inst.getWidth();
+		} else if (x<0) {
+			x = 0;
+		}
+		
+		int y = rr.getLocation().y+getHeight()+1;
+		if (y+inst.getHeight()>dim.height) {
+			y = rr.getLocation().y-inst.getHeight();
+			if (y<0) {
+				y=dim.height-inst.getHeight();
+			}
+		}
+		inst.setLocation(x, y);
+		inst.setVisible(true);
+	}
+
+	private AtomicBoolean capturing = new AtomicBoolean(false);
+	private AtomicBoolean recording = new AtomicBoolean(false);
+
+}

+ 606 - 0
main/document/jcapture/src/com/hammurapi/jcapture/CaptureOptionsDialog.java

@@ -0,0 +1,606 @@
+package com.hammurapi.jcapture;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.TargetDataLine;
+import javax.swing.BorderFactory;
+import javax.swing.ButtonGroup;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+
+public class CaptureOptionsDialog extends javax.swing.JDialog {
+	private JRadioButton sampleSize16Button;
+	private JTextField timeLineScaleTextField;
+	private JCheckBox toobarCheckBox;
+	private JCheckBox videoBorderCheckBox;
+	private JTextField screenScaleTextField;
+	private JLabel timelineScalingLabel;
+	private JLabel screenScalingLabel;
+	private JTextField fpsTextField;
+	private JLabel fpsLabel;
+	private ButtonGroup sampleSizeButtonGroup;
+	private JComboBox<String> sampleRateComboBox;
+	private JRadioButton sampleSize8Button;
+	private JTextField inactivityIntervalTextField;
+	private JLabel inactivityIntervalLabel;
+	private JCheckBox inactivityCheckBox;
+	private JPanel inactivityPanel;
+	private JPanel scalingPanel;
+	private JCheckBox stereoCheckBox;
+	private JLabel sampleSizeLabel;
+	private JTabbedPane recordingSettingsPane;
+	private JCheckBox recordSoundCheckBox;
+	private JLabel sampleRateLabel;
+	private JComboBox<String> soundLineComboBox;
+	private JLabel soundSourceLabel;
+	private JPanel audioSettingsPanel;
+	private JPanel videoSettingsPanel;
+	private JButton cancelButton;
+	private JButton okButton;
+	private JPanel recordPanel;
+	private JComboBox<VideoEncoder> encodersComboBox;
+	private JTextField mp3Text;
+
+	public CaptureOptionsDialog(final CaptureFrame owner) {
+		super(owner);
+		setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+		BorderLayout thisLayout = new BorderLayout();
+		this.setLayout(thisLayout);
+		this.setPreferredSize(new java.awt.Dimension(333, 186));
+
+		recordPanel = new JPanel();
+		this.add(recordPanel);
+		GridBagLayout recordPanelLayout = new GridBagLayout();
+		recordPanelLayout.rowWeights = new double[] { 0.1, 0.0, 0.0, 0.0 };
+		recordPanelLayout.rowHeights = new int[] { 7, 7, 20, 7 };
+		recordPanelLayout.columnWeights = new double[] { 0.1, 0.0, 0.0, 0.0, 0.0 };
+		recordPanelLayout.columnWidths = new int[] { 20, 7, 7, 7, 7 };
+		recordPanel.setLayout(recordPanelLayout);
+		recordPanel.setPreferredSize(new java.awt.Dimension(335, 297));
+
+		okButton = new JButton();
+		recordPanel.add(okButton, new GridBagConstraints(1, 2, 1, 1, 0.0,
+				0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		okButton.setText("OK");
+		okButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				try {
+					if (recordSoundCheckBox.isSelected()) {
+						owner.getCaptureConfig().setAudioFormat(audioFormat);
+						owner.getCaptureConfig().setMixerName((String) soundLineComboBox.getSelectedItem());
+					} else {
+						owner.getCaptureConfig().setRemoveInactivity(inactivityCheckBox.isSelected());
+						if (owner.getCaptureConfig().isRemoveInactivity()) {
+							owner.getCaptureConfig().setInactivityInterval(Double.parseDouble(inactivityIntervalTextField.getText()));
+						}
+					}
+
+					owner.getCaptureConfig().setLoop(getLoopCheckBox().isSelected());
+					owner.getCaptureConfig().setPlay(getPlayCheckBox().isSelected());
+					owner.getCaptureConfig().setMouse(getMouseCheckBox().isSelected());
+					owner.getCaptureConfig().setSound(recordSoundCheckBox.isSelected());
+					owner.getCaptureConfig().setImageFormat(getImageFormatTextField().getText().trim());
+					owner.getCaptureConfig().setBorder(videoBorderCheckBox.isSelected());
+					owner.getCaptureConfig().setFramesPerSecond(Float.parseFloat(fpsTextField.getText()));
+					owner.getCaptureConfig().setScreenScale(Double.parseDouble(screenScaleTextField.getText()) / 100.0);
+					owner.getCaptureConfig().setSpeedScale((float) (Float.parseFloat(timeLineScaleTextField.getText()) / 100.0));
+					owner.getCaptureConfig().setToolBar(toobarCheckBox.isSelected());
+					owner.getApplet().storeConfig(owner.getCaptureConfig().store());
+					owner.getCaptureConfig().setMp3command(mp3Text.getText());
+					owner.getCaptureConfig().setEncoder((VideoEncoder) encodersComboBox.getSelectedItem());
+					owner.setRecordButtonState();
+					CaptureOptionsDialog.this.setVisible(false);
+				} catch (Exception e) {
+					e.printStackTrace();
+					JOptionPane.showMessageDialog(CaptureOptionsDialog.this,
+							e.toString(), "Error in configuration parameters",
+							JOptionPane.ERROR_MESSAGE);
+				}
+			}
+		});
+
+		cancelButton = new JButton();
+		recordPanel.add(cancelButton, new GridBagConstraints(3, 2, 1, 1, 0.0,
+				0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		cancelButton.setText("Cancel");
+		cancelButton.addActionListener(new ActionListener() {
+			
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				CaptureOptionsDialog.this.setVisible(false);
+			}
+		});
+
+		recordingSettingsPane = new JTabbedPane();
+		recordPanel.add(recordingSettingsPane, new GridBagConstraints(0, 0, 5,
+				1, 0.0, 0.0, GridBagConstraints.CENTER,
+				GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
+
+		videoSettingsPanel = new JPanel();
+		GridBagLayout videoSettingsPanelLayout = new GridBagLayout();
+		recordingSettingsPane.addTab("Video", null, videoSettingsPanel, null);
+		videoSettingsPanel.setPreferredSize(new java.awt.Dimension(112, 207));
+		videoSettingsPanelLayout.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1 };
+		videoSettingsPanelLayout.rowHeights = new int[] { 7, 7, 7, 7, 7, 7, 7, 20 };
+		videoSettingsPanelLayout.columnWeights = new double[] { 0.0, 0.0, 0.0,
+				0.0, 0.0, 0.0, 0.0, 0.0, 0.1 };
+		videoSettingsPanelLayout.columnWidths = new int[] { 115, 7, 40, 7, 20,
+				7, 20, 7, 20 };
+		videoSettingsPanel.setLayout(videoSettingsPanelLayout);
+
+		scalingPanel = new JPanel();
+		GridBagLayout scalingPanelLayout = new GridBagLayout();
+		videoSettingsPanel.add(scalingPanel, new GridBagConstraints(0, 6, 1, 1,
+				0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+				new Insets(0, 0, 0, 0), 0, 0));
+		scalingPanel.setBorder(BorderFactory.createTitledBorder("Scaling (%)"));
+		scalingPanelLayout.rowWeights = new double[] { 0.1, 0.0, 0.1 };
+		scalingPanelLayout.rowHeights = new int[] { 7, 7, 7 };
+		scalingPanelLayout.columnWeights = new double[] { 0.0, 0.0, 0.1 };
+		scalingPanelLayout.columnWidths = new int[] { 7, 7, 7 };
+		scalingPanel.setLayout(scalingPanelLayout);
+		scalingPanel.add(getScreenScalingLabel(), new GridBagConstraints(0, 0,
+				1, 1, 0.0, 0.0, GridBagConstraints.EAST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		scalingPanel.add(getTimelineScalingLabel(), new GridBagConstraints(0,
+				2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		scalingPanel.add(getScreenScaleTextField(), new GridBagConstraints(2,
+				0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
+				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+		scalingPanel.add(getTimeLineScaleTextField(), new GridBagConstraints(2,
+				2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
+				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+
+		inactivityPanel = new JPanel();
+		GridBagLayout inactivityPanelLayout = new GridBagLayout();
+		videoSettingsPanel.add(inactivityPanel, new GridBagConstraints(2, 6, 8,
+				1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.VERTICAL, new Insets(0, 0, 0, 0), 0, 0));
+		videoSettingsPanel.add(getFpsLabel(), new GridBagConstraints(0, 0, 1,
+				1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		videoSettingsPanel.add(getFpsTextField(), new GridBagConstraints(2, 0,
+				1, 1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+		
+		videoSettingsPanel.add(getImageFormatLabel(), new GridBagConstraints(0, 2, 1,
+				1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		videoSettingsPanel.add(getImageFormatTextField(), new GridBagConstraints(2, 2,
+				1, 1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+		
+		videoSettingsPanel.add(getEncoderLabel(), new GridBagConstraints(0, 4, 1,
+				1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		videoSettingsPanel.add(getEncoderComboBox(), new GridBagConstraints(2, 4,
+				6, 1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+		
+		videoSettingsPanel.add(getVideoBorderCheckBox(),
+				new GridBagConstraints(4, 0, 1, 1, 0.0, 0.0,
+						GridBagConstraints.WEST, GridBagConstraints.NONE,
+						new Insets(0, 0, 0, 0), 0, 0));
+		videoSettingsPanel.add(getLoopCheckBox(),
+				new GridBagConstraints(6, 2, 1, 1, 0.0, 0.0,
+						GridBagConstraints.WEST, GridBagConstraints.NONE,
+						new Insets(0, 0, 0, 0), 0, 0));
+		videoSettingsPanel.add(getPlayCheckBox(),
+				new GridBagConstraints(8, 2, 1, 1, 0.0, 0.0,
+						GridBagConstraints.WEST, GridBagConstraints.NONE,
+						new Insets(0, 0, 0, 0), 0, 0));
+		videoSettingsPanel.add(getMouseCheckBox(),
+				new GridBagConstraints(4, 2, 1, 1, 0.0, 0.0,
+						GridBagConstraints.WEST, GridBagConstraints.NONE,
+						new Insets(0, 0, 0, 0), 0, 0));
+		videoSettingsPanel.add(getJToobarCheckBox(), new GridBagConstraints(6,
+				0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		inactivityPanel.setBorder(BorderFactory
+				.createTitledBorder("Inactivity processing"));
+		inactivityPanel
+				.setToolTipText("Inactivity handling, enabled if audio is not being recorded.");
+		inactivityPanelLayout.rowWeights = new double[] { 0.0, 0.0, 0.0 };
+		inactivityPanelLayout.rowHeights = new int[] { 7, 7, 7 };
+		inactivityPanelLayout.columnWeights = new double[] { 0.0, 0.0, 0.0,
+				0.0, 0.1 };
+		inactivityPanelLayout.columnWidths = new int[] { 7, 7, 7, 47, 7 };
+		inactivityPanel.setLayout(inactivityPanelLayout);
+		inactivityPanel.setEnabled(false);
+
+		inactivityCheckBox = new JCheckBox();
+		inactivityPanel.add(inactivityCheckBox, new GridBagConstraints(1, 0, 4,
+				1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+		inactivityCheckBox.setText("Remove inactivity");
+		inactivityCheckBox.setEnabled(false);
+
+		inactivityIntervalLabel = new JLabel();
+		inactivityPanel.add(inactivityIntervalLabel, new GridBagConstraints(1,
+				2, 1, 1, 0.0, 0.0, GridBagConstraints.EAST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		inactivityIntervalLabel.setText("Inactivity interval (sec)");
+		inactivityIntervalLabel.setEnabled(false);
+
+		inactivityIntervalTextField = new JTextField();
+		inactivityPanel.add(inactivityIntervalTextField,
+				new GridBagConstraints(3, 2, 1, 1, 0.0, 0.0,
+						GridBagConstraints.CENTER,
+						GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0),
+						0, 0));
+		inactivityIntervalTextField.setText("0.7");
+		inactivityIntervalTextField.setEnabled(false);
+
+		audioSettingsPanel = new JPanel();
+		GridBagLayout audioSettingsPanelLayout = new GridBagLayout();
+		recordingSettingsPane.addTab("Audio", null, audioSettingsPanel, null);
+		audioSettingsPanelLayout.rowWeights = new double[] { 0.0, 0.0, 0.0,
+				0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1 };
+		audioSettingsPanelLayout.rowHeights = new int[] { 7, 7, 7, 7, 7, 7, 7, 7, 7,
+				20 };
+		audioSettingsPanelLayout.columnWeights = new double[] { 0.0, 0.0, 0.0,
+				0.0, 0.0, 0.0, 0.1 };
+		audioSettingsPanelLayout.columnWidths = new int[] { 7, 7, 49, 7, 135,
+				7, 20 };
+		audioSettingsPanel.setLayout(audioSettingsPanelLayout);
+
+		sampleSize16Button = new JRadioButton();
+		audioSettingsPanel.add(sampleSize16Button, new GridBagConstraints(4, 4,
+				1, 1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		sampleSize16Button.setText("16");
+		sampleSize16Button.setSelected(true);
+		getSampleSizeButtonGroup().add(sampleSize16Button);
+		sampleSize16Button.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				selectSoundSource();
+			}
+		});
+
+		recordSoundCheckBox = new JCheckBox();
+		audioSettingsPanel.add(recordSoundCheckBox, new GridBagConstraints(0,
+				0, 4, 1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		recordSoundCheckBox.setText("Record sound");
+		recordSoundCheckBox.setSelected(true);
+		recordSoundCheckBox.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				onSetSound();
+			}
+		});
+
+		soundSourceLabel = new JLabel();
+		audioSettingsPanel.add(soundSourceLabel, new GridBagConstraints(0, 6,
+				1, 1, 0.0, 0.0, GridBagConstraints.EAST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		soundSourceLabel.setText("Source");
+
+		soundLineComboBox = new JComboBox();
+		audioSettingsPanel.add(soundLineComboBox, new GridBagConstraints(2, 6,
+				3, 1, 0.0, 0.0, GridBagConstraints.CENTER,
+				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+
+		audioSettingsPanel.add(new JLabel("WAV2MP3 command"), new GridBagConstraints(0, 8,
+				1, 1, 0.0, 0.0, GridBagConstraints.EAST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+
+		mp3Text = new JTextField();
+		audioSettingsPanel.add(mp3Text, new GridBagConstraints(2, 8,
+				5, 1, 0.0, 0.0, GridBagConstraints.CENTER,
+				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
+
+		sampleRateLabel = new JLabel();
+		audioSettingsPanel.add(sampleRateLabel, new GridBagConstraints(0, 2, 1,
+				1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		sampleRateLabel.setText("Sample rate (khz)");
+
+		sampleSizeLabel = new JLabel();
+		audioSettingsPanel.add(sampleSizeLabel, new GridBagConstraints(0, 4, 1,
+				1, 0.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		sampleSizeLabel.setText("Sample size (bits)");
+
+		stereoCheckBox = new JCheckBox();
+		audioSettingsPanel.add(stereoCheckBox, new GridBagConstraints(4, 2, 1,
+				1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		stereoCheckBox.setText("Stereo");
+		stereoCheckBox.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				selectSoundSource();
+			}
+		});
+
+		sampleSize8Button = new JRadioButton();
+		audioSettingsPanel.add(sampleSize8Button, new GridBagConstraints(2, 4,
+				1, 1, 0.0, 0.0, GridBagConstraints.WEST,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		sampleSize8Button.setText("8");
+		getSampleSizeButtonGroup().add(sampleSize8Button);
+		sampleSize8Button.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				selectSoundSource();
+			}
+		});
+
+		ComboBoxModel<String> sampleRateComboBoxModel = new DefaultComboBoxModel<String>(
+				new String[] { "5.5", "11", "22", "44" });
+		sampleRateComboBox = new JComboBox<String>();
+		audioSettingsPanel.add(sampleRateComboBox, new GridBagConstraints(2, 2,
+				1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		sampleRateComboBox.setModel(sampleRateComboBoxModel);
+		sampleRateComboBox.setSelectedIndex(2);
+		sampleRateComboBox.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				selectSoundSource();
+			}
+		});
+		
+		getImageFormatTextField().setText(owner.getCaptureConfig().getImageFormat());
+		
+		recordSoundCheckBox.setSelected(owner.getCaptureConfig().isSound());
+		stereoCheckBox.setSelected(owner.getCaptureConfig().getAudioFormat().getChannels()>1);
+		if (owner.getCaptureConfig().getAudioFormat().getSampleSizeInBits()==8) {
+			sampleSize8Button.setSelected(true);
+		} else {
+			sampleSize16Button.setSelected(true);
+		}
+		
+		float sampleRate = owner.getCaptureConfig().getAudioFormat().getSampleRate();
+		float proximity = Math.abs(sampleRate-sampleRates[0]);
+		sampleRateComboBox.setSelectedIndex(0);
+		for (int i=1; i<sampleRates.length; ++i) {
+			float prx = Math.abs(sampleRate-sampleRates[i]);
+			if (prx<proximity) {
+				sampleRateComboBox.setSelectedIndex(i);
+				proximity = prx;
+			}				
+		}			
+		inactivityCheckBox.setSelected(owner.getCaptureConfig().isRemoveInactivity());
+		if (owner.getCaptureConfig().isRemoveInactivity()) {
+			inactivityIntervalTextField.setText(String.valueOf(owner.getCaptureConfig().getInactivityInterval()));
+		}
+		onSetSound();
+		
+		videoBorderCheckBox.setSelected(owner.getCaptureConfig().isBorder());
+		getMouseCheckBox().setSelected(owner.getCaptureConfig().isMouse());
+		getLoopCheckBox().setSelected(owner.getCaptureConfig().isLoop());
+		getPlayCheckBox().setSelected(owner.getCaptureConfig().isPlay());
+		fpsTextField.setText(Float.toString(owner.getCaptureConfig().getFramesPerSecond()));
+		screenScaleTextField.setText(Long.toString(Math.round(owner.getCaptureConfig().getScreenScale()*100.0)));
+		timeLineScaleTextField.setText(Long.toString(Math.round(owner.getCaptureConfig().getSpeedScale()*100.0)));
+		toobarCheckBox.setSelected(owner.getCaptureConfig().isToolBar());
+		mp3Text.setText(owner.getCaptureConfig().getMp3command());
+
+		selectSoundSource();
+		soundLineComboBox.setSelectedItem(owner.getCaptureConfig().getMixerName());
+				
+		timeLineScaleTextField.setEnabled(!recordSoundCheckBox.isSelected());
+		
+		getEncoderComboBox().setSelectedItem(owner.getCaptureConfig().getEncoder());
+		
+		JPanel aboutPanel = new JPanel();
+		aboutPanel.setLayout(new BorderLayout());
+		recordingSettingsPane.addTab("About", aboutPanel);
+		aboutPanel.add(new JLabel("jCapture", SwingConstants.CENTER), BorderLayout.NORTH);
+		aboutPanel.add(new JLabel("by Hammurapi Group (http://www.hammurapi.com)", SwingConstants.CENTER), BorderLayout.CENTER);
+		aboutPanel.add(new JLabel("Memory (available/max): "+AbstractCaptureApplet.formatByteSize(Runtime.getRuntime().freeMemory())+"/"+AbstractCaptureApplet.formatByteSize(Runtime.getRuntime().maxMemory()), SwingConstants.CENTER), BorderLayout.SOUTH);		
+		
+		setSize(400, 300);
+		setLocationRelativeTo(owner);
+	}
+
+	private AudioFormat audioFormat;
+	private float[] sampleRates = { 5512.0F, 11025.0F, 22050.0F, 44100.0F };
+	private DefaultComboBoxModel<String> soundLineComboBoxModel;
+	private JLabel imageFormatLabel;
+	private JTextField imageFormatTextField;
+	private JCheckBox mouseCheckBox;
+	private JCheckBox playCheckBox;
+	private JCheckBox loopCheckBox;
+	private JLabel encoderLabel;
+
+	private void selectSoundSource() {
+		audioFormat = new AudioFormat(
+				sampleRates[sampleRateComboBox.getSelectedIndex()],
+				sampleSize8Button.isSelected() ? 8 : 16,
+				stereoCheckBox.isSelected() ? 2 : 1, true, false);
+
+		String sourceName = (String) soundLineComboBox.getSelectedItem();
+
+		if (soundLineComboBoxModel == null) {
+			soundLineComboBoxModel = new DefaultComboBoxModel<String>();
+			soundLineComboBox.setModel(soundLineComboBoxModel);
+		} else {
+			soundLineComboBoxModel.removeAllElements();
+		}
+
+		DataLine.Info info = new DataLine.Info(TargetDataLine.class, audioFormat);
+
+		boolean hasSourceName = false;
+		for (Mixer.Info mi : AudioSystem.getMixerInfo()) {
+			Mixer mx = AudioSystem.getMixer(mi);
+			if (mx.isLineSupported(info)) {
+				soundLineComboBoxModel.addElement(mi.getName());
+				if (sourceName!=null && mi.getName().equals(sourceName)) {
+					hasSourceName = true;
+				}
+			}
+		}
+
+		if (hasSourceName) {
+			soundLineComboBoxModel.setSelectedItem(sourceName);
+		}
+	}
+
+	private ButtonGroup getSampleSizeButtonGroup() {
+		if (sampleSizeButtonGroup == null) {
+			sampleSizeButtonGroup = new ButtonGroup();
+		}
+		return sampleSizeButtonGroup;
+	}
+
+	private JLabel getFpsLabel() {
+		if (fpsLabel == null) {
+			fpsLabel = new JLabel();
+			fpsLabel.setText("Frames Per Second");
+		}
+		return fpsLabel;
+	}
+
+	private JTextField getFpsTextField() {
+		if (fpsTextField == null) {
+			fpsTextField = new JTextField();
+			fpsTextField.setText("10");
+			fpsTextField.setSize(30, 23);
+		}
+		return fpsTextField;
+	}
+
+	private JLabel getImageFormatLabel() {
+		if (imageFormatLabel == null) {
+			imageFormatLabel = new JLabel();
+			imageFormatLabel.setText("Image format");
+		}
+		return imageFormatLabel;
+	}
+
+	private JTextField getImageFormatTextField() {
+		if (imageFormatTextField == null) {
+			imageFormatTextField = new JTextField();
+			imageFormatTextField.setText("png");
+			imageFormatTextField.setSize(30, 23);
+		}
+		return imageFormatTextField;
+	}
+
+	private JLabel getEncoderLabel() {
+		if (encoderLabel == null) {
+			encoderLabel = new JLabel();
+			encoderLabel.setText("Video format");
+		}
+		return encoderLabel;
+	}
+
+	private JComboBox<VideoEncoder> getEncoderComboBox() {
+		if (encodersComboBox == null) {
+			List<VideoEncoder> el = ((CaptureFrame) getOwner()).getCaptureConfig().getEncoders();
+			encodersComboBox = new JComboBox<VideoEncoder>(el.toArray(new VideoEncoder[el.size()]));
+//			encodersComboBox.setSize(30, 23);
+		}
+		return encodersComboBox;
+	}
+
+	private JLabel getScreenScalingLabel() {
+		if (screenScalingLabel == null) {
+			screenScalingLabel = new JLabel();
+			screenScalingLabel.setText("Graphics");
+		}
+		return screenScalingLabel;
+	}
+
+	private JLabel getTimelineScalingLabel() {
+		if (timelineScalingLabel == null) {
+			timelineScalingLabel = new JLabel();
+			timelineScalingLabel.setText("Speed");
+		}
+		return timelineScalingLabel;
+	}
+
+	private JTextField getScreenScaleTextField() {
+		if (screenScaleTextField == null) {
+			screenScaleTextField = new JTextField();
+			screenScaleTextField.setText("100");
+		}
+		return screenScaleTextField;
+	}
+
+	private JTextField getTimeLineScaleTextField() {
+		if (timeLineScaleTextField == null) {
+			timeLineScaleTextField = new JTextField();
+			timeLineScaleTextField.setText("100");
+		}
+		return timeLineScaleTextField;
+	}
+
+	private JCheckBox getVideoBorderCheckBox() {
+		if (videoBorderCheckBox == null) {
+			videoBorderCheckBox = new JCheckBox();
+			videoBorderCheckBox.setText("Border");
+		}
+		return videoBorderCheckBox;
+	}
+
+	private JCheckBox getMouseCheckBox() {
+		if (mouseCheckBox == null) {
+			mouseCheckBox = new JCheckBox();
+			mouseCheckBox.setText("Mouse");
+		}
+		return mouseCheckBox;
+	}
+
+	private JCheckBox getLoopCheckBox() {
+		if (loopCheckBox == null) {
+			loopCheckBox = new JCheckBox();
+			loopCheckBox.setText("Loop");
+		}
+		return loopCheckBox;
+	}
+
+	private JCheckBox getPlayCheckBox() {
+		if (playCheckBox == null) {
+			playCheckBox = new JCheckBox();
+			playCheckBox.setText("Play");
+		}
+		return playCheckBox;
+	}
+
+	private JCheckBox getJToobarCheckBox() {
+		if (toobarCheckBox == null) {
+			toobarCheckBox = new JCheckBox();
+			toobarCheckBox.setText("Toolbar");
+			toobarCheckBox.setSelected(true);
+		}
+		return toobarCheckBox;
+	}
+
+	void onSetSound() {
+		for (Component child : recordSoundCheckBox.getParent().getComponents()) {
+			if (child != recordSoundCheckBox) {
+				child.setEnabled(recordSoundCheckBox.isSelected());
+			}
+		}
+		inactivityPanel.setEnabled(!recordSoundCheckBox.isSelected());
+		timeLineScaleTextField.setEnabled(!recordSoundCheckBox.isSelected());
+		for (Component child : inactivityPanel.getComponents()) {
+			child.setEnabled(!recordSoundCheckBox.isSelected());
+		}
+	}
+
+}

+ 361 - 0
main/document/jcapture/src/com/hammurapi/jcapture/ComponentMover.java

@@ -0,0 +1,361 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.GraphicsEnvironment;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Window;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+
+/**
+ * This class allows you to move a Component by using a mouse. The Component
+ * moved can be a high level Window (ie. Window, Frame, Dialog) in which case
+ * the Window is moved within the desktop. Or the Component can belong to a
+ * Container in which case the Component is moved within the Container.
+ * 
+ * When moving a Window, the listener can be added to a child Component of the
+ * Window. In this case attempting to move the child will result in the Window
+ * moving. For example, you might create a custom "Title Bar" for an undecorated
+ * Window and moving of the Window is accomplished by moving the title bar only.
+ * Multiple components can be registered as "window movers".
+ * 
+ * Components can be registered when the class is created. Additional components
+ * can be added at any time using the registerComponent() method.
+ * 
+ * Taken from http://tips4java.wordpress.com/2009/06/14/moving-windows/
+ */
+public class ComponentMover extends MouseAdapter {
+	private Insets dragInsets = new Insets(0, 0, 0, 0);
+	private Dimension snapSize = new Dimension(1, 1);
+	private Insets edgeInsets = new Insets(0, 0, 0, 0);
+	private boolean changeCursor = true;
+	private boolean autoLayout = false;
+
+	private Class destinationClass;
+	private Component destinationComponent;
+	private Component destination;
+	private Component source;
+
+	private Point pressed;
+	private Point location;
+
+	private Cursor originalCursor;
+	private boolean autoscrolls;
+	private boolean potentialDrag;
+
+	/**
+	 * Constructor for moving individual components. The components must be
+	 * regisetered using the registerComponent() method.
+	 */
+	public ComponentMover() {
+	}
+
+	/**
+	 * Constructor to specify a Class of Component that will be moved when drag
+	 * events are generated on a registered child component. The events will be
+	 * passed to the first ancestor of this specified class.
+	 * 
+	 * @param destinationClass
+	 *            the Class of the ancestor component
+	 * @param component
+	 *            the Components to be registered for forwarding drag events to
+	 *            the ancestor Component.
+	 */
+	public ComponentMover(Class destinationClass, Component... components) {
+		this.destinationClass = destinationClass;
+		registerComponent(components);
+	}
+
+	/**
+	 * Constructor to specify a parent component that will be moved when drag
+	 * events are generated on a registered child component.
+	 * 
+	 * @param destinationComponent
+	 *            the component drage events should be forwareded to
+	 * @param components
+	 *            the Components to be registered for forwarding drag events to
+	 *            the parent component to be moved
+	 */
+	public ComponentMover(Component destinationComponent,
+			Component... components) {
+		this.destinationComponent = destinationComponent;
+		registerComponent(components);
+	}
+
+	/**
+	 * Get the auto layout property
+	 * 
+	 * @return the auto layout property
+	 */
+	public boolean isAutoLayout() {
+		return autoLayout;
+	}
+
+	/**
+	 * Set the auto layout property
+	 * 
+	 * @param autoLayout
+	 *            when true layout will be invoked on the parent container
+	 */
+	public void setAutoLayout(boolean autoLayout) {
+		this.autoLayout = autoLayout;
+	}
+
+	/**
+	 * Get the change cursor property
+	 * 
+	 * @return the change cursor property
+	 */
+	public boolean isChangeCursor() {
+		return changeCursor;
+	}
+
+	/**
+	 * Set the change cursor property
+	 * 
+	 * @param changeCursor
+	 *            when true the cursor will be changed to the Cursor.MOVE_CURSOR
+	 *            while the mouse is pressed
+	 */
+	public void setChangeCursor(boolean changeCursor) {
+		this.changeCursor = changeCursor;
+	}
+
+	/**
+	 * Get the drag insets
+	 * 
+	 * @return the drag insets
+	 */
+	public Insets getDragInsets() {
+		return dragInsets;
+	}
+
+	/**
+	 * Set the drag insets. The insets specify an area where mouseDragged events
+	 * should be ignored and therefore the component will not be moved. This
+	 * will prevent these events from being confused with a MouseMotionListener
+	 * that supports component resizing.
+	 * 
+	 * @param dragInsets
+	 */
+	public void setDragInsets(Insets dragInsets) {
+		this.dragInsets = dragInsets;
+	}
+
+	/**
+	 * Get the bounds insets
+	 * 
+	 * @return the bounds insets
+	 */
+	public Insets getEdgeInsets() {
+		return edgeInsets;
+	}
+
+	/**
+	 * Set the edge insets. The insets specify how close to each edge of the
+	 * parent component that the child component can be moved. Positive values
+	 * means the component must be contained within the parent. Negative values
+	 * means the component can be moved outside the parent.
+	 * 
+	 * @param edgeInsets
+	 */
+	public void setEdgeInsets(Insets edgeInsets) {
+		this.edgeInsets = edgeInsets;
+	}
+
+	/**
+	 * Remove listeners from the specified component
+	 * 
+	 * @param component
+	 *            the component the listeners are removed from
+	 */
+	public void deregisterComponent(Component... components) {
+		for (Component component : components)
+			component.removeMouseListener(this);
+	}
+
+	/**
+	 * Add the required listeners to the specified component
+	 * 
+	 * @param component
+	 *            the component the listeners are added to
+	 */
+	public void registerComponent(Component... components) {
+		for (Component component : components)
+			component.addMouseListener(this);
+	}
+
+	/**
+	 * Get the snap size
+	 * 
+	 * @return the snap size
+	 */
+	public Dimension getSnapSize() {
+		return snapSize;
+	}
+
+	/**
+	 * Set the snap size. Forces the component to be snapped to the closest grid
+	 * position. Snapping will occur when the mouse is dragged half way.
+	 */
+	public void setSnapSize(Dimension snapSize) {
+		if (snapSize.width < 1 || snapSize.height < 1)
+			throw new IllegalArgumentException(
+					"Snap sizes must be greater than 0");
+
+		this.snapSize = snapSize;
+	}
+
+	/**
+	 * Setup the variables used to control the moving of the component:
+	 * 
+	 * source - the source component of the mouse event destination - the
+	 * component that will ultimately be moved pressed - the Point where the
+	 * mouse was pressed in the destination component coordinates.
+	 */
+	@Override
+	public void mousePressed(MouseEvent e) {
+		source = e.getComponent();
+		int width = source.getSize().width - dragInsets.left - dragInsets.right;
+		int height = source.getSize().height - dragInsets.top
+				- dragInsets.bottom;
+		Rectangle r = new Rectangle(dragInsets.left, dragInsets.top, width,
+				height);
+
+		if (r.contains(e.getPoint()))
+			setupForDragging(e);
+	}
+
+	private void setupForDragging(MouseEvent e) {
+		source.addMouseMotionListener(this);
+		potentialDrag = true;
+
+		// Determine the component that will ultimately be moved
+
+		if (destinationComponent != null) {
+			destination = destinationComponent;
+		} else if (destinationClass == null) {
+			destination = source;
+		} else // forward events to destination component
+		{
+			destination = SwingUtilities.getAncestorOfClass(destinationClass,
+					source);
+		}
+
+		pressed = e.getLocationOnScreen();
+		location = destination.getLocation();
+
+		if (changeCursor) {
+			originalCursor = source.getCursor();
+			source.setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
+		}
+
+		// Making sure autoscrolls is false will allow for smoother dragging of
+		// individual components
+
+		if (destination instanceof JComponent) {
+			JComponent jc = (JComponent) destination;
+			autoscrolls = jc.getAutoscrolls();
+			jc.setAutoscrolls(false);
+		}
+	}
+
+	/**
+	 * Move the component to its new location. The dragged Point must be in the
+	 * destination coordinates.
+	 */
+	@Override
+	public void mouseDragged(MouseEvent e) {
+		Point dragged = e.getLocationOnScreen();
+		int dragX = getDragDistance(dragged.x, pressed.x, snapSize.width);
+		int dragY = getDragDistance(dragged.y, pressed.y, snapSize.height);
+
+		int locationX = location.x + dragX;
+		int locationY = location.y + dragY;
+
+		// Mouse dragged events are not generated for every pixel the mouse
+		// is moved. Adjust the location to make sure we are still on a
+		// snap value.
+
+//		while (locationX < edgeInsets.left)
+//			locationX += snapSize.width;
+//
+//		while (locationY < edgeInsets.top)
+//			locationY += snapSize.height;
+//
+//		Dimension d = getBoundingSize(destination);
+
+//		while (locationX + destination.getSize().width + edgeInsets.right > d.width)
+//			locationX -= snapSize.width;
+//
+//		while (locationY + destination.getSize().height + edgeInsets.bottom > d.height)
+//			locationY -= snapSize.height;
+
+		// Adjustments are finished, move the component
+
+		destination.setLocation(locationX, locationY);
+	}
+
+	/*
+	 * Determine how far the mouse has moved from where dragging started (Assume
+	 * drag direction is down and right for positive drag distance)
+	 */
+	private int getDragDistance(int larger, int smaller, int snapSize) {
+		int halfway = snapSize / 2;
+		int drag = larger - smaller;
+		drag += (drag < 0) ? -halfway : halfway;
+		drag = (drag / snapSize) * snapSize;
+
+		return drag;
+	}
+
+	/*
+	 * Get the bounds of the parent of the dragged component.
+	 */
+	private Dimension getBoundingSize(Component source) {
+		if (source instanceof Window) {
+			GraphicsEnvironment env = GraphicsEnvironment
+					.getLocalGraphicsEnvironment();
+			Rectangle bounds = env.getMaximumWindowBounds();
+			return new Dimension(bounds.width, bounds.height);
+		} else {
+			return source.getParent().getSize();
+		}
+	}
+
+	/**
+	 * Restore the original state of the Component
+	 */
+	@Override
+	public void mouseReleased(MouseEvent e) {
+		if (!potentialDrag)
+			return;
+
+		source.removeMouseMotionListener(this);
+		potentialDrag = false;
+
+		if (changeCursor)
+			source.setCursor(originalCursor);
+
+		if (destination instanceof JComponent) {
+			((JComponent) destination).setAutoscrolls(autoscrolls);
+		}
+
+		// Layout the components on the parent container
+
+		if (autoLayout) {
+			if (destination instanceof JComponent) {
+				((JComponent) destination).revalidate();
+			} else {
+				destination.validate();
+			}
+		}
+	}
+}

+ 446 - 0
main/document/jcapture/src/com/hammurapi/jcapture/ComponentResizer.java

@@ -0,0 +1,446 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.GraphicsEnvironment;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Window;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+
+/**
+ * The ComponentResizer allows you to resize a component by dragging a border of
+ * the component.
+ * 
+ * Taken from http://tips4java.wordpress.com/2009/09/13/resizing-components/
+ */
+public class ComponentResizer extends MouseAdapter {
+	private final static Dimension MINIMUM_SIZE = new Dimension(10, 10);
+	private final static Dimension MAXIMUM_SIZE = new Dimension(
+			Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+	private static Map<Integer, Integer> cursors = new HashMap<Integer, Integer>();
+	{
+		cursors.put(1, Cursor.N_RESIZE_CURSOR);
+		cursors.put(2, Cursor.W_RESIZE_CURSOR);
+		cursors.put(4, Cursor.S_RESIZE_CURSOR);
+		cursors.put(8, Cursor.E_RESIZE_CURSOR);
+		cursors.put(3, Cursor.NW_RESIZE_CURSOR);
+		cursors.put(9, Cursor.NE_RESIZE_CURSOR);
+		cursors.put(6, Cursor.SW_RESIZE_CURSOR);
+		cursors.put(12, Cursor.SE_RESIZE_CURSOR);
+	}
+
+	private Insets dragInsets;
+	private Dimension snapSize;
+
+	private int direction;
+	protected static final int NORTH = 1;
+	protected static final int WEST = 2;
+	protected static final int SOUTH = 4;
+	protected static final int EAST = 8;
+
+	private Cursor sourceCursor;
+	private boolean resizing;
+	private Rectangle bounds;
+	private Point pressed;
+	private boolean autoscrolls;
+
+	private Dimension minimumSize = MINIMUM_SIZE;
+	private Dimension maximumSize = MAXIMUM_SIZE;
+
+	/**
+	 * Convenience contructor. All borders are resizable in increments of a
+	 * single pixel. Components must be registered separately.
+	 */
+	public ComponentResizer() {
+		this(new Insets(5, 5, 5, 5), new Dimension(1, 1));
+	}
+
+	/**
+	 * Convenience contructor. All borders are resizable in increments of a
+	 * single pixel. Components can be registered when the class is created or
+	 * they can be registered separately afterwards.
+	 * 
+	 * @param components
+	 *            components to be automatically registered
+	 */
+	public ComponentResizer(Component... components) {
+		this(new Insets(5, 5, 5, 5), new Dimension(1, 1), components);
+	}
+
+	/**
+	 * Convenience contructor. Eligible borders are resisable in increments of a
+	 * single pixel. Components can be registered when the class is created or
+	 * they can be registered separately afterwards.
+	 * 
+	 * @param dragInsets
+	 *            Insets specifying which borders are eligible to be resized.
+	 * @param components
+	 *            components to be automatically registered
+	 */
+	public ComponentResizer(Insets dragInsets, Component... components) {
+		this(dragInsets, new Dimension(1, 1), components);
+	}
+
+	/**
+	 * Create a ComponentResizer.
+	 * 
+	 * @param dragInsets
+	 *            Insets specifying which borders are eligible to be resized.
+	 * @param snapSize
+	 *            Specify the dimension to which the border will snap to when
+	 *            being dragged. Snapping occurs at the halfway mark.
+	 * @param components
+	 *            components to be automatically registered
+	 */
+	public ComponentResizer(Insets dragInsets, Dimension snapSize,
+			Component... components) {
+		setDragInsets(dragInsets);
+		setSnapSize(snapSize);
+		registerComponent(components);
+	}
+
+	/**
+	 * Get the drag insets
+	 * 
+	 * @return the drag insets
+	 */
+	public Insets getDragInsets() {
+		return dragInsets;
+	}
+
+	/**
+	 * Set the drag dragInsets. The insets specify an area where mouseDragged
+	 * events are recognized from the edge of the border inwards. A value of 0
+	 * for any size will imply that the border is not resizable. Otherwise the
+	 * appropriate drag cursor will appear when the mouse is inside the
+	 * resizable border area.
+	 * 
+	 * @param dragInsets
+	 *            Insets to control which borders are resizeable.
+	 */
+	public void setDragInsets(Insets dragInsets) {
+		validateMinimumAndInsets(minimumSize, dragInsets);
+
+		this.dragInsets = dragInsets;
+	}
+
+	/**
+	 * Get the components maximum size.
+	 * 
+	 * @return the maximum size
+	 */
+	public Dimension getMaximumSize() {
+		return maximumSize;
+	}
+
+	/**
+	 * Specify the maximum size for the component. The component will still be
+	 * constrained by the size of its parent.
+	 * 
+	 * @param maximumSize
+	 *            the maximum size for a component.
+	 */
+	public void setMaximumSize(Dimension maximumSize) {
+		this.maximumSize = maximumSize;
+	}
+
+	/**
+	 * Get the components minimum size.
+	 * 
+	 * @return the minimum size
+	 */
+	public Dimension getMinimumSize() {
+		return minimumSize;
+	}
+
+	/**
+	 * Specify the minimum size for the component. The minimum size is
+	 * constrained by the drag insets.
+	 * 
+	 * @param minimumSize
+	 *            the minimum size for a component.
+	 */
+	public void setMinimumSize(Dimension minimumSize) {
+		validateMinimumAndInsets(minimumSize, dragInsets);
+
+		this.minimumSize = minimumSize;
+	}
+
+	/**
+	 * Remove listeners from the specified component
+	 * 
+	 * @param component
+	 *            the component the listeners are removed from
+	 */
+	public void deregisterComponent(Component... components) {
+		for (Component component : components) {
+			component.removeMouseListener(this);
+			component.removeMouseMotionListener(this);
+		}
+	}
+
+	/**
+	 * Add the required listeners to the specified component
+	 * 
+	 * @param component
+	 *            the component the listeners are added to
+	 */
+	public void registerComponent(Component... components) {
+		for (Component component : components) {
+			component.addMouseListener(this);
+			component.addMouseMotionListener(this);
+		}
+	}
+
+	/**
+	 * Get the snap size.
+	 * 
+	 * @return the snap size.
+	 */
+	public Dimension getSnapSize() {
+		return snapSize;
+	}
+
+	/**
+	 * Control how many pixels a border must be dragged before the size of the
+	 * component is changed. The border will snap to the size once dragging has
+	 * passed the halfway mark.
+	 * 
+	 * @param snapSize
+	 *            Dimension object allows you to separately spcify a horizontal
+	 *            and vertical snap size.
+	 */
+	public void setSnapSize(Dimension snapSize) {
+		this.snapSize = snapSize;
+	}
+
+	/**
+	 * When the components minimum size is less than the drag insets then we
+	 * can't determine which border should be resized so we need to prevent this
+	 * from happening.
+	 */
+	private void validateMinimumAndInsets(Dimension minimum, Insets drag) {
+		int minimumWidth = drag.left + drag.right;
+		int minimumHeight = drag.top + drag.bottom;
+
+		if (minimum.width < minimumWidth || minimum.height < minimumHeight) {
+			String message = "Minimum size cannot be less than drag insets";
+			throw new IllegalArgumentException(message);
+		}
+	}
+
+	/**
+	 */
+	@Override
+	public void mouseMoved(MouseEvent e) {
+		Component source = e.getComponent();
+		Point location = e.getPoint();
+		direction = 0;
+
+		if (location.x < dragInsets.left)
+			direction += WEST;
+
+		if (location.x > source.getWidth() - dragInsets.right - 1)
+			direction += EAST;
+
+		if (location.y < dragInsets.top)
+			direction += NORTH;
+
+		if (location.y > source.getHeight() - dragInsets.bottom - 1)
+			direction += SOUTH;
+
+		// Mouse is no longer over a resizable border
+
+		if (direction == 0) {
+			source.setCursor(sourceCursor);
+		} else // use the appropriate resizable cursor
+		{
+			int cursorType = cursors.get(direction);
+			Cursor cursor = Cursor.getPredefinedCursor(cursorType);
+			source.setCursor(cursor);
+		}
+	}
+
+	@Override
+	public void mouseEntered(MouseEvent e) {
+		if (!resizing) {
+			Component source = e.getComponent();
+			sourceCursor = source.getCursor();
+		}
+	}
+
+	@Override
+	public void mouseExited(MouseEvent e) {
+		if (!resizing) {
+			Component source = e.getComponent();
+			source.setCursor(sourceCursor);
+		}
+	}
+
+	@Override
+	public void mousePressed(MouseEvent e) {
+		// The mouseMoved event continually updates this variable
+
+		if (direction == 0)
+			return;
+
+		// Setup for resizing. All future dragging calculations are done based
+		// on the original bounds of the component and mouse pressed location.
+
+		resizing = true;
+
+		Component source = e.getComponent();
+		pressed = e.getPoint();
+		SwingUtilities.convertPointToScreen(pressed, source);
+		bounds = source.getBounds();
+
+		// Making sure autoscrolls is false will allow for smoother resizing
+		// of components
+
+		if (source instanceof JComponent) {
+			JComponent jc = (JComponent) source;
+			autoscrolls = jc.getAutoscrolls();
+			jc.setAutoscrolls(false);
+		}
+	}
+
+	/**
+	 * Restore the original state of the Component
+	 */
+	@Override
+	public void mouseReleased(MouseEvent e) {
+		resizing = false;
+
+		Component source = e.getComponent();
+		source.setCursor(sourceCursor);
+
+		if (source instanceof JComponent) {
+			((JComponent) source).setAutoscrolls(autoscrolls);
+		}
+	}
+
+	/**
+	 * Resize the component ensuring location and size is within the bounds of
+	 * the parent container and that the size is within the minimum and maximum
+	 * constraints.
+	 * 
+	 * All calculations are done using the bounds of the component when the
+	 * resizing started.
+	 */
+	@Override
+	public void mouseDragged(MouseEvent e) {
+		if (resizing == false)
+			return;
+
+		Component source = e.getComponent();
+		Point dragged = e.getPoint();
+		SwingUtilities.convertPointToScreen(dragged, source);
+
+		changeBounds(source, direction, bounds, pressed, dragged);
+	}
+
+	protected void changeBounds(Component source, int direction,
+			Rectangle bounds, Point pressed, Point current) {
+		// Start with original locaton and size
+
+		int x = bounds.x;
+		int y = bounds.y;
+		int width = bounds.width;
+		int height = bounds.height;
+
+		// Resizing the West or North border affects the size and location
+
+		if (WEST == (direction & WEST)) {
+			int drag = getDragDistance(pressed.x, current.x, snapSize.width);
+			int maximum = Math.min(width + x, maximumSize.width);
+			drag = getDragBounded(drag, snapSize.width, width,
+					minimumSize.width, maximum);
+
+			x -= drag;
+			width += drag;
+		}
+
+		if (NORTH == (direction & NORTH)) {
+			int drag = getDragDistance(pressed.y, current.y, snapSize.height);
+			int maximum = Math.min(height + y, maximumSize.height);
+			drag = getDragBounded(drag, snapSize.height, height,
+					minimumSize.height, maximum);
+
+			y -= drag;
+			height += drag;
+		}
+
+		// Resizing the East or South border only affects the size
+
+		if (EAST == (direction & EAST)) {
+			int drag = getDragDistance(current.x, pressed.x, snapSize.width);
+			Dimension boundingSize = getBoundingSize(source);
+			int maximum = Math.min(boundingSize.width - x, maximumSize.width);
+			drag = getDragBounded(drag, snapSize.width, width,
+					minimumSize.width, maximum);
+			width += drag;
+		}
+
+		if (SOUTH == (direction & SOUTH)) {
+			int drag = getDragDistance(current.y, pressed.y, snapSize.height);
+			Dimension boundingSize = getBoundingSize(source);
+			int maximum = Math.min(boundingSize.height - y, maximumSize.height);
+			drag = getDragBounded(drag, snapSize.height, height,
+					minimumSize.height, maximum);
+			height += drag;
+		}
+
+		source.setBounds(x, y, width, height);
+		source.validate();
+	}
+
+	/*
+	 * Determine how far the mouse has moved from where dragging started
+	 */
+	private int getDragDistance(int larger, int smaller, int snapSize) {
+		int halfway = snapSize / 2;
+		int drag = larger - smaller;
+		drag += (drag < 0) ? -halfway : halfway;
+		drag = (drag / snapSize) * snapSize;
+
+		return drag;
+	}
+
+	/*
+	 * Adjust the drag value to be within the minimum and maximum range.
+	 */
+	private int getDragBounded(int drag, int snapSize, int dimension,
+			int minimum, int maximum) {
+		while (dimension + drag < minimum)
+			drag += snapSize;
+
+		while (dimension + drag > maximum)
+			drag -= snapSize;
+
+		return drag;
+	}
+
+	/*
+	 * Keep the size of the component within the bounds of its parent.
+	 */
+	private Dimension getBoundingSize(Component source) {
+		if (source instanceof Window) {
+			GraphicsEnvironment env = GraphicsEnvironment
+					.getLocalGraphicsEnvironment();
+			Rectangle bounds = env.getMaximumWindowBounds();
+			return new Dimension(bounds.width, bounds.height);
+		} else {
+			return source.getParent().getSize();
+		}
+	}
+}

+ 28 - 0
main/document/jcapture/src/com/hammurapi/jcapture/FragmentImpl.java

@@ -0,0 +1,28 @@
+package com.hammurapi.jcapture;
+
+import java.io.File;
+import java.util.List;
+
+import com.hammurapi.jcapture.VideoEncoder.Fragment;
+
+class FragmentImpl implements Fragment {		
+	
+	private File audio;
+	private List<Frame> frames;
+	
+	FragmentImpl( List<Frame> frames, File audio) {
+		this.audio = audio;
+		this.frames = frames;
+	}
+
+	@Override
+	public List<Frame> getFrames() {
+		return frames;
+	}
+
+	@Override
+	public File getAudio() {
+		return audio;
+	}
+
+}

+ 57 - 0
main/document/jcapture/src/com/hammurapi/jcapture/FrameImpl.java

@@ -0,0 +1,57 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Dimension;
+import java.awt.Point;
+import java.util.List;
+
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame;
+
+class FrameImpl implements Frame {
+	
+	private List<Shape> shapes;
+	private Point mousePointer;
+	private Dimension size;
+	private boolean isActive;
+	
+	FrameImpl(List<Shape> shapes, Point mousePointer, Dimension size, boolean isActive) {
+		super();
+		this.shapes = shapes;
+		this.mousePointer = mousePointer;
+		this.size = size;
+		this.isActive = isActive;
+	}
+	
+	/**
+	 * Merges frame before this frame into this frame by incorporating its shapes.
+	 * This method is used for merging deleted frames.
+	 * @param frame
+	 */
+	void merge(Frame frame) {
+		for (Shape shape: shapes) {
+			if (shape.getContent().coversEverything()) {
+				return; // No need in previous shapes.
+			}
+		}
+		shapes.addAll(0, frame.getShapes());
+	}
+
+	@Override
+	public List<Shape> getShapes() {
+		return shapes;
+	}
+
+	@Override
+	public Point getMousePointer() {
+		return mousePointer;
+	}
+
+	@Override
+	public Dimension getSize() {
+		return size;
+	}
+
+	@Override
+	public boolean isActive() {
+		return isActive;
+	}
+}

+ 21 - 0
main/document/jcapture/src/com/hammurapi/jcapture/GraphicsDeviceTranslucener.java

@@ -0,0 +1,21 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Frame;
+import java.awt.GraphicsDevice;
+import java.awt.GraphicsEnvironment;
+import java.awt.GraphicsDevice.WindowTranslucency;
+
+public class GraphicsDeviceTranslucener extends Translucener {
+
+	@Override
+	protected void makeTranslucent(Frame frame) {
+		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
+        GraphicsDevice gd = ge.getDefaultScreenDevice();
+
+        //If translucent windows aren't supported, exit.
+        if (gd.isWindowTranslucencySupported(WindowTranslucency.TRANSLUCENT)) {
+        	frame.setOpacity(0.7f);	
+        }		
+	}
+
+}

+ 80 - 0
main/document/jcapture/src/com/hammurapi/jcapture/ImageToolTip.java

@@ -0,0 +1,80 @@
+/*  
+ * This file is an adapted example from javareference.com  
+ * for more information visit,  
+ * http://www.javareference.com  
+ */
+package com.hammurapi.jcapture;
+
+import java.awt.Dimension;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Image;
+
+import javax.swing.JComponent;
+import javax.swing.JToolTip;
+import javax.swing.SwingUtilities;
+import javax.swing.plaf.metal.MetalToolTipUI;
+
+/**
+ * This class extends JToolTip and set the UI to ImageToolTipUI.
+ * 
+ * @author Rahul Sapkal(rahul@javareference.com)
+ */
+public class ImageToolTip extends JToolTip {
+
+	/**
+	 * This class extends MetalToolTipUI and provides customizes it to draw a
+	 * given image on it.
+	 * 
+	 * @author Rahul Sapkal(rahul@javareference.com)
+	 */
+	private class ImageToolTipUI extends MetalToolTipUI {
+		private Image m_image;
+
+		public ImageToolTipUI(Image image) {
+			m_image = image;
+		}
+
+		/**
+		 * This method is overriden from the MetalToolTipUI to draw the given
+		 * image and text
+		 */
+		public void paint(Graphics g, JComponent c) {
+			FontMetrics metrics = c.getFontMetrics(g.getFont());
+			g.setColor(c.getForeground());
+
+			g.drawString(((ImageToolTip) c).text, 3, 15);
+
+			g.drawImage(m_image, 3, metrics.getHeight() + 3, c);
+		}
+
+		/**
+		 * This method is overriden from the MetalToolTipUI to return the
+		 * appropiate preferred size to size the ToolTip to show both the text
+		 * and image.
+		 */
+		public Dimension getPreferredSize(JComponent c) {
+			FontMetrics metrics = c.getFontMetrics(c.getFont());
+			String tipText = ((JToolTip) c).getTipText();
+			if (tipText == null) {
+				tipText = "";
+			}
+
+			int width = SwingUtilities.computeStringWidth(metrics, tipText);
+			int height = metrics.getHeight() + m_image.getHeight(c) + 6;
+
+			if (width < m_image.getWidth(c)) {
+				width = m_image.getWidth(c);
+			}
+
+			return new Dimension(width, height);
+		}
+	}
+
+	private String text;
+
+	public ImageToolTip(String text, Image image) {
+		this.text = text;
+		setUI(new ImageToolTipUI(image));
+	}
+}

+ 92 - 0
main/document/jcapture/src/com/hammurapi/jcapture/JCaptureApplet.java

@@ -0,0 +1,92 @@
+package com.hammurapi.jcapture;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.mime.MultipartEntity;
+import org.apache.http.entity.mime.content.InputStreamBody;
+import org.apache.http.entity.mime.content.StringBody;
+
+public class JCaptureApplet extends AbstractCaptureApplet {
+	
+	private static final String HTTPS_PREFIX = "https://";
+
+	protected HttpUriRequest createRequest(String fileName, InputStreamBody bin) throws Exception {
+		String uploadUrl = getParameter("uploadUrl");
+		if (uploadUrl==null || uploadUrl.trim().length()==0) {		
+			String host = getParameter("host");
+			String dokuHost = host;
+			
+			if (dokuHost.toLowerCase().startsWith(HTTPS_PREFIX)) {
+				if (dokuHost.lastIndexOf(":")<HTTPS_PREFIX.length()) { // No port number
+					dokuHost+=":443";
+				}
+			} else if (dokuHost.endsWith(":80")) {
+				dokuHost = dokuHost.substring(0, dokuHost.length()-3);
+			} 
+			System.out.println("DokuHost: "+dokuHost);
+			String dokuBase = getDokuBase();
+			System.out.println("DokuBase: "+dokuBase);
+
+			StringBuilder uploadUrlBuilder = new StringBuilder(dokuHost);
+
+			if (dokuBase.startsWith(host)) {
+				dokuBase = dokuBase.substring(host.length());
+			}
+			uploadUrlBuilder.append(dokuBase);
+			uploadUrlBuilder.append("lib/exe/mediamanager.php");
+			uploadUrl = uploadUrlBuilder.toString();
+		}
+		System.out.println("Uploading to "+uploadUrl);
+        HttpPost httppost = new HttpPost(uploadUrl);
+
+        if (!httppost.containsHeader("Cookie")) {
+        	httppost.setHeader("Cookie", getCookies());
+        }
+        
+        httppost.setHeader("Pragma", "No-cache");       
+	    
+	    MultipartEntity reqEntity = new MultipartEntity();
+	    String sectok = getParameter("sectok");
+	    if (sectok!=null && sectok.trim().length()>0) {
+			reqEntity.addPart("sectok", new StringBody(sectok));
+	    }
+	    reqEntity.addPart("ow", new StringBody("1"));	
+	    
+	    String opaque = getParameter("opaque");
+	    if (opaque!=null && opaque.trim().length()>0) {
+			reqEntity.addPart("opaque", new StringBody(opaque));	    	
+	    }
+	    
+	    reqEntity.addPart("Filename", new StringBody(fileName));
+	    
+	    int nsIdx = fileName.lastIndexOf(":");
+	    String namespace;
+	    if (nsIdx==-1) {
+	    	namespace = ":";
+	    } else {
+	    	namespace = ":"+fileName.substring(0, nsIdx);
+	    	fileName = fileName.substring(nsIdx+1);
+	    }
+        
+	    if (namespace!=null) {
+		    reqEntity.addPart("ns", new StringBody(namespace));		    	
+	    }
+	    
+	    reqEntity.addPart("Filedata", bin);
+	    
+	    httppost.setEntity(reqEntity);
+		return httppost;
+	}
+
+	String getDokuBase() throws DecoderException {
+		return new String(Hex.decodeHex(getParameter("dokuBase").toCharArray()));
+	}
+
+	@Override
+	protected String bodyName(String fileName) {
+	    return fileName.substring(fileName.lastIndexOf(":")+1);
+	}
+	
+}

+ 121 - 0
main/document/jcapture/src/com/hammurapi/jcapture/MappedImage.java

@@ -0,0 +1,121 @@
+package com.hammurapi.jcapture;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+import java.util.zip.Adler32;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Mapped image is softly kept in memory and also is written to a temporary file.
+ * If image reference is cleared by the garbage collector, the image is loaded from the file on demand.
+ * @author Pavel
+ *
+ */
+public class MappedImage {
+		
+	private Reference<BufferedImage> imageRef;
+	private Reference<byte[]> imageBytesRef;
+	private MappedByteBuffer buffer;
+	private int height;
+	private int width;
+	private String format;
+	private long checksum;
+	private int bytesLength;
+	
+	public MappedImage(final BufferedImage image, String format, FileChannel channel) throws IOException {	
+		if (format==null) {
+			throw new NullPointerException("Format is null");
+		}
+		
+		class HardReference extends SoftReference<BufferedImage> {
+
+			HardReference(BufferedImage referent) {
+				super(referent);
+			}
+			
+			@Override
+			public BufferedImage get() {
+				return image;
+			}
+			
+		}
+		imageRef = channel==null ? new HardReference(image) : new SoftReference<BufferedImage>(image);
+		width = image.getWidth();
+		height = image.getHeight();
+		this.format = format;
+		if (channel!=null) {
+			ByteArrayOutputStream baos = new ByteArrayOutputStream();
+			ImageIO.write(imageRef.get(), format, baos);
+			baos.close();
+			byte[] imageBytes = baos.toByteArray();
+			Adler32 adler = new Adler32();
+			adler.update(imageBytes);
+			checksum = adler.getValue();
+			bytesLength = imageBytes.length;
+			imageBytesRef = new SoftReference<byte[]>(imageBytes);
+			synchronized (channel) {
+				long position = channel.position();
+				channel.write(ByteBuffer.wrap(imageBytes));
+				buffer = channel.map(MapMode.READ_ONLY, position, imageBytes.length);
+			}
+		}
+	}
+	
+	public byte[] getImageBytes() throws IOException {
+		if (imageBytesRef==null) {
+			ByteArrayOutputStream baos = new ByteArrayOutputStream();
+			ImageIO.write(imageRef.get(), format, baos);
+			return baos.toByteArray();			
+		}
+		byte[] ret = imageBytesRef.get();
+		if (ret==null) {
+			buffer.load();
+			buffer.rewind();
+			ret = new byte[buffer.remaining()];
+			buffer.get(ret);
+			if (bytesLength != ret.length) {
+				throw new IllegalStateException("Invalid image bytes length, expected "+bytesLength+", got "+ret.length);
+			}
+										
+			Adler32 adler = new Adler32();
+			adler.update(ret);
+			if (checksum != adler.getValue()) {
+				throw new IllegalStateException("Invalid image bytes checksum");
+			}
+			imageBytesRef = new SoftReference<byte[]>(ret);
+		}
+		return ret;
+	}
+
+	/**
+	 * Reads from reference, if reference was cleared, loads from the mapped buffer.
+	 * @return
+	 * @throws IOException
+	 */
+	public BufferedImage getImage() throws IOException {
+		BufferedImage ret = imageRef.get();
+		if (ret==null) {
+			ret = ImageIO.read(new ByteArrayInputStream(getImageBytes()));
+			imageRef = new SoftReference<BufferedImage>(ret);
+		}
+		return ret;
+	}
+	
+	public int getHeight() {
+		return height;
+	}
+	
+	public int getWidth() {
+		return width;
+	}
+
+}

+ 55 - 0
main/document/jcapture/src/com/hammurapi/jcapture/Movie.java

@@ -0,0 +1,55 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Dimension;
+import java.io.Closeable;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.List;
+
+public class Movie implements Closeable {
+	
+	private float framesPerSecond;
+	private List<VideoEncoder.Fragment> fragments;
+	private Dimension frameDimension;
+	private Closeable imagesFileCloseable;	
+
+	public Movie(Dimension frameDimension, float framesPerSecond, List<VideoEncoder.Fragment> fragments, Closeable imagesFileCloseable) {
+		super();
+		this.frameDimension = frameDimension;
+		this.framesPerSecond = framesPerSecond;
+		this.fragments = fragments;
+		this.imagesFileCloseable = imagesFileCloseable;
+	}
+
+	public List<VideoEncoder.Fragment> getFragments() {
+		return fragments;
+	}
+	
+	public float getFramesPerSecond() {
+		return framesPerSecond;
+	}
+	
+	public Dimension getFrameDimension() {
+		return frameDimension;
+	}
+	
+	@Override
+	public String toString() {
+		int frames = 0;
+		for (VideoEncoder.Fragment f: fragments) {
+			frames+=f.getFrames().size();
+		}
+		
+		long length = (long) (frames/framesPerSecond);
+		
+		return MessageFormat.format("{0,number,00}:{1,number,00}:{2,number,00}, {3} frames", length/3600, (length/60) % 60, length % 60, frames);
+	}
+
+	@Override
+	public void close() throws IOException {
+		if (imagesFileCloseable!=null) {
+			imagesFileCloseable.close();
+		}		
+	}
+	
+}

+ 1124 - 0
main/document/jcapture/src/com/hammurapi/jcapture/MovieEditorDialog.java

@@ -0,0 +1,1124 @@
+package com.hammurapi.jcapture;
+
+import java.awt.AlphaComposite;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Image;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.SourceDataLine;
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JToolTip;
+import javax.swing.ListSelectionModel;
+import javax.swing.ProgressMonitor;
+import javax.swing.SwingWorker;
+import javax.swing.Timer;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableModel;
+
+import com.hammurapi.jcapture.ShapeImpl.ImageImpl;
+import com.hammurapi.jcapture.VideoEncoder.Fragment;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ImageReference;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ShapeContent;
+
+public class MovieEditorDialog extends javax.swing.JDialog {
+	
+	private static final double DECIBELS_PER_PIXEL = 2.0;
+	private static final double NORMALIZED_LEVEL = 0.95;
+	private static final int AUDIO_CELL_HEIGHT = 50;
+	private static final int MEDIAN = AUDIO_CELL_HEIGHT/2;
+	int minCellDimension = 10;
+	int minToolTipImageDimension = 150;
+	
+	int splashIndex = -1;
+	
+	double coeff;
+	
+	private static Color INACTIVE_COLOR = new Color(230, 230, 230);
+	private static Color ACTIVE_COLOR = Color.white;
+	private static Color SELECTED_COLOR = new Color(0, 0, 255, 70);
+	private static Color FOCUSED_COLOR = new Color(0, 0, 255, 100);
+	private static Color PLAYING_COLOR = new Color(255, 0, 0, 100);
+
+	private static Color SPLASH_COLOR = new Color(0, 255, 0, 127);	
+	
+	private static Color SOUND_COLOR = new Color(0, 0, 127);
+	private static Color DELETED_SOUND_COLOR = new Color(100, 100, 100);
+	
+	private JButton saveButton;
+	private JPanel contentPanel;
+	private JScrollPane timeLineScrollPane;
+	private JCheckBox normalizeVolumeCheckBox;
+	private JPanel frameCanvas;
+	JTable timeLineTable;
+	private JButton discardButton;
+	private int focusColumn = 0;
+	private int playingColumn = -1;
+	private Image mouseImage;
+	private double maxVolume = -1;
+	FrameEntry[] frameEntries;
+	int cellWidth;
+	int cellHeight;
+	int toolTipImageWidth;
+	int toolTipImageHeight;
+	boolean hasAudio;
+	private Movie movie;
+	
+	int numChannels;
+	int validBits;
+	long sampleRate;
+	Timer[] playTimera = {null};
+	private Executor backgroundProcessor;
+	private double inactivityInterval;
+	private String imageFormat;
+	
+	private class FrameEntry {
+		
+		// Not null for first frames in fragments indicating that 
+		// Indicating that it's time to open a new audio file.
+		File audioFile;
+		
+		boolean mouseMoved;
+		
+		// Scaled samples for painting - not real ones.
+		// idx, {min, max}
+		double[] audioSamples;
+
+		// Number of real samples falling to this frame.
+		int audioSamplesInFrame;
+		
+		boolean isDeleted;
+		
+		Reference<BufferedImage> toolTipImageRef;
+		
+		Reference<BufferedImage> frameImageRef;
+		
+		// row, selected, focus
+		private JPanel[][][] canvases = {
+				{ {new FrameCellCanvas(false, false), new FrameCellCanvas(false, true)}, {new FrameCellCanvas(true, false), new FrameCellCanvas(true, true)} },
+				{ {new AudioCellCanvas(false, false), new AudioCellCanvas(false, true)}, {new AudioCellCanvas(true, false), new AudioCellCanvas(true, true)} }
+		};
+		
+		class CellCanvas extends JPanel {
+			
+			boolean selected;
+			boolean hasFocus;
+
+			CellCanvas(boolean selected, boolean hasFocus) {
+				this.selected = selected;
+				this.hasFocus = hasFocus;
+			}
+			
+		}
+		
+		class FrameCellCanvas extends CellCanvas {
+
+			FrameCellCanvas(boolean selected, boolean hasFocus) {
+				super(selected, hasFocus);
+			}
+			
+			@Override
+			public void paintComponent(Graphics g) {
+				super.paintComponent(g);
+				paintFrame(this, g, selected, hasFocus);
+			}
+			
+		}
+		
+		class AudioCellCanvas extends CellCanvas {
+
+			AudioCellCanvas(boolean selected, boolean hasFocus) {
+				super(selected, hasFocus);
+			}
+			
+			@Override
+			public void paintComponent(Graphics g) {
+				super.paintComponent(g);
+				paintAudio(this, g, selected, hasFocus);
+			}
+			
+		}
+		
+		int idx;
+		int delta;
+		
+		Frame frame;
+		
+		BufferedImage getToolTipImage() throws IOException {
+			BufferedImage ret = toolTipImageRef==null ? null : toolTipImageRef.get();
+			if (ret == null) {
+				BufferedImage image = getImage();
+		    	ret = new BufferedImage(toolTipImageWidth, toolTipImageHeight, image.getType());
+		    	Graphics2D g = ret.createGraphics();
+		    	g.setComposite(AlphaComposite.Src);
+		    	g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+		    	g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
+		    	g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
+		    	g.drawImage(image, 0, 0, ret.getWidth(), ret.getHeight(), null);
+		    	g.dispose();
+				toolTipImageRef = new SoftReference<BufferedImage>(ret);
+			}
+			return ret;
+			
+		}
+		
+		void paintFrame(FrameCellCanvas frameCellCanvas, Graphics g, boolean selected, boolean hasFocus) {
+			g.setColor(frame.isActive() ? ACTIVE_COLOR : INACTIVE_COLOR);
+			g.fillRect(0, 0, frameCellCanvas.getWidth(), frameCellCanvas.getHeight());
+			
+			if (idx==splashIndex) {
+				g.setColor(SPLASH_COLOR);
+				g.fillRect(1, 1, frameCellCanvas.getWidth()-2, frameCellCanvas.getHeight()-2);
+				
+			}
+			
+			if (frame.getMousePointer()!=null) {
+				int mx = (int) (frame.getMousePointer().getX()*(frameCellCanvas.getWidth()-3)/frame.getSize().getWidth())+1;
+				int my = (int) (frame.getMousePointer().getY()*(frameCellCanvas.getHeight()-3)/frame.getSize().getHeight())+1;
+				g.setColor(mouseMoved ? Color.BLACK : Color.GRAY);
+				g.fillRect(mx, my, 2, 2);
+			}
+			
+			if (isDeleted) {
+				g.setColor(Color.RED);
+				g.drawLine(2, 2, frameCellCanvas.getWidth()-2, frameCellCanvas.getHeight()-2);
+				g.drawLine(frameCellCanvas.getWidth()-2, 2, 2, frameCellCanvas.getHeight()-2);
+			}
+			
+			decorate(frameCellCanvas, g, selected, hasFocus);
+		}
+
+		void paintAudio(AudioCellCanvas audioCellCanvas, Graphics g, boolean selected, boolean hasFocus) {
+			g.setColor(frame.isActive() ? ACTIVE_COLOR : INACTIVE_COLOR);
+			g.fillRect(0, 0, audioCellCanvas.getWidth(), audioCellCanvas.getHeight());
+			
+			if (audioSamples!=null) {
+				for (int i = 0; i<audioCellCanvas.getWidth(); ++i) {
+					g.setColor(isDeleted ? DELETED_SOUND_COLOR : SOUND_COLOR);
+					int volume = (int) (20.0*Math.log10(coeff*audioSamples[Math.min(i, audioSamples.length-1)]+1)/DECIBELS_PER_PIXEL);
+					g.drawLine(i, MEDIAN - volume, i,  MEDIAN + volume);
+				}
+			}
+			
+			decorate(audioCellCanvas, g, selected, hasFocus);
+		}
+		
+		private void decorate(JComponent component, Graphics g, boolean selected, boolean hasFocus) {
+			if (idx==playingColumn) {
+				g.setColor(PLAYING_COLOR);
+				Rectangle bounds = g.getClipBounds();
+				g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
+			} else if (hasFocus) {
+				g.setColor(FOCUSED_COLOR);
+				Rectangle bounds = g.getClipBounds();
+				g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
+			} else if (selected) {
+				g.setColor(SELECTED_COLOR);
+				Rectangle bounds = g.getClipBounds();
+				g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
+			}
+		}
+		
+		BufferedImage getImage() throws IOException {
+			BufferedImage ret = frameImageRef==null ? null : frameImageRef.get();
+			if (ret == null) {
+				int startIdx = idx;
+				while (startIdx>0 && !coversEverything(startIdx)) {
+					--startIdx;
+				}
+				int deltaArea = 0;
+				ret = new BufferedImage(frame.getSize().width, frame.getSize().height, shapeImage(frameEntries[startIdx].frame.getShapes().get(0)).getType());
+				Graphics2D g = ret.createGraphics();
+				for (int i=startIdx; i<=idx; ++i) {
+					for (Shape shape: frameEntries[i].frame.getShapes()) {
+						BufferedImage si = shapeImage(shape);
+						g.drawImage(si, shape.getLocation().x, shape.getLocation().y, null);
+						if (i==idx) {
+							deltaArea+=si.getWidth()*si.getHeight();
+						}
+					}
+				}
+				delta = (int) (100.0*deltaArea/(frame.getSize().width * frame.getSize().height));
+				if (frame.getMousePointer()!=null) {
+					g.drawImage(mouseImage, frame.getMousePointer().x, frame.getMousePointer().y, null);
+				}
+				frameImageRef = new SoftReference<BufferedImage>(ret);
+			}
+			return ret;
+		}
+
+		private BufferedImage shapeImage(Shape shape) throws IOException {
+			ShapeContent shapeContent = shape.getContent();
+			if (shapeContent instanceof ImageReference) {
+				return ((ImageReference) shapeContent).getImage().getImage().getImage();
+			}
+			return ((com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.Image) shapeContent).getImage().getImage();
+		}
+
+		boolean coversEverything(int entryIdx) {
+			for (Shape shape: frameEntries[entryIdx].frame.getShapes()) {
+				if (shape.getContent().coversEverything()) {
+					return true;
+				}
+			}
+			return false;
+		}
+
+		public Component getCellRendererComponent(int row, boolean isSelected, boolean hasFocus) {
+			return canvases[row][isSelected ? 1 : 0][hasFocus ? 1 : 0];
+		}
+		
+	}
+	
+	public MovieEditorDialog(
+			final JFrame frame, 
+			final Movie movie, 
+			final Executor backgroundProcessor, 
+			double inactivityInterval, 
+			String imageFormat) {
+		
+		super(frame, "Movie editor ("+movie+")");
+		frame.setAlwaysOnTop(false);
+		frame.setVisible(false);
+		
+		this.movie = movie;
+		this.backgroundProcessor = backgroundProcessor;
+		this.inactivityInterval = inactivityInterval;
+		this.imageFormat = imageFormat;
+		
+		setModal(true);
+		setIconImage(frame.getIconImage());
+		
+		mouseImage = Toolkit.getDefaultToolkit().getImage(getClass().getResource("mouse.png"));
+		
+		double aspectRatio = (double) movie.getFrameDimension().getWidth()/(double) movie.getFrameDimension().getHeight();
+		
+		if (aspectRatio>1) {
+			cellHeight = minCellDimension;
+			cellWidth = (int) Math.round(aspectRatio*cellHeight);
+			
+			toolTipImageHeight = minToolTipImageDimension;
+			toolTipImageWidth = (int) Math.round(aspectRatio*toolTipImageHeight);
+		} else {
+			cellWidth = minCellDimension;
+			cellHeight = (int) Math.round((double) cellWidth/aspectRatio);
+			
+			toolTipImageWidth = minToolTipImageDimension;
+			toolTipImageHeight = (int) Math.round((double) toolTipImageWidth/aspectRatio);
+		}
+		
+		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
+		
+		addWindowListener(new WindowAdapter() {
+            public void windowClosing(WindowEvent e) {
+                int confirmed = JOptionPane.showConfirmDialog(null,
+                                "Are you sure you want to exit and discard the movie?", "User Confirmation",
+                                JOptionPane.YES_NO_OPTION);
+                if (confirmed == JOptionPane.YES_OPTION) {
+                	dispose();
+					getOwner().setVisible(false);
+                }
+            }
+		});
+		
+		SwingWorker<Boolean, Long> loader = new SwingWorker<Boolean, Long>() {
+
+			@Override
+			protected Boolean doInBackground() throws Exception {
+				int totalFrames = 0;
+				for (Fragment fr: movie.getFragments()) {
+					if (fr.getAudio()!=null) {
+						hasAudio = true;
+					}
+					totalFrames+=fr.getFrames().size();
+				}
+				
+				ProgressMonitor progressMonitor = new ProgressMonitor(frame, "Loading frames", "Loading movie frames", 0, totalFrames);
+				
+				try {
+					frameEntries = new FrameEntry[totalFrames];
+					int idx = 0;
+					double audioSamplesPerFrame = -1;
+					Point prevMouse = null;
+					for (Fragment fr: movie.getFragments()) {
+						WavFile wavFile = fr.getAudio()==null ? null : WavFile.openWavFile(fr.getAudio());
+						if (wavFile!=null) {
+							audioSamplesPerFrame = wavFile.getSampleRate()/movie.getFramesPerSecond();	
+							numChannels = wavFile.getNumChannels();
+							validBits = wavFile.getValidBits();
+							sampleRate = wavFile.getSampleRate();
+						}
+						int audioFramesRead = 0;
+						int framePosition = 0;
+						for (Frame frm: fr.getFrames()) {
+							if (progressMonitor.isCanceled()) {
+								return false;
+							}
+							frameEntries[idx] = new FrameEntry();
+							frameEntries[idx].frame = frm;
+							frameEntries[idx].idx = idx;
+							if (frm.getMousePointer()!=null) {
+								frameEntries[idx].mouseMoved = !frm.getMousePointer().equals(prevMouse);
+							}
+							prevMouse = frm.getMousePointer();
+							
+							if (framePosition == 0) {
+								frameEntries[idx].audioFile = fr.getAudio();
+							}
+							
+							if (wavFile!=null && wavFile.getFramesRemaining()>0) {
+								frameEntries[idx].audioSamplesInFrame = (int) ((framePosition+1)*audioSamplesPerFrame-audioFramesRead);
+								frameEntries[idx].audioSamples = new double[cellWidth];
+								double[][] sampleBuffer = new double[wavFile.getNumChannels()][frameEntries[idx].audioSamplesInFrame];
+								frameEntries[idx].audioSamplesInFrame=wavFile.readFrames(sampleBuffer, frameEntries[idx].audioSamplesInFrame);								
+								audioFramesRead+=frameEntries[idx].audioSamplesInFrame;
+								for (int i=0; i<frameEntries[idx].audioSamplesInFrame; ++i) {
+									for (int ch=0; ch<wavFile.getNumChannels(); ++ch) {
+										maxVolume = Math.max(maxVolume, Math.abs(sampleBuffer[ch][i]));
+										int asidx = i*cellWidth/frameEntries[idx].audioSamplesInFrame;
+										frameEntries[idx].audioSamples[asidx] = Math.max(Math.abs(sampleBuffer[ch][i]), frameEntries[idx].audioSamples[asidx]);
+									}
+								}																
+							}
+							
+							++idx;
+							++framePosition;
+							progressMonitor.setProgress(idx);
+						}
+						if (wavFile!=null) {
+							wavFile.close();
+						}
+						
+					}
+					
+					coeff = Math.pow(10.0, DECIBELS_PER_PIXEL*(MEDIAN-1)/20.0)/maxVolume;
+					
+					return true;
+				} finally {
+					progressMonitor.close();
+				}
+			}
+			
+			protected void done() {
+				try {
+					if (get()) {
+						buildUI();
+						setLocationRelativeTo(frame);
+						setVisible(true);
+					} else {
+				    	JOptionPane.showMessageDialog(
+				    			MovieEditorDialog.this,
+								"Loading operation was cancelled", 
+								"Loading cancelled",
+								JOptionPane.ERROR_MESSAGE);				    	
+					}
+				} catch (Exception e) {
+					e.printStackTrace();
+			    	JOptionPane.showMessageDialog(
+			    			MovieEditorDialog.this,
+							e.toString(), 
+							"Error loading frames",
+							JOptionPane.ERROR_MESSAGE);				    	
+				}
+				
+			};
+			
+		};
+		
+		loader.execute();
+		
+	}
+						
+	void buildUI() {
+		BorderLayout thisLayout = new BorderLayout();
+		getContentPane().setLayout(thisLayout);
+
+		contentPanel = new JPanel();
+		GridBagLayout contentPanelLayout = new GridBagLayout();
+		getContentPane().add(contentPanel, BorderLayout.CENTER);
+		contentPanelLayout.rowWeights = new double[] { 0.1, 0.0, 0.0, 0.0, 0.0,	0.0 };
+		contentPanelLayout.rowHeights = new int[] { movie.getFrameDimension().height, 7, cellHeight+(hasAudio? 23 + AUDIO_CELL_HEIGHT : 22), 7, 7, 7 };
+		contentPanelLayout.columnWeights = new double[] { 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
+		contentPanelLayout.columnWidths = new int[] { 7, 7, 7, 7, 7, 7, 7 };
+		contentPanel.setLayout(contentPanelLayout);
+
+		saveButton = new JButton("Save");
+		contentPanel.add(saveButton, new GridBagConstraints(3, 4, 1, 1, 0.0,
+				0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		saveButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				SwingWorker<Movie, Long> encoder = new SwingWorker<Movie, Long>() {
+
+					@Override
+					protected Movie doInBackground() throws Exception {
+						ProgressMonitor progressMonitor = new ProgressMonitor(MovieEditorDialog.this, "Saving movie", "Composing movie", 0, frameEntries.length);
+
+						List<Frame> newFrames = new ArrayList<Frame>();
+						
+						if (splashIndex!=-1) {
+							newFrames.add(new FrameImpl(
+									Collections.singletonList((Shape) new ShapeImpl(new Point(0,0), new ImageImpl(new MappedImage(frameEntries[splashIndex].getImage(), imageFormat, null), true))) , 
+									frameEntries[splashIndex].frame.getMousePointer(), 
+									frameEntries[splashIndex].frame.getSize(), 
+									false));
+						}
+						
+						File newAudio = hasAudio ? File.createTempFile("jCaptureAudioSink", ".wav") : null;
+												
+						long numFrames=0;						
+						for (FrameEntry fe: frameEntries) {
+							if (!fe.isDeleted) {
+								numFrames+=fe.audioSamplesInFrame;
+							}
+						}
+						WavFile newWavFile = newAudio==null ? null : WavFile.newWavFile(newAudio, numChannels, numFrames, validBits, sampleRate);						
+						
+						File currentAudio = null;
+						WavFile currentWav = null;
+						
+						for (int i=0; i<frameEntries.length; ++i) {
+							if (frameEntries[i].audioFile!=null) {
+								if (currentWav!=null) {
+									currentWav.close();
+								}
+								if (currentAudio!=null) {
+									if (!currentAudio.delete()) {
+										currentAudio.deleteOnExit();
+									}
+								}
+
+								currentAudio = frameEntries[i].audioFile;
+								currentWav = WavFile.openWavFile(currentAudio);
+							}
+							
+							if (currentWav!=null) {
+								if (normalizeVolumeCheckBox.isSelected()) {
+									double[][] buf = new double[numChannels][frameEntries[i].audioSamplesInFrame];
+									int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame);
+									if (read>0 && !frameEntries[i].isDeleted) {
+										// Normalization
+										for (double[] ch: buf) {
+											for (int j=0; j<ch.length; ++j) {
+												ch[j] = ch[j] * NORMALIZED_LEVEL / maxVolume; 
+											}
+										}
+										newWavFile.writeFrames(buf, read);
+									}
+								} else {
+									long[][] buf = new long[numChannels][frameEntries[i].audioSamplesInFrame];
+									int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame);
+									if (read>0 && !frameEntries[i].isDeleted) {
+										newWavFile.writeFrames(buf, read);
+									}									
+								}
+							}
+							
+							if (frameEntries[i].isDeleted) {
+								if (i<frameEntries.length-1) {
+									((FrameImpl) frameEntries[i+1].frame).merge(frameEntries[i].frame);
+								}
+							} else {
+								newFrames.add(frameEntries[i].frame);
+							}
+							
+							progressMonitor.setProgress(i);
+							if (progressMonitor.isCanceled()) {
+								if (currentWav!=null) {
+									currentWav.close();
+								}
+								if (currentAudio!=null) {
+									if (!currentAudio.delete()) {
+										currentAudio.deleteOnExit();
+									}
+								}
+								if (newWavFile!=null) {
+									newWavFile.close();
+								}
+								if (newAudio!=null) {
+									if (!newAudio.delete()) {
+										newAudio.deleteOnExit();
+									}
+								}
+								return null;								
+							}
+						}
+						
+						if (currentWav!=null) {
+							currentWav.close();
+						}
+						if (currentAudio!=null) {
+							if (!currentAudio.delete()) {
+								currentAudio.deleteOnExit();
+							}
+						}
+						
+						if (newWavFile!=null) {
+							newWavFile.close();
+						}
+						
+						return new Movie(movie.getFrameDimension(), movie.getFramesPerSecond(), Collections.singletonList((Fragment) new FragmentImpl(newFrames, newAudio)), movie);
+					}
+					
+					@Override
+					protected void done() {
+						try {
+							MovieEditorDialog.this.setVisible(false);
+							((RecordingControlsFrame) getOwner()).uploadMovie(get());
+							MovieEditorDialog.this.dispose();
+						} catch (Exception e) {
+							e.printStackTrace();
+							JOptionPane.showMessageDialog(
+									MovieEditorDialog.this, e.toString(),
+									"Error saving recording",
+									JOptionPane.ERROR_MESSAGE);
+							MovieEditorDialog.this.setVisible(false);		
+							MovieEditorDialog.this.getOwner().setVisible(false);
+						}
+					}
+					
+				};
+				
+				encoder.execute();
+			}
+		});
+
+		discardButton = new JButton("Discard");
+		contentPanel.add(discardButton, new GridBagConstraints(5, 4, 1, 1, 0.0,
+				0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0));
+		discardButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				if (JOptionPane.showConfirmDialog(MovieEditorDialog.this, "Are you sure you want to discard the recording?", "Confirm discarding movie", JOptionPane.YES_NO_OPTION)==JOptionPane.YES_OPTION) {
+					MovieEditorDialog.this.setVisible(false);
+					MovieEditorDialog.this.dispose();
+					MovieEditorDialog.this.getOwner().setVisible(false);
+				}
+			}
+		});
+
+		timeLineScrollPane = new JScrollPane();
+		timeLineScrollPane.setPreferredSize(new Dimension(movie.getFrameDimension().width, cellHeight+(hasAudio? 23 + AUDIO_CELL_HEIGHT : 22)));
+		contentPanel.add(timeLineScrollPane, new GridBagConstraints(0, 2, 7, 1,
+				0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+				new Insets(0, 0, 0, 0), 0, 0));
+
+		System.out.println("Loaded "+frameEntries.length+" frames");
+		TableModel timeLineTableModel = new DefaultTableModel(hasAudio ? 2 : 1, frameEntries.length) {
+			
+			@Override
+			public boolean isCellEditable(int row, int column) {
+				return false;
+			}
+		};
+		timeLineTable = new JTable() {
+			
+		    @Override
+			public JToolTip createToolTip() {
+				Point p = getMousePosition();
+
+				// Locate the renderer under the event location
+				int hitColumnIndex = columnAtPoint(p);
+				int hitRowIndex = rowAtPoint(p);
+				
+				if ((hitColumnIndex != -1) && (hitRowIndex != -1)) {
+					try {
+						BufferedImage toolTipImage = frameEntries[hitColumnIndex].getToolTipImage();
+						return new ImageToolTip("Frame "+(hitColumnIndex+1)+", delta "+frameEntries[hitColumnIndex].delta+"%", toolTipImage);
+					} catch (IOException e) {
+						e.printStackTrace();
+					}
+				}
+				return super.createToolTip();
+			}
+			
+		};
+		
+		JPopupMenu popup = new JPopupMenu("Context");
+		
+		popup.addPopupMenuListener(new PopupMenuListener() {
+			
+			@Override
+			public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+				if (playTimera[0]!=null) {
+					playTimera[0].stop();
+				}				
+			}
+			
+			@Override
+			public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+			
+			@Override
+			public void popupMenuCanceled(PopupMenuEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+		});
+		
+		addDeleteFramesMenuItem(popup);				
+		addUndeleteFramesMenuItem(popup);
+		addRemoveInactivityMenuItem(popup);				
+		
+		final JCheckBoxMenuItem splashMenuItem = new JCheckBoxMenuItem("Splash");
+		
+		splashMenuItem.addActionListener(new ActionListener() {
+			
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				splashIndex = splashMenuItem.isSelected() ? focusColumn : -1;
+				timeLineTable.repaint();				
+			}
+		});
+		
+		popup.add(splashMenuItem);
+		
+		popup.addPopupMenuListener(new PopupMenuListener() {
+			
+			@Override
+			public void popupMenuWillBecomeVisible(PopupMenuEvent e) {				
+				splashMenuItem.setSelected(focusColumn!=-1 && focusColumn==splashIndex);
+				
+			}
+			
+			@Override
+			public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+				
+			}
+			
+			@Override
+			public void popupMenuCanceled(PopupMenuEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+		});
+				
+		JMenuItem playMenuItem = new JMenuItem();
+		Action playAction = new AbstractAction("Play") {
+			
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				if (playTimera[0]!=null) {
+					playTimera[0].stop();
+					playTimera[0] = null;
+				}
+								
+				final int range[] = {focusColumn, focusColumn};
+				
+				for (int i=focusColumn; i<frameEntries.length && timeLineTable.isColumnSelected(i); ++i) {
+					range[1] = i;
+				}
+				
+				for (int i=focusColumn; i>=0 && timeLineTable.isColumnSelected(i); --i) {
+					range[0] = i;
+				}
+				
+				if (range[0]==range[1]) {
+					range[1]=frameEntries.length-1;
+				}
+				
+				playingColumn = range[0];				
+				
+				if (hasAudio) {
+					try {
+						backgroundProcessor.execute(new SoundPlayer(range[0], range[1]));
+					} catch (Exception ex) {
+						ex.printStackTrace();
+				    	JOptionPane.showMessageDialog(
+				    			MovieEditorDialog.this,
+								ex.toString(), 
+								"Audio problem",
+								JOptionPane.ERROR_MESSAGE);	
+						
+					}
+				}
+				
+				playTimera[0] = new Timer((int) ((double) 1000/movie.getFramesPerSecond()), new ActionListener() {
+
+					@Override
+					public void actionPerformed(ActionEvent e) {
+						while (frameEntries[playingColumn].isDeleted) {
+							++playingColumn;
+							if (playingColumn>range[1]) {
+								((Timer) e.getSource()).stop();
+								return;
+							}
+						}
+						
+						Rectangle visibleRect = timeLineTable.getVisibleRect();
+						Rectangle playingRect = timeLineTable.getCellRect(0, playingColumn, true);
+						if (!visibleRect.contains(playingRect)) {
+							Rectangle scrollTo = new Rectangle(playingRect.x, playingRect.width, visibleRect.width-1, visibleRect.height-1);
+							timeLineTable.scrollRectToVisible(scrollTo);
+						}
+						
+						frameCanvas.repaint();
+						timeLineTable.repaint();		
+						
+						++playingColumn;
+						if (playingColumn>range[1]) {
+							((Timer) e.getSource()).stop();
+							return;
+						}						
+					}
+					
+					
+				}) {
+					@Override
+					public void stop() {
+						super.stop();
+						playingColumn=-1;
+						timeLineTable.scrollRectToVisible(timeLineTable.getCellRect(0, focusColumn, true));
+						frameCanvas.repaint();
+						timeLineTable.repaint();						
+						playTimera[0] = null;						
+					}
+				};
+				
+				playTimera[0].start();
+				synchronized (playTimera) {
+					playTimera.notifyAll();
+				}
+			}
+		};
+		playMenuItem.setAction(playAction);
+		
+		popup.add(playMenuItem);				
+		
+		timeLineTable.setComponentPopupMenu(popup );
+		
+		timeLineTable.addMouseListener(new MouseAdapter() {
+			
+			@Override
+			public void mouseClicked(MouseEvent e) {
+				if (playTimera[0]!=null) {
+					playTimera[0].stop();
+				}
+				
+				if (e.getClickCount()==2) {
+					int hitColumnIndex = timeLineTable.columnAtPoint(e.getPoint());
+					if (hitColumnIndex!=-1) {
+						frameEntries[hitColumnIndex].isDeleted=!frameEntries[hitColumnIndex].isDeleted;
+						timeLineTable.repaint();
+					}
+				}
+			}
+		});
+			
+		timeLineTable.setToolTipText("Movie timeline");
+		timeLineScrollPane.setViewportView(timeLineTable);
+		timeLineTable.setModel(timeLineTableModel);
+		timeLineTable.setRowHeight(0, cellHeight+timeLineTable.getRowMargin()*2);
+		timeLineTable.setRowHeight(1, AUDIO_CELL_HEIGHT+timeLineTable.getRowMargin()*2);
+		for (int i=0; i<frameEntries.length; ++i) {
+			timeLineTable.getColumnModel().getColumn(i).setPreferredWidth(cellWidth);
+			timeLineTable.setValueAt(frameEntries[i], 0, i);
+			if (hasAudio) {
+				timeLineTable.setValueAt(frameEntries[i], 1, i);
+			}
+		}
+		timeLineTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+		timeLineTable.setTableHeader(null);
+		timeLineTable.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
+		timeLineTable.setColumnSelectionAllowed(true);
+		timeLineTable.setRowSelectionAllowed(false);
+
+		TableCellRenderer renderer = new TableCellRenderer() {
+
+			@Override
+			public Component getTableCellRendererComponent(JTable table, Object value, final boolean isSelected, final boolean hasFocus, int row, int column) {
+				
+				if (hasFocus && column!=focusColumn) {
+					focusColumn = column;
+					frameCanvas.repaint();					
+				}
+				
+				return frameEntries[column].getCellRendererComponent(row, isSelected, hasFocus);
+			}
+		};
+		timeLineTable.setDefaultRenderer(Object.class, renderer);
+
+		frameCanvas = new JPanel() {
+			
+			@Override
+			public void paintComponent(Graphics g) {
+				super.paintComponent(g);
+				Rectangle bounds = getBounds();
+				
+				try {
+					Image image = frameEntries[playingColumn==-1 ? focusColumn : playingColumn].getImage();
+					double xScale = (double) bounds.width/(double) image.getWidth(null);
+					double yScale = (double) bounds.height/(double) image.getHeight(null);
+					double scale = Math.min(xScale, yScale);
+					int scaledWidth = (int) (image.getWidth(null)*scale);
+					int scaledHeight = (int) (image.getHeight(null)*scale);
+					if (g instanceof Graphics2D) {
+				    	((Graphics2D) g).setComposite(AlphaComposite.Src);
+				    	((Graphics2D) g).setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+				    	((Graphics2D) g).setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
+				    	((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
+					}
+					g.drawImage(image, (bounds.width-scaledWidth)/2, (bounds.height-scaledHeight)/2, scaledWidth, scaledHeight, null);
+				} catch (Exception e) {
+					e.printStackTrace();
+					g.clearRect(0, 0, bounds.width, bounds.height);
+					g.drawString(e.toString(), 10, 20);
+				}
+			}
+			
+		};
+		
+		frameCanvas.addMouseListener(new MouseAdapter() {
+			
+			@Override
+			public void mouseClicked(MouseEvent e) {
+				if (playTimera[0]!=null) {
+					playTimera[0].stop();					
+				}				
+			}
+		});
+		
+		frameCanvas.setPreferredSize(movie.getFrameDimension());
+		contentPanel.add(frameCanvas, new GridBagConstraints(0, 0, 7, 1, 0.0,
+				0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+				new Insets(1, 1, 1, 1), 0, 0));
+
+		normalizeVolumeCheckBox = new JCheckBox();
+		contentPanel.add(normalizeVolumeCheckBox, new GridBagConstraints(1, 4,
+				1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
+				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+		normalizeVolumeCheckBox.setText("Normalize volume (+"+Math.round(Math.log10(NORMALIZED_LEVEL/maxVolume)*20)+" dB)");
+
+		timeLineTable.changeSelection(0, 0, false, false);
+		frameCanvas.repaint();
+		
+		pack();	
+		
+	}
+
+	void addUndeleteFramesMenuItem(JPopupMenu popup) {
+		JMenuItem unDeleteFrameMenuItem = new JMenuItem();
+		Action unDeleteFrameAction = new AbstractAction("Undelete frame(s)") {
+			
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				for (int idx: timeLineTable.getSelectedColumns()) {
+					frameEntries[idx].isDeleted = false;
+				}
+				timeLineTable.repaint();
+			}
+		};
+		unDeleteFrameMenuItem.setAction(unDeleteFrameAction);
+		
+		popup.add(unDeleteFrameMenuItem);
+	}
+
+	void addDeleteFramesMenuItem(JPopupMenu popup) {
+		JMenuItem deleteFrameMenuItem = new JMenuItem();
+		Action deleteFrameAction = new AbstractAction("Delete frame(s)") {
+			
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				for (int idx: timeLineTable.getSelectedColumns()) {
+					frameEntries[idx].isDeleted = true;
+				}
+				timeLineTable.repaint();
+			}
+		};
+				
+		deleteFrameMenuItem.setAction(deleteFrameAction);
+		popup.add(deleteFrameMenuItem);
+	}
+	
+	void addRemoveInactivityMenuItem(JPopupMenu popup) {
+		JMenuItem removeInactivityMenuItem = new JMenuItem();
+		Action deleteFrameAction = new AbstractAction("Remove inactivity") {
+			
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				String msg = "Inactivity interval";
+				while (true) {
+					String newVal = JOptionPane.showInputDialog(msg, String.valueOf(inactivityInterval));
+					if (newVal==null) {
+						return;
+					}
+					
+					try {
+						inactivityInterval = Double.parseDouble(newVal);
+						if (inactivityInterval > 0) {
+							break;
+						}
+					} catch (NumberFormatException nfe) {
+						// NOP - loop
+					}
+					msg = "Invalid double value for inactivity interval: "+newVal+", enter valid value";					
+				}
+				int inactivityInFrames = (int) (inactivityInterval*movie.getFramesPerSecond());
+				int lastActivity = -inactivityInFrames-1;
+				for (int idx: timeLineTable.getSelectedColumns()) {
+					if (!frameEntries[idx].isDeleted && frameEntries[idx].frame.isActive()) {
+						lastActivity = idx;
+					} else if (idx - lastActivity > inactivityInFrames && !frameEntries[idx].frame.isActive()) {
+						frameEntries[idx].isDeleted = true;
+					}
+				}
+				timeLineTable.repaint();
+			}
+		};
+				
+		removeInactivityMenuItem.setAction(deleteFrameAction);
+		popup.add(removeInactivityMenuItem);
+	}
+	
+	private class SoundPlayer implements Runnable {
+
+	    private final int BUFFER_SIZE;
+	    private AudioInputStream audioStream;
+	    private SourceDataLine sourceLine;
+		private File audioFile;
+	    
+	    public SoundPlayer(int start, int end) throws Exception {
+	    	
+			audioFile = hasAudio ? File.createTempFile("jCaptureRangeAudio", ".wav") : null;
+			
+			BUFFER_SIZE = (int) ((double) numChannels*sampleRate*validBits/(movie.getFramesPerSecond()*8)); // 1 frame buffer.
+									
+			long numFrames=0;						
+			for (int i = start; i<=end; ++i) {
+				if (!frameEntries[i].isDeleted) {
+					numFrames+=frameEntries[i].audioSamplesInFrame;
+				}
+			}
+			WavFile newWavFile = audioFile==null ? null : WavFile.newWavFile(audioFile, numChannels, numFrames, validBits, sampleRate);						
+			
+			File currentAudio = null;
+			WavFile currentWav = null;
+			
+			for (int i=0; i<=end; ++i) {
+				if (frameEntries[i].audioFile!=null) {
+					if (currentWav!=null) {
+						currentWav.close();
+					}
+
+					currentAudio = frameEntries[i].audioFile;
+					currentWav = WavFile.openWavFile(currentAudio);
+				}
+				
+				if (currentWav!=null) {
+					if (normalizeVolumeCheckBox!=null && normalizeVolumeCheckBox.isSelected()) {
+						double[][] buf = new double[numChannels][frameEntries[i].audioSamplesInFrame];
+						int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame);
+						if (read>0 && i>=start && !frameEntries[i].isDeleted) {
+							// Normalization
+							for (double[] ch: buf) {
+								for (int j=0; j<ch.length; ++j) {
+									ch[j] = ch[j] * NORMALIZED_LEVEL / maxVolume; 
+								}
+							}
+							newWavFile.writeFrames(buf, read);
+						}
+					} else {
+						long[][] buf = new long[numChannels][frameEntries[i].audioSamplesInFrame];
+						int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame);
+						if (read>0 && i>=start && !frameEntries[i].isDeleted) {
+							newWavFile.writeFrames(buf, read);
+						}									
+					}
+				}				
+			}
+			
+			if (currentWav!=null) {
+				currentWav.close();
+			}
+			if (newWavFile!=null) {
+				newWavFile.close();
+			}
+			
+			if (audioFile!=null) {
+	            audioStream = AudioSystem.getAudioInputStream(audioFile);
+		        AudioFormat audioFormat = audioStream.getFormat();
+		        DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
+	            sourceLine = (SourceDataLine) AudioSystem.getLine(info);
+	            sourceLine.open(audioFormat);
+			}
+		}
+	    
+	    @Override
+	    public void run() {
+	    	try {
+		        sourceLine.start();
+		        synchronized (playTimera) {
+			        if (playTimera[0] == null) {
+			        	playTimera.wait(100);
+			        }
+		        }
+		        try {
+			        byte[] buf = new byte[BUFFER_SIZE];
+			        int l;
+			        while (playTimera[0]!=null && (l=audioStream.read(buf))!=-1) {
+			        	sourceLine.write(buf, 0, l);
+			        }
+		        } finally {
+		        	audioStream.close();
+			        sourceLine.drain();
+			        sourceLine.close();
+			        if (!audioFile.delete()) {
+			        	audioFile.deleteOnExit();
+			        }
+		        }
+	    	} catch (Exception e) {
+	    		e.printStackTrace();
+	    	}
+	    }
+	    
+	}
+
+}

+ 321 - 0
main/document/jcapture/src/com/hammurapi/jcapture/RecordingControlsFrame.java

@@ -0,0 +1,321 @@
+package com.hammurapi.jcapture;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.SwingWorker;
+import javax.swing.WindowConstants;
+import javax.swing.border.LineBorder;
+
+import netscape.javascript.JSObject;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+
+public class RecordingControlsFrame extends javax.swing.JFrame {
+	private static final String RESUME_TEXT = "Resume";
+	private static final String PAUSE_TEXT = "Pause";
+	private JButton pauseButton;
+	private JButton cancelButton;
+	private JButton stopButton;
+	private ScreenRecorder screenRecorder;
+	private CaptureFrame captureFrame;
+
+	public RecordingControlsFrame(final CaptureFrame captureFrame, final JFrame[] borderFrames) {
+		super("jCapture recording");		
+		setIconImage(captureFrame.getIconImage());
+		this.captureFrame = captureFrame;
+		
+		setUndecorated(true);
+		setAlwaysOnTop(!getBounds().intersects(captureFrame.getBounds()));
+		
+    	addComponentListener(new ComponentListener() {
+			
+			@Override
+			public void componentShown(ComponentEvent e) {
+				for (JFrame bf: borderFrames) {
+					if (bf!=null) {
+						bf.setVisible(true);
+					}
+				}				
+			}
+			
+			@Override
+			public void componentResized(ComponentEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+			
+			@Override
+			public void componentMoved(ComponentEvent e) {
+				// TODO Auto-generated method stub
+				
+			}
+			
+			@Override
+			public void componentHidden(ComponentEvent e) {
+				for (JFrame bf: borderFrames) {
+					if (bf!=null) {
+						bf.setVisible(false);
+					}
+				}				
+			}
+		});
+				
+		JPanel contentPanel = new JPanel();
+		contentPanel.setBorder(new LineBorder(new java.awt.Color(0, 0, 0), 1, false));
+		getContentPane().add(contentPanel, BorderLayout.CENTER);
+
+		GridBagLayout thisLayout = new GridBagLayout();
+		setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+		thisLayout.rowWeights = new double[] { 0.0, 0.1, 0.0 };
+		thisLayout.rowHeights = new int[] { 7, 7, 7 };
+		thisLayout.columnWeights = new double[] { 0.0, 0.1, 0.0, 0.1, 0.0, 0.1,	0.0 };
+		thisLayout.columnWidths = new int[] { 7, 20, 7, 20, 7, 7, 7 };
+		contentPanel.setLayout(thisLayout);
+
+		pauseButton = new JButton();
+		contentPanel.add(pauseButton, new GridBagConstraints(1, 1, 1, 1, 0.0,
+				0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+				new Insets(0, 0, 0, 0), 0, 0));
+		pauseButton.setText(PAUSE_TEXT);
+		pauseButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				try {
+					if (PAUSE_TEXT.equals(pauseButton.getText())) {
+						screenRecorder.stop();
+						pauseButton.setText(RESUME_TEXT);
+					} else {
+						screenRecorder.start();
+						pauseButton.setText(PAUSE_TEXT);
+					}
+				} catch (Exception e) {
+					e.printStackTrace();
+					JOptionPane.showMessageDialog(RecordingControlsFrame.this,
+							e.toString(), "Error pausing/resuming recording",
+							JOptionPane.ERROR_MESSAGE);
+				}
+			}
+		});
+
+		stopButton = new JButton();
+		contentPanel.add(stopButton, new GridBagConstraints(3, 1, 1, 1, 0.0,
+				0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+				new Insets(0, 0, 0, 0), 0, 0));
+		stopButton.setText("Stop");
+		stopButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				
+				pauseButton.setEnabled(false);
+				stopButton.setEnabled(false);
+				cancelButton.setEnabled(false);
+				
+				SwingWorker<Movie, Long> task = new SwingWorker<Movie, Long>() {
+
+					@Override
+					protected Movie doInBackground() throws Exception {
+						screenRecorder.stop();
+						return screenRecorder.getMovie();
+					}
+
+					@Override
+					protected void done() {
+						try {							
+							final Movie movie = get();
+							if (movie!=null) {
+								if (JOptionPane.showConfirmDialog(RecordingControlsFrame.this, "Would you like to edit the movie before uploading?", "Edit movie?", JOptionPane.YES_NO_OPTION)==JOptionPane.YES_OPTION) {
+									new MovieEditorDialog(
+											RecordingControlsFrame.this, 
+											movie, 
+											captureFrame.getCaptureConfig().getBackgroundProcessor(), 
+											captureFrame.getCaptureConfig().getInactivityInterval(),
+											captureFrame.getCaptureConfig().getImageFormat());									
+								} else {
+									uploadMovie(movie);
+								}
+							} else {
+								JOptionPane.showMessageDialog(
+										RecordingControlsFrame.this,
+										"Recording discarded",
+										"Saving recording",
+										JOptionPane.INFORMATION_MESSAGE);
+								RecordingControlsFrame.this.setVisible(false);
+							}							
+						} catch (Exception e) {
+							e.printStackTrace();
+							JOptionPane.showMessageDialog(
+									RecordingControlsFrame.this, e.toString(),
+									"Error saving recording",
+									JOptionPane.ERROR_MESSAGE);
+							RecordingControlsFrame.this.setVisible(false);
+						}
+					}
+
+				};
+
+				task.execute();
+
+			}
+		});
+
+		cancelButton = new JButton();
+		contentPanel.add(cancelButton, new GridBagConstraints(5, 1, 1, 1, 0.0,
+				0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
+				new Insets(0, 0, 0, 0), 0, 0));
+		cancelButton.setText("Cancel");
+		cancelButton.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent evt) {
+				if (JOptionPane.showConfirmDialog(RecordingControlsFrame.this, "Are you sure you want to discard the recording?", "Confirm discarding movie", JOptionPane.YES_NO_OPTION)==JOptionPane.YES_OPTION) {;
+					try {
+						screenRecorder.stop();
+					} catch (Exception e) {
+						e.printStackTrace();
+						JOptionPane.showMessageDialog(RecordingControlsFrame.this,
+								e.toString(), "Error cancelling recording",
+								JOptionPane.ERROR_MESSAGE);
+					} finally {
+						RecordingControlsFrame.this.setVisible(false);
+						captureFrame.setVisible(true);
+					}
+				}
+			}
+		});
+
+		pack();
+		this.setSize(301, 40);
+		captureFrame.getCaptureConfig().setParentComponent(this);
+		try {
+			screenRecorder = new ScreenRecorder(captureFrame.getCaptureConfig(), captureFrame.getApplet());
+		} catch (Exception e) {
+			e.printStackTrace();
+			JOptionPane.showMessageDialog(this, e.toString(), "Error starting recording", JOptionPane.ERROR_MESSAGE);
+			setVisible(false);
+		}
+
+	}
+	
+	/**
+	 * Asks for file name and uploads the movie.
+	 * @param movie
+	 */
+	void uploadMovie(final Movie movie) {
+		try {
+			if (movie!=null) {
+				
+				final String fileName = JOptionPane.showInputDialog(
+						RecordingControlsFrame.this, 
+						"Upload as", 
+						captureFrame.getApplet().getParameter("pageName")+
+						"-recording-"+
+								captureFrame.getDatePrefix()+
+								"-" + captureFrame.nextCounter() +"."+captureFrame.getCaptureConfig().getEncoder().getFileExtension());
+				
+				if (fileName!=null) {
+					// Uploading
+					SwingWorker<Dimension, Long> task = new SwingWorker<Dimension, Long>() {
+	
+						@Override
+						protected Dimension doInBackground() throws Exception {
+							
+							File savedTo = null;
+							
+							try {		
+								// encode and upload
+								File tmpFile = File.createTempFile("jCaptureMovie", "."+captureFrame.getCaptureConfig().getEncoder().getFileExtension());
+								FileOutputStream out = new FileOutputStream(tmpFile);
+								Dimension dimension = captureFrame.getCaptureConfig().getEncoder().encode(captureFrame.getCaptureConfig(), movie, out);
+								if (dimension==null) {
+									return null;
+								}
+								out.close();
+								savedTo = tmpFile;
+								
+								HttpResponse iResponse = captureFrame.getApplet().post(
+										RecordingControlsFrame.this, 
+										new FileInputStream(tmpFile),
+										tmpFile.length(),
+										fileName, 
+										"application/octet-stream");
+								
+								if (iResponse!=null) {
+									System.out.println("Response status line: "+iResponse.getStatusLine());
+									if (iResponse.getStatusLine().getStatusCode()!=HttpStatus.SC_OK) {
+								    	errorMessage = iResponse.getStatusLine();
+								    	errorTitle = "Error saving recording";
+								    	return null;
+									}
+								}
+								if (!tmpFile.delete()) {
+									tmpFile.deleteOnExit();
+								}
+								return dimension;
+							} catch (Error e) {										
+								errorMessage=e.toString();
+								if (savedTo!=null) {
+									errorMessage=errorMessage + ",\n recording was saved to "+savedTo.getAbsolutePath();
+								}
+								errorTitle = "Upload error";
+								e.printStackTrace();
+								return null;
+							}
+						}
+						
+						private Object errorMessage;
+						private String errorTitle;
+						
+						protected void done() {
+							try {
+								Dimension dimension = get();
+								if (dimension!=null) {
+									JSObject window = JSObject.getWindow(captureFrame.getApplet());
+									String toEval = "insertAtCarret('"+captureFrame.getApplet().getParameter("edid")+"','{{:"+fileName+"?"+dimension.width+"x"+dimension.height+"|}}')";
+									System.out.println("Evaluating: "+toEval);
+									window.eval(toEval);
+								} else {
+							    	JOptionPane.showMessageDialog(
+							    			RecordingControlsFrame.this,
+											errorMessage, 
+											errorTitle,
+											JOptionPane.ERROR_MESSAGE);	
+								}
+							} catch (Exception e) {
+								e.printStackTrace();
+						    	JOptionPane.showMessageDialog(
+						    			RecordingControlsFrame.this,
+										e.toString(), 
+										"Exception",
+										JOptionPane.ERROR_MESSAGE);																			
+							}
+						};
+						
+					};
+					
+					task.execute();
+				}
+			} else {
+				JOptionPane.showMessageDialog(
+						RecordingControlsFrame.this,
+						"Recording discarded",
+						"Saving recording",
+						JOptionPane.INFORMATION_MESSAGE);
+			}		
+		} finally {
+			RecordingControlsFrame.this.setVisible(false);																																						
+		}
+	}
+
+}

+ 221 - 0
main/document/jcapture/src/com/hammurapi/jcapture/Region.java

@@ -0,0 +1,221 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Color;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+
+public class Region extends Rectangle {
+	
+	private static final int TRANSPARENT_COLOR = new Color(0,0,0,0).getRGB();
+	
+	private BufferedImage master;
+	private BufferedImage prev;
+	private int grabRange;
+	private MappedImage image;
+	private boolean transparency;
+
+	public Region(BufferedImage master, String format, FileChannel channel, BufferedImage prev, boolean transparency, int x, int y, int grabRange) {
+		this.master = master;
+		this.format = format;
+		this.channel = channel;
+		this.prev = prev;
+		this.transparency = transparency;
+		this.grabRange = grabRange;		
+		
+		setBounds(x-grabRange, y-grabRange, grabRange*2+1, grabRange*2+1);
+	}
+	
+	/**
+	 * Special case when region covers the whole image.
+	 * @param master
+	 * @param x
+	 * @param y
+	 * @param grabRange
+	 * @throws IOException 
+	 */
+	public Region(MappedImage master) throws IOException {
+		this.image = master;
+		this.grabRange = 0;		
+		imageLocation = new Point(0,0);
+		coversEverything = true;
+		
+		setBounds(0,0,master.getWidth(),master.getHeight());
+		BufferedImage img = master.getImage();
+		for (int sx=0, sw=master.getWidth(); sx<sw; ++sx) {
+			for (int sy=0, sh=master.getHeight(); sy<sh; ++sy) {
+				imageHash^=img.getRGB(sx, sy);
+				Long.rotateRight(imageHash, 1);
+			}
+		}
+	}	
+	
+	private Point imageLocation;
+
+	private String format;
+
+	private FileChannel channel;
+	
+	public void grabImage() throws IOException {
+		if (image==null) {
+			imageLocation = new Point(Math.max(0, x), Math.max(0, y));
+			
+			int imageWidth = width; 			
+			int widthDelta = imageWidth+imageLocation.x - master.getWidth();
+			if (widthDelta>0) {
+				imageWidth-=widthDelta;
+			}
+			
+			int imageHeight = height;
+			int heightDelta = imageHeight+imageLocation.y - master.getHeight();
+			if (heightDelta>0) {
+				imageHeight-=heightDelta;
+			}
+			
+			BufferedImage bImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_ARGB);
+			for (int x=0; x<imageWidth; ++x) {
+				for (int y=0; y<imageHeight; ++y) {					
+					int xt = x + imageLocation.x;
+					int yt = y + imageLocation.y;
+					int newPixel = master.getRGB(xt, yt);
+					int oldPixel = prev.getRGB(xt, yt);
+					int pixelRGB = newPixel==oldPixel && transparency ? TRANSPARENT_COLOR : newPixel;
+					bImage.setRGB(x, y, pixelRGB);
+					imageHash^=pixelRGB;
+					Long.rotateRight(imageHash, 1);
+				}
+			}
+			
+			image = new MappedImage(bImage, format, channel);
+			
+			//For debugging
+//			Graphics2D ssg = image.createGraphics();
+//			ssg.setColor(java.awt.Color.GRAY);
+//			ssg.drawRect(0, 0, image.getWidth()-1, image.getHeight()-1);
+						
+			// Make eligible for garbage collection.
+			master = null;
+			prev = null;
+		}
+	}
+	
+	public MappedImage getImage() {
+		return image;
+	}
+		
+	public Point getImageLocation() {
+		return imageLocation;
+	}
+	
+	/**
+	 * Add point, extend region if added (if it is within grab range). Returns true if point was added
+	 */
+	boolean merge(int x, int y) {
+		if (image!=null) {
+			throw new IllegalStateException("Image already grabbed");
+		}
+		if (contains(x, y)) {			
+			int newMinX = Math.min(x-grabRange, this.x);
+			int newMinY = Math.min(y-grabRange, this.y);
+			int newMaxX = Math.max(x+grabRange, this.x+this.width);
+			int newMaxY = Math.max(y+grabRange, this.y+this.height);
+			setBounds(newMinX, newMinY, newMaxX-newMinX, newMaxY-newMinY);
+			return true;
+		}
+		return false;
+	}
+	
+	/**
+	 * 
+	 * @param region
+	 * @return True if region is within grab range and was merged.
+	 */
+	boolean merge(Region region) {
+		if (region==this) {
+			throw new IllegalArgumentException("Self-merge");
+		}
+		if (image!=null) {
+			throw new IllegalStateException("Image already grabbed");
+		}
+		if (intersects(region)) {
+			int newMinX = Math.min(region.x, this.x);
+			int newMinY = Math.min(region.y, this.y);
+			int newMaxX = Math.max(region.x+region.width, this.x+this.width);
+			int newMaxY = Math.max(region.y+region.height, this.y+this.height);
+			setBounds(newMinX, newMinY, newMaxX-newMinX, newMaxY-newMinY);
+			return true;			
+		}
+		return false;
+	}
+		
+	private Region masterImageRegion;
+	
+	/**
+	 * @return Region with the identical image.
+	 */
+	public Region getMasterImageRegion() {
+		return masterImageRegion;
+	}
+
+	/**
+	 * Sets master region with identical image.
+	 * @param masterImageRegion
+	 */
+	public void setMasterImageRegion(Region masterImageRegion) {
+		this.masterImageRegion = masterImageRegion;
+		while (this.masterImageRegion.getMasterImageRegion()!=null) {
+			this.masterImageRegion = this.masterImageRegion.getMasterImageRegion();
+		}
+	}
+
+	public boolean imageEquals(Region other) throws IOException {
+		if (image==null) {
+			throw new IllegalStateException("Image not grabbed");
+		}
+				
+		MappedImage otherImage = other.getImage();		
+		if (otherImage==null 
+				|| imageHash!=other.imageHash 
+				|| image.getHeight()!=otherImage.getHeight() 
+				|| image.getWidth()!=otherImage.getWidth()) {
+			return false;
+		}
+		
+		BufferedImage bImage = image.getImage();
+		BufferedImage oImage = otherImage.getImage();
+		for (int sx=0, sw=bImage.getWidth(); sx<sw; ++sx) {
+			for (int sy=0, sh=bImage.getHeight(); sy<sh; ++sy) {
+				if (bImage.getRGB(sx, sy)!=oImage.getRGB(sx, sy)) {
+					return false;
+				}
+			}
+		}	
+		return true;
+	}
+	
+	private long imageHash;
+	
+	/**
+	 * Analyzes if this region image is a duplicate of the argument region image and if so sets it as its master. 
+	 * @param sr
+	 * @return true if duplicate.
+	 * @throws IOException 
+	 */
+	public boolean dedup(Region sr) throws IOException {
+		if (imageEquals(sr)) {
+			setMasterImageRegion(sr);
+			image=null;
+			return true;
+		}
+		return false;
+	}
+	
+	public boolean coversEverything() {
+		return coversEverything;
+	}
+	
+	private boolean coversEverything;	
+
+}

+ 312 - 0
main/document/jcapture/src/com/hammurapi/jcapture/ScreenRecorder.java

@@ -0,0 +1,312 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Dimension;
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Future;
+import java.util.zip.DataFormatException;
+
+import javax.sound.sampled.AudioFileFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.TargetDataLine;
+import javax.swing.ProgressMonitor;
+
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.Image;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ShapeContent;
+
+/**
+ * Records screen into SWF movie.
+ * @author Pavel Vlasov
+ *
+ */
+public class ScreenRecorder {
+	
+	private CaptureConfig config;
+	private Closeable imagesFileCloseable;
+	
+	class Fragment {
+		
+		private ScreenShot first;
+		
+		float getActualFps() {
+			return first.getFramesPerSecond(); 
+		}
+		
+		private class AudioRecordingThread extends SafeThread {
+			
+			public AudioRecordingThread() {
+				super("Audio recording thread");
+			}
+
+			@Override
+			protected void runInternal() throws Exception {
+				AudioSystem.write(new AudioInputStream(targetDataLine), AudioFileFormat.Type.WAVE, audioSink);
+			}
+			
+		}
+				
+		private class ScreenCapturingThread  extends SafeThread {
+			
+			public ScreenCapturingThread() {
+				super("Screen capturing thread");
+			}
+
+			@Override
+			protected void runInternal() throws Exception {
+				long start = System.currentTimeMillis();
+				ScreenShot screenShot = null;
+				for (int shot=0; !isDone; ++shot) {
+					
+		       		long toSleep = (shot+1)*frameLength - (System.currentTimeMillis()-start);
+		       		if (toSleep>0) {
+		       			Thread.sleep(toSleep);
+		       		}
+					
+		       		screenShot = config.createScreenShot(screenShot, imagesChannel);
+		       		if (first==null) {
+		       			first = screenShot;
+		       		}
+		       		screenshots.add(config.submit(screenShot));		       		
+		       	}
+				
+				System.out.println("Captured "+screenshots.size()+" screenshots");
+			}
+			
+		}		
+		
+		public Fragment() throws Exception {	        
+			if (targetDataLine!=null) {
+	        	audioSink = File.createTempFile("jCaptureAudioSink", ".wav");           
+				targetDataLine.start();
+				audioRecordingThread = new AudioRecordingThread();
+				audioRecordingThread.start();
+			}
+			
+			screenCapturingThread = new ScreenCapturingThread();			
+			screenCapturingThread.start();
+		}
+		
+		File audioSink;
+		List<Future<ScreenShot>> screenshots = new ArrayList<Future<ScreenShot>>();
+		
+		AudioRecordingThread audioRecordingThread;
+		ScreenCapturingThread screenCapturingThread;
+		
+		volatile boolean isDone;
+
+		void stop() throws Exception {
+			if (targetDataLine!=null) {
+				targetDataLine.stop();
+			}
+			isDone = true;
+			if (audioRecordingThread!=null) {
+				audioRecordingThread.join();
+			}
+			screenCapturingThread.join();
+			if (screenCapturingThread.getException()!=null) {
+				throw screenCapturingThread.getException();
+			}
+			if (audioRecordingThread!=null && audioRecordingThread.getException()!=null) {
+				throw audioRecordingThread.getException();
+			}
+		}
+				
+	}
+	
+	LinkedList<Fragment> fragments = new LinkedList<Fragment>();
+	private FileChannel imagesChannel;
+	
+	public ScreenRecorder(CaptureConfig config, AbstractCaptureApplet applet) throws Exception {
+		this.config = config;
+		final File imagesFile = File.createTempFile("jCaptureImages", ".tmp");
+		imagesFile.deleteOnExit();
+		final RandomAccessFile raf = new RandomAccessFile(imagesFile, "rw");
+		this.imagesChannel = raf.getChannel();
+		
+		imagesFileCloseable = new Closeable() {
+
+			@Override
+			public void close() throws IOException {
+				imagesChannel.close();
+				raf.close();
+				if (!imagesFile.delete()) {
+					imagesFile.deleteOnExit();
+				}				
+			}
+			
+		};
+		
+		applet.addCloseable(imagesFileCloseable);
+        
+        if (config.isSound()) {
+			DataLine.Info info = new DataLine.Info(TargetDataLine.class, config.getAudioFormat());
+			
+			Mixer mixer = null;
+			Mixer firstMixer = null;
+			for (Mixer.Info mi: AudioSystem.getMixerInfo()) {
+				Mixer mx = AudioSystem.getMixer(mi);
+				if (mx.isLineSupported(info)) {
+					if (firstMixer==null) {
+						firstMixer = mx;
+					}
+					if (config.getMixerName()==null || mi.getName().equals(config.getMixerName())) {
+						mixer = mx;
+						break;
+					}
+				}			
+			}
+			
+			if (mixer==null) {
+				mixer = firstMixer;
+			}
+			
+			if (mixer!=null) {
+				targetDataLine = (TargetDataLine) mixer.getLine(info); 
+				targetDataLine.open(config.getAudioFormat());
+			}
+        }
+				
+		frameLength = (long) (1000.0/config.getFramesPerSecond());
+		
+		start();
+	}
+	
+	public synchronized void start() throws Exception {
+        fragments.add(new Fragment());
+	}
+	
+	public void stop() throws Exception {
+		fragments.getLast().stop();
+	}
+	
+	/**
+	 * Recording is discarded if saveTo is null
+	 * @param saveTo
+	 * @return Movie size in pixels or null if saving was cancelled.
+	 * @throws IOException 
+	 * @throws DataFormatException 
+	 */
+	public Movie getMovie() throws Exception {
+		stop();
+		
+		if (targetDataLine!=null) {
+			targetDataLine.close();
+		}
+					
+		int totalWork = 3;
+		for (Fragment f: fragments) {
+			totalWork+=f.screenshots.size()+1;
+		}
+		
+		Map<Region, Image> imageCache = new IdentityHashMap<Region, VideoEncoder.Fragment.Frame.Shape.Image>();
+		
+		Dimension frameDimension = null;
+		
+		ProgressMonitor progressMonitor = new ProgressMonitor(config.getParentComponent(), "Encoding video", "Preparing frames", 0, totalWork+4);
+		try {				
+			int progressCounter = 0;
+	        
+	        //In frames
+	        int inactivityInterval = config.isRemoveInactivity() && !config.isSound() ? (int) (1000.0 * config.getInactivityInterval() / frameLength) : -1;
+	        float fps = -1;
+	        final List<VideoEncoder.Fragment> fragmentCollector = new ArrayList<VideoEncoder.Fragment>();
+	        for (Fragment fragment: fragments) {
+	 			if (progressMonitor.isCanceled()) {
+	 				return null;
+	 			}
+	 			
+		        if (fps<0) {
+		        	fps = config.isSound() ? fragment.getActualFps() : config.getSpeedScale()*fragment.getActualFps();
+		        }
+		        
+		        progressMonitor.setProgress(++progressCounter);
+		        			        
+		        int lastActivity = -1;
+		        List<VideoEncoder.Fragment.Frame> framesCollector = new ArrayList<VideoEncoder.Fragment.Frame>();
+		 		for (Future<ScreenShot> sf: fragment.screenshots) {
+		 			
+		 			if (progressMonitor.isCanceled()) {
+		 				return null;
+		 			}
+		 		
+		 			ScreenShot screenShot = sf.get();
+		 			
+			        if (inactivityInterval<0 || screenShot.isActive() || screenShot.getSecNo()-lastActivity<inactivityInterval) {
+				        List<Shape> frameShapes = new ArrayList<VideoEncoder.Fragment.Frame.Shape>();
+				        for (Region region: screenShot.getRegions()) {					        	
+				        	ShapeContent content;
+				        	if (region.getMasterImageRegion()==null) {
+				        		content = new ShapeImpl.ImageImpl(region.getImage(), region.coversEverything());
+				        		imageCache.put(region, (Image) content);
+				        		if (frameDimension==null && region.coversEverything()) {
+				        			frameDimension = region.getSize();
+				        		}
+				        	} else {
+				        		content = new ShapeImpl.ImageReferenceImpl(imageCache.get(region.getMasterImageRegion()));
+				        	}
+							frameShapes.add(new ShapeImpl(region.getImageLocation(), content));
+				        }
+						framesCollector.add(new FrameImpl(frameShapes, screenShot.getMousePosition(), screenShot.getSize(), screenShot.isActive()));
+			        } else {
+			 			progressMonitor.setProgress(++progressCounter);	// Skipping frame, report progress here.		        	        				        	
+			        }
+			        
+			        if (screenShot.isActive()) {
+			        	lastActivity = screenShot.getSecNo();
+			        }
+
+		 			progressMonitor.setProgress(++progressCounter);			        	        
+		        }	
+		 		
+	 			fragmentCollector.add(new FragmentImpl(Collections.unmodifiableList(framesCollector), fragment.audioSink));
+	        }
+	        		        		        
+			return new Movie(frameDimension, fps, fragmentCollector, imagesFileCloseable);
+		} finally {
+			progressMonitor.close();
+		}		
+	}
+	
+	private static abstract class SafeThread extends Thread {
+		private Exception exception;
+
+		public SafeThread(String name) {
+			super(name);
+		}
+
+		@Override
+		public void run() {
+			try {
+				runInternal();
+			} catch (Exception e) {
+				this.exception = e;
+				e.printStackTrace();
+			}
+		}
+		
+		protected abstract void runInternal() throws Exception;
+		
+		public Exception getException() {
+			return exception;
+		}
+	}
+	
+	long frameLength;
+
+	
+	private TargetDataLine targetDataLine;	
+			
+}

+ 251 - 0
main/document/jcapture/src/com/hammurapi/jcapture/ScreenShot.java

@@ -0,0 +1,251 @@
+package com.hammurapi.jcapture;
+ 
+import java.awt.AlphaComposite;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.concurrent.Callable;
+
+import javax.imageio.ImageIO;
+
+public class ScreenShot implements Callable<ScreenShot> {
+	
+	private final ScreenShot prev;
+	private final int secNo;
+	private ScreenShot next;
+	final private long timeStamp;
+	private int grabRange;
+	private boolean transparency;
+	private MappedImage image;
+	private Point mousePosition;
+	private double scale;
+	private boolean border;
+	private Dimension size;
+	private FileChannel imageChannel;
+	private String imageFormat;
+	
+	public ScreenShot(
+			BufferedImage image, 
+			Point mousePosition, 
+			ScreenShot prev, 
+			long timeStamp, 
+			int grabRange, 
+			boolean transparency, 
+			boolean border, 
+			double scale, 
+			FileChannel imageChannel,
+			String imageFormat) throws IOException {
+		
+		this.image = new MappedImage(image, imageFormat, imageChannel);
+		this.mousePosition = mousePosition;
+		this.prev = prev;
+		if (prev==null) {
+			secNo=0;
+		} else {
+			prev.next = this;
+			secNo = prev.secNo+1;
+		}
+		this.timeStamp = timeStamp;
+		this.grabRange = grabRange;
+		this.transparency = transparency;
+		this.scale = scale;		
+		this.border = border;
+		this.imageChannel = imageChannel;
+		this.imageFormat = imageFormat;
+	}
+	
+	public Point getMousePosition() {
+		return mousePosition;
+	}
+	
+	/**
+	 * Calculates actual FPS.
+	 * @return
+	 */
+	public float getFramesPerSecond() {
+		long start = timeStamp;
+		long end = 0;
+		int length = 0;
+		for (ScreenShot sibling = next; sibling!=null; sibling=sibling.next) {
+			++length;
+			end = sibling.timeStamp;
+		}
+		if (length==0) {
+			return -1; // No way to tell.
+		}
+		return (float) (length * 1000.0)/(end - start);
+	}
+		
+	private List<Region> regions;
+	
+	private long totalPixels;
+	private long differentPixels;
+	
+	public double getDiffLevel() {
+		return (double) differentPixels/(double) totalPixels;
+	}
+	
+	/**
+	 * If images are different more than diffThreshold, then the 
+	 * entire screenshot shall be taken.
+	 */
+	private double diffThreshold = 0.7;
+	
+	/**
+	 * Performs processing and returns self.
+	 * Screenshot is structured as Callable to simplify live processing in a background thread.
+	 */
+	@Override
+	public ScreenShot call() throws Exception {
+		BufferedImage img = image.getImage();
+    	// No petty scaling.
+    	if (scale<0.99 || scale > 1.01) {
+	    	BufferedImage scaled = new BufferedImage((int) (img.getWidth()*scale), (int) (img.getHeight()*scale), img.getType());
+	    	Graphics2D g = scaled.createGraphics();
+	    	g.setComposite(AlphaComposite.Src);
+	    	g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+	    	g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
+	    	g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
+	    	g.drawImage(img, 0, 0, scaled.getWidth(), scaled.getHeight(), null);
+	    	g.dispose();
+	    	img = scaled;
+	    	
+	    	if (mousePosition!=null) {
+	    		mousePosition = new Point((int) (mousePosition.x*scale), (int) (mousePosition.y*scale));
+	    	}
+    	}
+		
+		if (border) {
+			Graphics2D ssg = img.createGraphics();
+			ssg.setColor(java.awt.Color.GRAY);
+			ssg.drawRect(0, 0, img.getWidth()-1, img.getHeight()-1);
+		}
+		
+		size = new Dimension(image.getWidth(), image.getHeight());
+		
+		regions = new ArrayList<Region>();
+		if (prev==null) {
+			regions.add(new Region(image));
+		} else {
+			BufferedImage pimg = prev.image.getImage();
+			for (int x=0, w=img.getWidth(); x<w; ++x) {
+				Y: for (int y=0, h=img.getHeight(); y<h; ++y) {
+					++totalPixels;
+					int newPixel = img.getRGB(x, y);
+					int oldPixel = pimg.getRGB(x, y);
+					if (newPixel!=oldPixel) {
+						++differentPixels;
+						for (Region region: regions) {
+							if (region.merge(x, y)) {
+								continue Y;
+							}
+						}
+						regions.add(new Region(img, imageFormat, imageChannel, pimg, transparency, x, y, grabRange));
+					}
+				}
+			}
+			
+			if (getDiffLevel()>diffThreshold) {
+				regions.clear();
+				regions.add(new Region(image));
+			} else {				
+				// Merging adjacent regions
+				for (int i=0; i<regions.size()-1; ++i) {
+					ListIterator<Region> lit = regions.listIterator(i+1);
+					Region master = regions.get(i);
+					while (lit.hasNext()) {
+						if (master.merge(lit.next())) {
+							lit.remove();
+						}
+					}
+				}
+				
+				for (Region region: regions) {
+					region.grabImage();
+				}
+			}
+	
+			// Eligible for garbage collection
+			if (prev!=null) {
+				prev.image=null;
+			}
+		}
+		
+		// De-dup
+		ListIterator<Region> oit = regions.listIterator();
+		R: while (oit.hasNext()) {
+			Region or = oit.next();
+			
+			if (oit.hasPrevious()) {
+				ListIterator<Region> iit = regions.listIterator(oit.previousIndex());
+				while (iit.hasPrevious()) {
+					if (or.dedup(iit.previous())) {
+						continue R;
+					}
+				}
+			}
+			
+			for (ScreenShot sibling=prev; sibling!=null; sibling=sibling.prev) {
+				for (Region sr: sibling.regions) {
+					if (or.dedup(sr)) {
+						continue R;
+					}
+				}
+			}				
+		}
+		return this;
+	}
+
+	public void dump(File dir, String imageFormat) throws IOException {
+		for (int i=0; i<regions.size(); ++i) {
+			BufferedImage img = regions.get(i).getImage().getImage();
+			if (img!=null) {
+				ImageIO.write(img, imageFormat, new File(dir, "s_"+secNo+"_"+i+"."+imageFormat));
+			}
+		}
+	}
+	
+	public List<Region> getRegions() {
+		return regions;
+	}
+	
+	public int getSecNo() {
+		return secNo;
+	}
+	
+	public boolean isActive() {
+		if (!regions.isEmpty()) {
+			return true;
+		}
+		if (mousePosition==null) {
+			if (prev==null) {
+				return false;
+			}
+			if (prev.getMousePosition()!=null) {
+				return true;
+			}
+			return false;
+		}
+		
+		if (prev==null) {
+			return true;
+		}
+		if (!mousePosition.equals(prev.getMousePosition())) {
+			return true;
+		}
+		return false;
+	}
+	
+	public Dimension getSize() {
+		return size;
+	}
+
+}

+ 75 - 0
main/document/jcapture/src/com/hammurapi/jcapture/ShapeImpl.java

@@ -0,0 +1,75 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Point;
+
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape;
+
+class ShapeImpl implements Shape {
+	
+	static class ImageImpl implements Image {
+		
+		MappedImage image;
+		private boolean coversEverything;		
+		
+		ImageImpl(MappedImage image, boolean coversEverything) {
+			super();
+			this.image = image;
+			this.coversEverything = coversEverything;
+		}
+
+		@Override
+		public boolean coversEverything() {
+			return coversEverything;
+		}
+
+		@Override
+		public MappedImage getImage() {
+			return image;
+		}
+		
+	}
+	
+	static class ImageReferenceImpl implements ImageReference {
+
+		private Image image;
+		
+		ImageReferenceImpl(Image image) {
+			super();
+			if (image==null) {
+				throw new NullPointerException();
+			}
+			this.image = image;
+		}
+
+		@Override
+		public boolean coversEverything() {
+			return image.coversEverything();
+		}
+
+		@Override
+		public Image getImage() {
+			return image;
+		}
+		
+	}
+	
+	private Point location;
+	private ShapeContent content;		
+
+	ShapeImpl(Point location, ShapeContent content) {
+		super();
+		this.location = location;
+		this.content = content;
+	}
+
+	@Override
+	public Point getLocation() {
+		return location;
+	}
+
+	@Override
+	public ShapeContent getContent() {
+		return content;
+	}
+
+}

+ 353 - 0
main/document/jcapture/src/com/hammurapi/jcapture/SwfEncoder.java

@@ -0,0 +1,353 @@
+package com.hammurapi.jcapture;
+
+import java.awt.AlphaComposite;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.imageio.ImageIO;
+import javax.swing.JOptionPane;
+import javax.swing.ProgressMonitor;
+
+import com.flagstone.transform.Background;
+import com.flagstone.transform.DefineTag;
+import com.flagstone.transform.DoAction;
+import com.flagstone.transform.Movie;
+import com.flagstone.transform.MovieHeader;
+import com.flagstone.transform.MovieTag;
+import com.flagstone.transform.Place2;
+import com.flagstone.transform.Remove;
+import com.flagstone.transform.Remove2;
+import com.flagstone.transform.ShowFrame;
+import com.flagstone.transform.action.Action;
+import com.flagstone.transform.action.BasicAction;
+import com.flagstone.transform.coder.Coder;
+import com.flagstone.transform.datatype.Bounds;
+import com.flagstone.transform.datatype.CoordTransform;
+import com.flagstone.transform.datatype.WebPalette;
+import com.flagstone.transform.image.ImageTag;
+import com.flagstone.transform.util.image.ImageDecoder;
+import com.flagstone.transform.util.image.ImageRegistry;
+import com.flagstone.transform.util.image.ImageShape;
+import com.flagstone.transform.util.shape.Canvas;
+import com.flagstone.transform.util.sound.SoundFactory;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.Image;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ImageReference;
+import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ShapeContent;
+
+public class SwfEncoder implements VideoEncoder {
+
+	@Override
+	public String getFileExtension() {
+		return "swf";
+	}
+
+	@Override
+	public String getMimeType() {
+		return "application/x-shockwave-flash";
+	}
+	
+	@Override
+	public String toString() {
+		return "SWF";
+	}
+
+	@Override
+	public Dimension encode(Config config, 
+		com.hammurapi.jcapture.Movie source,
+		OutputStream out)
+		throws Exception {
+
+        AtomicInteger uid = new AtomicInteger();
+        
+        /**
+         * For reusing shape id's.
+         */
+        int maxId = Coder.USHORT_MAX;
+        		        
+        ButtonManager manager = null;
+        if (config.isToolBar()) {
+	    	manager = new ButtonManager();
+	    	manager.loadLibrary(getClass().getResource("toolbar_buttons.swf"));				    			        
+        	uid.set(manager.maxIdentifier()+1);
+        }
+        
+        Canvas path = new Canvas();
+        path.setPixels(false);
+        
+        int minImgLayer = 10;
+        int imgLayer = minImgLayer;
+        int maxImgLayer = maxId - 1000;
+        
+        int mouseLayer = maxImgLayer+1;
+        int mouseUid = -1;
+    	Place2 mousePlace = null;		       
+    	ImageTag mouseImage = null;
+    	
+        int layer = maxImgLayer+2;        	
+        
+        int totalWork = 0; 
+        for (Fragment frg: source.getFragments()) {
+        	totalWork = frg.getFrames().size()+1;
+        }
+        
+        ProgressMonitor progressMonitor = new ProgressMonitor(config.getParentComponent(), "Encoding to SWF", "Composing movie", 0, totalWork);
+        int progressCounter = 0;
+	        
+        progressMonitor.setNote("Composing movie");
+        boolean firstFrame = true;
+        Dimension ret = null;
+        Map<Image, ImageTag> imageCache = new IdentityHashMap<Image, ImageTag>();
+        	        
+        Movie movie = new Movie();
+        
+        Point prevMouseLocation = null;
+        
+        int frameNo = 0;
+        
+        for (Fragment fragment: source.getFragments()) {
+		      	        	
+	        SoundFactory soundFactory = null;
+	        boolean soundHeaderAdded = false;
+	        File audio = fragment.getAudio();	        
+	        if (audio!=null) {
+		        progressMonitor.setNote("Loading sound");
+		        soundFactory = new SoundFactory();
+		        
+		 		// MP3 conversion
+		 		if (config.getMp3command()!=null && config.getMp3command().trim().length()>0) {
+			 		audio = new File(audio.getAbsolutePath()+".mp3"); 
+			 		Runtime runtime = Runtime.getRuntime();				 		 
+				    Process proc = runtime.exec(MessageFormat.format(config.getMp3command(), new Object[] {fragment.getAudio().getAbsolutePath(), audio.getAbsolutePath()}));
+				    proc.waitFor();
+				    if (!fragment.getAudio().delete()) {
+				    	fragment.getAudio().deleteOnExit();
+					}
+		 		}
+		        
+		        soundFactory.read(audio);
+	        }
+		        
+		    progressMonitor.setProgress(++progressCounter);
+		    
+ 			if (progressMonitor.isCanceled()) {
+ 				return null;
+ 			}
+ 			
+		    for (Frame frame: fragment.getFrames()) {
+	 			if (progressMonitor.isCanceled()) {
+	 				return null;
+	 			}
+	 			
+	 			boolean addStop = false;
+	 			
+	 			++frameNo;
+		        		
+		        if (firstFrame) {
+		        	firstFrame = false;
+		        	
+		            MovieHeader header = new MovieHeader();
+		            header.setCompressed(true);
+		            header.setFrameRate(source.getFramesPerSecond());	            
+		            
+		        	int toolbarHeight = 29 * 20;
+		        	int toolbarWidth = 495 * 20;
+
+		        	int toolbarX = 0; // - image.getWidth()*20/2;
+		        	int toolbarY = frame.getSize().height*20;		            
+		            
+					int movieWidth = frame.getSize().width*20;
+					int movieHeight = frame.getSize().height*20;
+					if (config.isToolBar()) {
+						movieHeight+=toolbarHeight;
+					}
+					
+					ret = new Dimension(movieWidth/20, movieHeight/20);
+					
+		        	float toolbarScaleX = (float) movieWidth / (float) toolbarWidth;
+		        	float toolbarScaleY = 1.0f;
+		        	
+					Bounds movieBounds = new Bounds(0, 0, movieWidth, movieHeight);
+					header.setFrameSize(movieBounds);
+		            movie.add(header);
+		            movie.add(new Background(WebPalette.WHITE.color()));
+		            
+		            if (config.isToolBar()) {	            
+			        	// Add all the shapes etc used for buttons
+			        	List<DefineTag> toolbarDefinitions = manager.getDefinitions();
+						movie.getObjects().addAll(toolbarDefinitions);
+			        			            
+			        	Place2 placeBackground = manager.getButton("background", layer++, 0, 0);
+			        	placeBackground.setTransform(new CoordTransform(toolbarScaleX, toolbarScaleY, 0, 0, toolbarX, toolbarY));
+		
+			        	// Get the button to use and give its position
+			        	movie.add(placeBackground);
+			        	movie.add(manager.getButton("play_button", layer++, toolbarX + 500, toolbarY + toolbarHeight / 2));
+			        	movie.add(manager.getButton("progress_bar", layer++, toolbarX + 1000, toolbarY + toolbarHeight / 2));
+			        	movie.add(manager.getButton("volume_control", layer++, toolbarX + 5600, toolbarY + toolbarHeight / 2));
+			        	
+			        	if (!config.isPlay()) {
+			        		addStop = true;
+			        	}
+		            }
+		        }
+		        
+		        if (!soundHeaderAdded && soundFactory!=null) {
+		            movie.add(soundFactory.streamHeader(source.getFramesPerSecond()));
+		            soundHeaderAdded = true;
+		        }
+			        
+		        if (soundFactory!=null) {
+			        MovieTag soundBlock = soundFactory.streamSound();
+			        if (soundBlock != null) {
+			            movie.add(soundBlock);
+			        }
+		        }
+			        
+		 		for (Shape shape: frame.getShapes()) {
+		 			if (shape.getContent().coversEverything() || imgLayer==maxImgLayer) {
+		 				for (int i=minImgLayer; i<=imgLayer; ++i) {
+		 					movie.add(new Remove2(i));
+		 				}
+		 				imgLayer = minImgLayer;
+		 			}
+		 			
+		 			ShapeContent shapeContent = shape.getContent();
+		 			Image image;
+		 			if (shapeContent instanceof Image) {
+		 				image = (Image) shapeContent;
+		 			} else if (shape.getContent() instanceof ImageReference) {
+		 				image = ((ImageReference) shapeContent).getImage();
+		 			} else {
+		 				throw new IllegalArgumentException("Unexpected content type: "+shapeContent);
+		 			}
+		 			
+		 			ImageTag imageTag = imageCache.get(image);
+		 			if (imageTag==null) {
+		 				try {
+				            ImageDecoder decoder = ImageRegistry.getImageProvider("image/"+config.getImageFormat().toLowerCase());           
+				            decoder.read(new ByteArrayInputStream(image.getImage().getImageBytes()));
+				            imageTag = decoder.defineImage(uid.incrementAndGet());
+				            imageCache.put(image, imageTag);
+				            movie.add(imageTag);
+		 				} catch (Exception e) {
+		 					// Doing our best to create movie, even with flaws.
+		 					System.err.println("Error encoding image at frame "+frameNo+": "+e);
+		 					e.printStackTrace();
+		 					if (JOptionPane.showConfirmDialog(config.getParentComponent(),
+		 							"Error encoding image ("+image.getImage().getWidth()+"*"+image.getImage().getHeight()+") at frame "+frameNo+": "+e+". Continue encoding?", 
+		 							"Encoding error",
+	                                JOptionPane.YES_NO_OPTION,
+	                                JOptionPane.ERROR_MESSAGE)==JOptionPane.NO_OPTION) {
+		 						throw e;
+		 					}
+		 				}
+		 			}
+		 			
+		 			int shapeId = uid.incrementAndGet();
+			        DefineTag shapeTag = new ImageShape().defineShape(shapeId, imageTag);	
+			        Place2 place = Place2.show(shapeTag.getIdentifier(), imgLayer++, shape.getLocation().x*20, shape.getLocation().y*20);
+		            movie.add(shapeTag);
+		            movie.add(place);			 			
+		 		}
+		    						        
+		        Point mouseLocation = frame.getMousePointer();
+		        if (mouseLocation!=null) {
+	        		if (mouseImage==null) {
+	        			BufferedImage mouseBi = ImageIO.read(getClass().getResource("mouse.png"));
+	        	    	if (config.getScreenScale()<0.99 || config.getScreenScale() > 1.01) {
+	        		    	BufferedImage scaled = new BufferedImage((int) (mouseBi.getWidth()*config.getScreenScale()), (int) (mouseBi.getHeight()*config.getScreenScale()), mouseBi.getType());
+	        		    	Graphics2D g = scaled.createGraphics();
+	        		    	g.setComposite(AlphaComposite.Src);
+	        		    	g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+	        		    	g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
+	        		    	g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
+	        		    	g.drawImage(mouseBi, 0, 0, scaled.getWidth(), scaled.getHeight(), null);
+	        		    	g.dispose();
+	        		    	mouseBi = scaled;
+	        	    	}
+
+	    				ByteArrayOutputStream baos = new ByteArrayOutputStream();        	
+	    				ImageIO.write(mouseBi, "PNG", baos);
+	    	        	baos.close();
+	    	            ImageDecoder decoder = ImageRegistry.getImageProvider("image/png");           
+	    	            decoder.read(new ByteArrayInputStream(baos.toByteArray()));
+		            	mouseImage = decoder.defineImage(uid.incrementAndGet());
+		            	movie.add(mouseImage);
+	        		}
+		                
+	        		if (!mouseLocation.equals(prevMouseLocation)) {
+	        			prevMouseLocation = mouseLocation;
+			            mouseUid = uid.incrementAndGet();
+				        DefineTag mShape = new ImageShape().defineShape(uid.incrementAndGet(), mouseImage); //createRect(mouseUid, 100, 100, WebPalette.RED.color());		
+				        if (mousePlace==null) {
+				        	mousePlace = Place2.show(mShape.getIdentifier(), mouseLayer, mouseLocation.x*20, mouseLocation.y*20);
+				        } else {
+				        	mousePlace = Place2.replace(mShape.getIdentifier(), mouseLayer, mouseLocation.x*20, mouseLocation.y*20);
+				        }
+			            movie.add(mShape);
+			            movie.add(mousePlace);
+	        		}
+	        	} else if (mouseUid!=-1) {
+	        		Remove remove = new Remove(mouseUid, mouseLayer);
+	        		movie.add(remove);
+	        	}
+			     
+		        if (addStop) {
+		        	DoAction cmd = new DoAction(new ArrayList<Action>());
+		        	cmd.add(BasicAction.STOP);
+		        	movie.add(cmd);
+		        }
+	        	movie.add(ShowFrame.getInstance());
+			        
+	 			progressMonitor.setProgress(++progressCounter);			        	        
+		    }
+
+	 		progressMonitor.setProgress(++progressCounter);
+	        if (soundFactory!=null) {
+		 		progressMonitor.setNote("Recording trailing sound");
+		        MovieTag block;
+		        while ((block = soundFactory.streamSound()) != null) {
+		            movie.add(block);
+		            movie.add(ShowFrame.getInstance());
+		        }
+	        }
+	        
+	        if (audio!=null) {
+	        	if (!audio.delete()) {
+	        		audio.deleteOnExit();
+	        	}
+	        }
+        }
+					        
+        if (!config.isLoop()) {
+        	List<Action> actions = new ArrayList<Action>();
+        	actions.add(BasicAction.STOP);
+        	actions.add(BasicAction.END);
+			DoAction doAction = new DoAction(actions);	        	
+			movie.add(doAction);
+            movie.add(ShowFrame.getInstance());
+        }
+        
+        progressMonitor.setProgress(++progressCounter);
+        progressMonitor.setNote("Encoding movie");
+		movie.encodeToStream(out);
+		source.close();
+		return ret;
+	}
+
+}

+ 17 - 0
main/document/jcapture/src/com/hammurapi/jcapture/Translucener.java

@@ -0,0 +1,17 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Frame;
+
+abstract class Translucener {
+	
+	protected abstract void makeTranslucent(Frame frame);
+	
+	static void makeFrameTranslucent(Frame frame) throws Exception {
+		String jVersion = System.getProperty("java.version");
+		if (jVersion==null || "1.6".equals(jVersion) || jVersion.startsWith("1.6.")) {
+			((Translucener) Class.forName("com.hammurapi.jcapture.AWTUtilitiesTranslucener").newInstance()).makeTranslucent(frame);
+		} else {
+			((Translucener) Class.forName("com.hammurapi.jcapture.GraphicsDeviceTranslucener").newInstance()).makeTranslucent(frame);			
+		}
+	}
+}

+ 164 - 0
main/document/jcapture/src/com/hammurapi/jcapture/VideoEncoder.java

@@ -0,0 +1,164 @@
+package com.hammurapi.jcapture;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.io.File;
+import java.io.OutputStream;
+import java.util.List;
+ 
+/**
+ * This is a service interface to be implemented by video encoders. 
+ * jCapture discovers encoders using java.util.ServiceLoader.
+ * 
+ * All interfaces used by this interface are defined as nested for easy reference.
+ * @author Pavel
+ *
+ */
+public interface VideoEncoder {
+	
+	interface Config {
+
+		/**
+		 * 
+		 * @return true if encoder shall add a toolbar to the movie.
+		 */
+		boolean isToolBar();
+		
+		/**
+		 * 
+		 * @return true if movie shall be played in a loop.
+		 */
+		boolean isLoop();
+		
+		/**
+		 * 
+		 * @return true if movie shall start playing after downloading.
+		 */
+		boolean isPlay();
+		
+		/**
+		 * @return For scaling mouse pointer.
+		 */
+		double getScreenScale();
+		
+		/**
+		 * For progress monitor.
+		 * @return
+		 */
+		Component getParentComponent();
+		
+		String getImageFormat();
+
+		/**
+		 * @return OS command to convert WAV to MP3 if encoder requires/benefits from it.
+		 */
+		String getMp3command();
+	}
+	
+	/**
+	 * Movie fragment is a collection of frames with associated audio.
+	 * @author Pavel
+	 *
+	 */
+	interface Fragment {
+		
+		/**
+		 * Frame contains zero or more shapes and mouse location.
+		 * @author Pavel
+		 *
+		 */
+		interface Frame {
+			
+			boolean isActive();
+			
+			/**
+			 * Image shape to be placed on the screen.
+			 * @author Pavel
+			 *
+			 */
+			interface Shape {
+				
+				/**
+				 * Base interface for shape content.
+				 * @author Pavel
+				 *
+				 */
+				interface ShapeContent {
+					
+					/**
+					 * @return true if this shape covers the entire screen area.
+					 */
+					boolean coversEverything();
+				}
+				
+				interface Image extends ShapeContent {
+					
+					MappedImage getImage();
+					
+				}
+
+				/**
+				 * References already defined image.
+				 * @author Pavel
+				 *
+				 */
+				interface ImageReference extends ShapeContent {
+					
+					Image getImage();
+					
+				}
+												
+				Point getLocation();
+				
+				ShapeContent getContent();
+				
+			}
+			
+			/**
+			 * Frame's shapes.
+			 * @return
+			 */
+			List<Shape> getShapes();
+			
+			Point getMousePointer();
+			
+			Dimension getSize();
+			
+		}
+		
+		/**
+		 * Fragment frames.
+		 * @return
+		 */
+		List<Frame> getFrames();
+		
+		/**
+		 * Audio file (WAV).
+		 * @return
+		 */
+		File getAudio();
+		
+	}
+	
+	String getFileExtension();
+	
+	String getMimeType();
+	
+	/**
+	 * This method shall return output format name, e.g. SWF.
+	 * @return
+	 */
+	String toString();
+	
+	/**
+	 * Encodes video to output stream.
+	 * @param fragments Fragments to encode
+	 * @param out Output stream
+	 * @param progressMonitor Progress monitor has work allocated for each frame plus one unit of work per fragment for sound decoding plus one unit for final encoding.
+	 * @param progressCounter current progress counter position.
+	 * @return movie size or null if operation was cancelled
+	 */
+	Dimension encode(Config config, Movie movie, OutputStream out) throws Exception;
+
+}

+ 787 - 0
main/document/jcapture/src/com/hammurapi/jcapture/WavFile.java

@@ -0,0 +1,787 @@
+package com.hammurapi.jcapture;
+
+// This file was taken from http://www.labbookpages.co.uk/audio/javaWavFiles.html
+
+// Wav file IO class
+// A.Greensted
+// http://www.labbookpages.co.uk
+
+// File format is based on the information from
+// http://www.sonicspot.com/guide/wavefiles.html
+// http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/wave.htm
+
+// Version 1.0
+
+import java.io.*;
+
+public class WavFile {
+	private enum IOState {
+		READING, WRITING, CLOSED
+	};
+
+	private final static int BUFFER_SIZE = 4096;
+
+	private final static int FMT_CHUNK_ID = 0x20746D66;
+	private final static int DATA_CHUNK_ID = 0x61746164;
+	private final static int RIFF_CHUNK_ID = 0x46464952;
+	private final static int RIFF_TYPE_ID = 0x45564157;
+
+	private File file; // File that will be read from or written to
+	private IOState ioState; // Specifies the IO State of the Wav File (used for
+								// snaity checking)
+	private int bytesPerSample; // Number of bytes required to store a single
+								// sample
+	private long numFrames; // Number of frames within the data section
+	private FileOutputStream oStream; // Output stream used for writting data
+	private FileInputStream iStream; // Input stream used for reading data
+	private double floatScale; // Scaling factor used for int <-> float
+								// conversion
+	private double floatOffset; // Offset factor used for int <-> float
+								// conversion
+	private boolean wordAlignAdjust; // Specify if an extra byte at the end of
+										// the data chunk is required for word
+										// alignment
+
+	// Wav Header
+	private int numChannels; // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535)
+	private long sampleRate; // 4 bytes unsigned, 0x00000001 (1) to 0xFFFFFFFF
+								// (4,294,967,295)
+								// Although a java int is 4 bytes, it is signed,
+								// so need to use a long
+	private int blockAlign; // 2 bytes unsigned, 0x0001 (1) to 0xFFFF (65,535)
+	private int validBits; // 2 bytes unsigned, 0x0002 (2) to 0xFFFF (65,535)
+
+	// Buffering
+	private byte[] buffer; // Local buffer used for IO
+	private int bufferPointer; // Points to the current position in local buffer
+	private int bytesRead; // Bytes read after last read into local buffer
+	private long frameCounter; // Current number of frames read or written
+
+	// Cannot instantiate WavFile directly, must either use newWavFile() or
+	// openWavFile()
+	private WavFile() {
+		buffer = new byte[BUFFER_SIZE];
+	}
+
+	public int getNumChannels() {
+		return numChannels;
+	}
+
+	public long getNumFrames() {
+		return numFrames;
+	}
+
+	public long getFramesRemaining() {
+		return numFrames - frameCounter;
+	}
+
+	public long getSampleRate() {
+		return sampleRate;
+	}
+
+	public int getValidBits() {
+		return validBits;
+	}
+
+	public static WavFile newWavFile(File file, int numChannels,
+			long numFrames, int validBits, long sampleRate) throws IOException,
+			WavFileException {
+		// Instantiate new Wavfile and initialise
+		WavFile wavFile = new WavFile();
+		wavFile.file = file;
+		wavFile.numChannels = numChannels;
+		wavFile.numFrames = numFrames;
+		wavFile.sampleRate = sampleRate;
+		wavFile.bytesPerSample = (validBits + 7) / 8;
+		wavFile.blockAlign = wavFile.bytesPerSample * numChannels;
+		wavFile.validBits = validBits;
+
+		// Sanity check arguments
+		if (numChannels < 1 || numChannels > 65535)
+			throw new WavFileException(
+					"Illegal number of channels, valid range 1 to 65536");
+		if (numFrames < 0)
+			throw new WavFileException("Number of frames must be positive");
+		if (validBits < 2 || validBits > 65535)
+			throw new WavFileException(
+					"Illegal number of valid bits, valid range 2 to 65536");
+		if (sampleRate < 0)
+			throw new WavFileException("Sample rate must be positive");
+
+		// Create output stream for writing data
+		wavFile.oStream = new FileOutputStream(file);
+
+		// Calculate the chunk sizes
+		long dataChunkSize = wavFile.blockAlign * numFrames;
+		long mainChunkSize = 4 + // Riff Type
+				8 + // Format ID and size
+				16 + // Format data
+				8 + // Data ID and size
+				dataChunkSize;
+
+		// Chunks must be word aligned, so if odd number of audio data bytes
+		// adjust the main chunk size
+		if (dataChunkSize % 2 == 1) {
+			mainChunkSize += 1;
+			wavFile.wordAlignAdjust = true;
+		} else {
+			wavFile.wordAlignAdjust = false;
+		}
+
+		// Set the main chunk size
+		putLE(RIFF_CHUNK_ID, wavFile.buffer, 0, 4);
+		putLE(mainChunkSize, wavFile.buffer, 4, 4);
+		putLE(RIFF_TYPE_ID, wavFile.buffer, 8, 4);
+
+		// Write out the header
+		wavFile.oStream.write(wavFile.buffer, 0, 12);
+
+		// Put format data in buffer
+		long averageBytesPerSecond = sampleRate * wavFile.blockAlign;
+
+		putLE(FMT_CHUNK_ID, wavFile.buffer, 0, 4); // Chunk ID
+		putLE(16, wavFile.buffer, 4, 4); // Chunk Data Size
+		putLE(1, wavFile.buffer, 8, 2); // Compression Code (Uncompressed)
+		putLE(numChannels, wavFile.buffer, 10, 2); // Number of channels
+		putLE(sampleRate, wavFile.buffer, 12, 4); // Sample Rate
+		putLE(averageBytesPerSecond, wavFile.buffer, 16, 4); // Average Bytes
+																// Per Second
+		putLE(wavFile.blockAlign, wavFile.buffer, 20, 2); // Block Align
+		putLE(validBits, wavFile.buffer, 22, 2); // Valid Bits
+
+		// Write Format Chunk
+		wavFile.oStream.write(wavFile.buffer, 0, 24);
+
+		// Start Data Chunk
+		putLE(DATA_CHUNK_ID, wavFile.buffer, 0, 4); // Chunk ID
+		putLE(dataChunkSize, wavFile.buffer, 4, 4); // Chunk Data Size
+
+		// Write Format Chunk
+		wavFile.oStream.write(wavFile.buffer, 0, 8);
+
+		// Calculate the scaling factor for converting to a normalised double
+		if (wavFile.validBits > 8) {
+			// If more than 8 validBits, data is signed
+			// Conversion required multiplying by magnitude of max positive
+			// value
+			wavFile.floatOffset = 0;
+			wavFile.floatScale = Long.MAX_VALUE >> (64 - wavFile.validBits);
+		} else {
+			// Else if 8 or less validBits, data is unsigned
+			// Conversion required dividing by max positive value
+			wavFile.floatOffset = 1;
+			wavFile.floatScale = 0.5 * ((1 << wavFile.validBits) - 1);
+		}
+
+		// Finally, set the IO State
+		wavFile.bufferPointer = 0;
+		wavFile.bytesRead = 0;
+		wavFile.frameCounter = 0;
+		wavFile.ioState = IOState.WRITING;
+
+		return wavFile;
+	}
+
+	public static WavFile openWavFile(File file) throws IOException,
+			WavFileException {
+		// Instantiate new Wavfile and store the file reference
+		WavFile wavFile = new WavFile();
+		wavFile.file = file;
+
+		// Create a new file input stream for reading file data
+		wavFile.iStream = new FileInputStream(file);
+
+		// Read the first 12 bytes of the file
+		int bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 12);
+		if (bytesRead != 12)
+			throw new WavFileException("Not enough wav file bytes for header");
+
+		// Extract parts from the header
+		long riffChunkID = getLE(wavFile.buffer, 0, 4);
+		long chunkSize = getLE(wavFile.buffer, 4, 4);
+		long riffTypeID = getLE(wavFile.buffer, 8, 4);
+
+		// Check the header bytes contains the correct signature
+		if (riffChunkID != RIFF_CHUNK_ID)
+			throw new WavFileException(
+					"Invalid Wav Header data, incorrect riff chunk ID");
+		if (riffTypeID != RIFF_TYPE_ID)
+			throw new WavFileException(
+					"Invalid Wav Header data, incorrect riff type ID");
+
+		// Check that the file size matches the number of bytes listed in header
+		if (file.length() != chunkSize + 8) {
+			throw new WavFileException("Header chunk size (" + chunkSize
+					+ ") does not match file size (" + file.length() + ")");
+		}
+
+		boolean foundFormat = false;
+		boolean foundData = false;
+
+		// Search for the Format and Data Chunks
+		while (true) {
+			// Read the first 8 bytes of the chunk (ID and chunk size)
+			bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 8);
+			if (bytesRead == -1)
+				throw new WavFileException(
+						"Reached end of file without finding format chunk");
+			if (bytesRead != 8)
+				throw new WavFileException("Could not read chunk header");
+
+			// Extract the chunk ID and Size
+			long chunkID = getLE(wavFile.buffer, 0, 4);
+			chunkSize = getLE(wavFile.buffer, 4, 4);
+
+			// Word align the chunk size
+			// chunkSize specifies the number of bytes holding data. However,
+			// the data should be word aligned (2 bytes) so we need to calculate
+			// the actual number of bytes in the chunk
+			long numChunkBytes = (chunkSize % 2 == 1) ? chunkSize + 1
+					: chunkSize;
+
+			if (chunkID == FMT_CHUNK_ID) {
+				// Flag that the format chunk has been found
+				foundFormat = true;
+
+				// Read in the header info
+				bytesRead = wavFile.iStream.read(wavFile.buffer, 0, 16);
+
+				// Check this is uncompressed data
+				int compressionCode = (int) getLE(wavFile.buffer, 0, 2);
+				if (compressionCode != 1)
+					throw new WavFileException("Compression Code "
+							+ compressionCode + " not supported");
+
+				// Extract the format information
+				wavFile.numChannels = (int) getLE(wavFile.buffer, 2, 2);
+				wavFile.sampleRate = getLE(wavFile.buffer, 4, 4);
+				wavFile.blockAlign = (int) getLE(wavFile.buffer, 12, 2);
+				wavFile.validBits = (int) getLE(wavFile.buffer, 14, 2);
+
+				if (wavFile.numChannels == 0)
+					throw new WavFileException(
+							"Number of channels specified in header is equal to zero");
+				if (wavFile.blockAlign == 0)
+					throw new WavFileException(
+							"Block Align specified in header is equal to zero");
+				if (wavFile.validBits < 2)
+					throw new WavFileException(
+							"Valid Bits specified in header is less than 2");
+				if (wavFile.validBits > 64)
+					throw new WavFileException(
+							"Valid Bits specified in header is greater than 64, this is greater than a long can hold");
+
+				// Calculate the number of bytes required to hold 1 sample
+				wavFile.bytesPerSample = (wavFile.validBits + 7) / 8;
+				if (wavFile.bytesPerSample * wavFile.numChannels != wavFile.blockAlign)
+					throw new WavFileException(
+							"Block Align does not agree with bytes required for validBits and number of channels");
+
+				// Account for number of format bytes and then skip over
+				// any extra format bytes
+				numChunkBytes -= 16;
+				if (numChunkBytes > 0)
+					wavFile.iStream.skip(numChunkBytes);
+			} else if (chunkID == DATA_CHUNK_ID) {
+				// Check if we've found the format chunk,
+				// If not, throw an exception as we need the format information
+				// before we can read the data chunk
+				if (foundFormat == false)
+					throw new WavFileException(
+							"Data chunk found before Format chunk");
+
+				// Check that the chunkSize (wav data length) is a multiple of
+				// the
+				// block align (bytes per frame)
+				if (chunkSize % wavFile.blockAlign != 0)
+					throw new WavFileException(
+							"Data Chunk size is not multiple of Block Align");
+
+				// Calculate the number of frames
+				wavFile.numFrames = chunkSize / wavFile.blockAlign;
+
+				// Flag that we've found the wave data chunk
+				foundData = true;
+
+				break;
+			} else {
+				// If an unknown chunk ID is found, just skip over the chunk
+				// data
+				wavFile.iStream.skip(numChunkBytes);
+			}
+		}
+
+		// Throw an exception if no data chunk has been found
+		if (foundData == false)
+			throw new WavFileException("Did not find a data chunk");
+
+		// Calculate the scaling factor for converting to a normalised double
+		if (wavFile.validBits > 8) {
+			// If more than 8 validBits, data is signed
+			// Conversion required dividing by magnitude of max negative value
+			wavFile.floatOffset = 0;
+			wavFile.floatScale = 1 << (wavFile.validBits - 1);
+		} else {
+			// Else if 8 or less validBits, data is unsigned
+			// Conversion required dividing by max positive value
+			wavFile.floatOffset = -1;
+			wavFile.floatScale = 0.5 * ((1 << wavFile.validBits) - 1);
+		}
+
+		wavFile.bufferPointer = 0;
+		wavFile.bytesRead = 0;
+		wavFile.frameCounter = 0;
+		wavFile.ioState = IOState.READING;
+
+		return wavFile;
+	}
+
+	// Get and Put little endian data from local buffer
+	// ------------------------------------------------
+	private static long getLE(byte[] buffer, int pos, int numBytes) {
+		numBytes--;
+		pos += numBytes;
+
+		long val = buffer[pos] & 0xFF;
+		for (int b = 0; b < numBytes; b++)
+			val = (val << 8) + (buffer[--pos] & 0xFF);
+
+		return val;
+	}
+
+	private static void putLE(long val, byte[] buffer, int pos, int numBytes) {
+		for (int b = 0; b < numBytes; b++) {
+			buffer[pos] = (byte) (val & 0xFF);
+			val >>= 8;
+			pos++;
+		}
+	}
+
+	// Sample Writing and Reading
+	// --------------------------
+	private void writeSample(long val) throws IOException {
+		for (int b = 0; b < bytesPerSample; b++) {
+			if (bufferPointer == BUFFER_SIZE) {
+				oStream.write(buffer, 0, BUFFER_SIZE);
+				bufferPointer = 0;
+			}
+
+			buffer[bufferPointer] = (byte) (val & 0xFF);
+			val >>= 8;
+			bufferPointer++;
+		}
+	}
+
+	private long readSample() throws IOException, WavFileException {
+		long val = 0;
+
+		for (int b = 0; b < bytesPerSample; b++) {
+			if (bufferPointer == bytesRead) {
+				int read = iStream.read(buffer, 0, BUFFER_SIZE);
+				if (read == -1)
+					throw new WavFileException("Not enough data available");
+				bytesRead = read;
+				bufferPointer = 0;
+			}
+
+			int v = buffer[bufferPointer];
+			if (b < bytesPerSample - 1 || bytesPerSample == 1)
+				v &= 0xFF;
+			val += v << (b * 8);
+
+			bufferPointer++;
+		}
+
+		return val;
+	}
+
+	// Integer
+	// -------
+	public int readFrames(int[] sampleBuffer, int numFramesToRead)
+			throws IOException, WavFileException {
+		return readFrames(sampleBuffer, 0, numFramesToRead);
+	}
+
+	public int readFrames(int[] sampleBuffer, int offset, int numFramesToRead)
+			throws IOException, WavFileException {
+		if (ioState != IOState.READING)
+			throw new IOException("Cannot read from WavFile instance");
+
+		for (int f = 0; f < numFramesToRead; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++) {
+				sampleBuffer[offset] = (int) readSample();
+				offset++;
+			}
+
+			frameCounter++;
+		}
+
+		return numFramesToRead;
+	}
+
+	public int readFrames(int[][] sampleBuffer, int numFramesToRead)
+			throws IOException, WavFileException {
+		return readFrames(sampleBuffer, 0, numFramesToRead);
+	}
+
+	public int readFrames(int[][] sampleBuffer, int offset, int numFramesToRead)
+			throws IOException, WavFileException {
+		if (ioState != IOState.READING)
+			throw new IOException("Cannot read from WavFile instance");
+
+		for (int f = 0; f < numFramesToRead; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++)
+				sampleBuffer[c][offset] = (int) readSample();
+
+			offset++;
+			frameCounter++;
+		}
+
+		return numFramesToRead;
+	}
+
+	public int writeFrames(int[] sampleBuffer, int numFramesToWrite)
+			throws IOException, WavFileException {
+		return writeFrames(sampleBuffer, 0, numFramesToWrite);
+	}
+
+	public int writeFrames(int[] sampleBuffer, int offset, int numFramesToWrite)
+			throws IOException, WavFileException {
+		if (ioState != IOState.WRITING)
+			throw new IOException("Cannot write to WavFile instance");
+
+		for (int f = 0; f < numFramesToWrite; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++) {
+				writeSample(sampleBuffer[offset]);
+				offset++;
+			}
+
+			frameCounter++;
+		}
+
+		return numFramesToWrite;
+	}
+
+	public int writeFrames(int[][] sampleBuffer, int numFramesToWrite)
+			throws IOException, WavFileException {
+		return writeFrames(sampleBuffer, 0, numFramesToWrite);
+	}
+
+	public int writeFrames(int[][] sampleBuffer, int offset,
+			int numFramesToWrite) throws IOException, WavFileException {
+		if (ioState != IOState.WRITING)
+			throw new IOException("Cannot write to WavFile instance");
+
+		for (int f = 0; f < numFramesToWrite; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++)
+				writeSample(sampleBuffer[c][offset]);
+
+			offset++;
+			frameCounter++;
+		}
+
+		return numFramesToWrite;
+	}
+
+	// Long
+	// ----
+	public int readFrames(long[] sampleBuffer, int numFramesToRead)
+			throws IOException, WavFileException {
+		return readFrames(sampleBuffer, 0, numFramesToRead);
+	}
+
+	public int readFrames(long[] sampleBuffer, int offset, int numFramesToRead)
+			throws IOException, WavFileException {
+		if (ioState != IOState.READING)
+			throw new IOException("Cannot read from WavFile instance");
+
+		for (int f = 0; f < numFramesToRead; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++) {
+				sampleBuffer[offset] = readSample();
+				offset++;
+			}
+
+			frameCounter++;
+		}
+
+		return numFramesToRead;
+	}
+
+	public int readFrames(long[][] sampleBuffer, int numFramesToRead)
+			throws IOException, WavFileException {
+		return readFrames(sampleBuffer, 0, numFramesToRead);
+	}
+
+	public int readFrames(long[][] sampleBuffer, int offset, int numFramesToRead)
+			throws IOException, WavFileException {
+		if (ioState != IOState.READING)
+			throw new IOException("Cannot read from WavFile instance");
+
+		for (int f = 0; f < numFramesToRead; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++)
+				sampleBuffer[c][offset] = readSample();
+
+			offset++;
+			frameCounter++;
+		}
+
+		return numFramesToRead;
+	}
+
+	public int writeFrames(long[] sampleBuffer, int numFramesToWrite)
+			throws IOException, WavFileException {
+		return writeFrames(sampleBuffer, 0, numFramesToWrite);
+	}
+
+	public int writeFrames(long[] sampleBuffer, int offset, int numFramesToWrite)
+			throws IOException, WavFileException {
+		if (ioState != IOState.WRITING)
+			throw new IOException("Cannot write to WavFile instance");
+
+		for (int f = 0; f < numFramesToWrite; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++) {
+				writeSample(sampleBuffer[offset]);
+				offset++;
+			}
+
+			frameCounter++;
+		}
+
+		return numFramesToWrite;
+	}
+
+	public int writeFrames(long[][] sampleBuffer, int numFramesToWrite)
+			throws IOException, WavFileException {
+		return writeFrames(sampleBuffer, 0, numFramesToWrite);
+	}
+
+	public int writeFrames(long[][] sampleBuffer, int offset,
+			int numFramesToWrite) throws IOException, WavFileException {
+		if (ioState != IOState.WRITING)
+			throw new IOException("Cannot write to WavFile instance");
+
+		for (int f = 0; f < numFramesToWrite; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++)
+				writeSample(sampleBuffer[c][offset]);
+
+			offset++;
+			frameCounter++;
+		}
+
+		return numFramesToWrite;
+	}
+
+	// Double
+	// ------
+	public int readFrames(double[] sampleBuffer, int numFramesToRead)
+			throws IOException, WavFileException {
+		return readFrames(sampleBuffer, 0, numFramesToRead);
+	}
+
+	public int readFrames(double[] sampleBuffer, int offset, int numFramesToRead)
+			throws IOException, WavFileException {
+		if (ioState != IOState.READING)
+			throw new IOException("Cannot read from WavFile instance");
+
+		for (int f = 0; f < numFramesToRead; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++) {
+				sampleBuffer[offset] = floatOffset + (double) readSample()
+						/ floatScale;
+				offset++;
+			}
+
+			frameCounter++;
+		}
+
+		return numFramesToRead;
+	}
+
+	public int readFrames(double[][] sampleBuffer, int numFramesToRead)
+			throws IOException, WavFileException {
+		return readFrames(sampleBuffer, 0, numFramesToRead);
+	}
+
+	public int readFrames(double[][] sampleBuffer, int offset,
+			int numFramesToRead) throws IOException, WavFileException {
+		if (ioState != IOState.READING)
+			throw new IOException("Cannot read from WavFile instance");
+
+		for (int f = 0; f < numFramesToRead; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++)
+				sampleBuffer[c][offset] = floatOffset + (double) readSample()
+						/ floatScale;
+
+			offset++;
+			frameCounter++;
+		}
+
+		return numFramesToRead;
+	}
+
+	public int writeFrames(double[] sampleBuffer, int numFramesToWrite)
+			throws IOException, WavFileException {
+		return writeFrames(sampleBuffer, 0, numFramesToWrite);
+	}
+
+	public int writeFrames(double[] sampleBuffer, int offset,
+			int numFramesToWrite) throws IOException, WavFileException {
+		if (ioState != IOState.WRITING)
+			throw new IOException("Cannot write to WavFile instance");
+
+		for (int f = 0; f < numFramesToWrite; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++) {
+				writeSample((long) (floatScale * (floatOffset + sampleBuffer[offset])));
+				offset++;
+			}
+
+			frameCounter++;
+		}
+
+		return numFramesToWrite;
+	}
+
+	public int writeFrames(double[][] sampleBuffer, int numFramesToWrite)
+			throws IOException, WavFileException {
+		return writeFrames(sampleBuffer, 0, numFramesToWrite);
+	}
+
+	public int writeFrames(double[][] sampleBuffer, int offset,
+			int numFramesToWrite) throws IOException, WavFileException {
+		if (ioState != IOState.WRITING)
+			throw new IOException("Cannot write to WavFile instance");
+
+		for (int f = 0; f < numFramesToWrite; f++) {
+			if (frameCounter == numFrames)
+				return f;
+
+			for (int c = 0; c < numChannels; c++)
+				writeSample((long) (floatScale * (floatOffset + sampleBuffer[c][offset])));
+
+			offset++;
+			frameCounter++;
+		}
+
+		return numFramesToWrite;
+	}
+
+	public void close() throws IOException {
+		// Close the input stream and set to null
+		if (iStream != null) {
+			iStream.close();
+			iStream = null;
+		}
+
+		if (oStream != null) {
+			// Write out anything still in the local buffer
+			if (bufferPointer > 0)
+				oStream.write(buffer, 0, bufferPointer);
+
+			// If an extra byte is required for word alignment, add it to the
+			// end
+			if (wordAlignAdjust)
+				oStream.write(0);
+
+			// Close the stream and set to null
+			oStream.close();
+			oStream = null;
+		}
+
+		// Flag that the stream is closed
+		ioState = IOState.CLOSED;
+	}
+
+	public void display() {
+		display(System.out);
+	}
+
+	public void display(PrintStream out) {
+		out.printf("File: %s\n", file);
+		out.printf("Channels: %d, Frames: %d\n", numChannels, numFrames);
+		out.printf("IO State: %s\n", ioState);
+		out.printf("Sample Rate: %d, Block Align: %d\n", sampleRate, blockAlign);
+		out.printf("Valid Bits: %d, Bytes per sample: %d\n", validBits,
+				bytesPerSample);
+	}
+
+	public static void main(String[] args) {
+		if (args.length < 1) {
+			System.err.println("Must supply filename");
+			System.exit(1);
+		}
+
+		try {
+			for (String filename : args) {
+				WavFile readWavFile = openWavFile(new File(filename));
+				readWavFile.display();
+
+				long numFrames = readWavFile.getNumFrames();
+				int numChannels = readWavFile.getNumChannels();
+				int validBits = readWavFile.getValidBits();
+				long sampleRate = readWavFile.getSampleRate();
+
+				WavFile writeWavFile = newWavFile(new File("out.wav"),
+						numChannels, numFrames, validBits, sampleRate);
+
+				final int BUF_SIZE = 5001;
+
+				// int[] buffer = new int[BUF_SIZE * numChannels];
+				// long[] buffer = new long[BUF_SIZE * numChannels];
+				double[] buffer = new double[BUF_SIZE * numChannels];
+
+				int framesRead = 0;
+				int framesWritten = 0;
+
+				do {
+					framesRead = readWavFile.readFrames(buffer, BUF_SIZE);
+					framesWritten = writeWavFile.writeFrames(buffer, BUF_SIZE);
+					System.out.printf("%d %d\n", framesRead, framesWritten);
+				} while (framesRead != 0);
+
+				readWavFile.close();
+				writeWavFile.close();
+			}
+
+			WavFile writeWavFile = newWavFile(new File("out2.wav"), 1, 10, 23,
+					44100);
+			double[] buffer = new double[10];
+			writeWavFile.writeFrames(buffer, 10);
+			writeWavFile.close();
+		} catch (Exception e) {
+			System.err.println(e);
+			e.printStackTrace();
+		}
+	}
+}

+ 26 - 0
main/document/jcapture/src/com/hammurapi/jcapture/WavFileException.java

@@ -0,0 +1,26 @@
+package com.hammurapi.jcapture;
+
+//This file was taken from http://www.labbookpages.co.uk/audio/javaWavFiles.html and package declaration was added.
+
+public class WavFileException extends Exception
+{
+	public WavFileException()
+	{
+		super();
+	}
+
+	public WavFileException(String message)
+	{
+		super(message);
+	}
+
+	public WavFileException(String message, Throwable cause)
+	{
+		super(message, cause);
+	}
+
+	public WavFileException(Throwable cause) 
+	{
+		super(cause);
+	}
+}

BIN
main/document/jcapture/src/com/hammurapi/jcapture/camera.png


BIN
main/document/jcapture/src/com/hammurapi/jcapture/mouse.png


BIN
main/document/jcapture/src/com/hammurapi/jcapture/toolbar_buttons.swf


+ 24 - 30
main/document/upload.php

@@ -127,25 +127,25 @@ if (api_get_group_id()) {
 	// Get group info
 	$group_properties = GroupManager::get_group_properties(api_get_group_id());
 
-	if ($is_allowed_to_edit || GroupManager::is_user_in_group($_user['user_id'], api_get_group_id())) { // Only courseadmin or group members allowed
-		$to_group_id = api_get_group_id();
-		$req_gid = '&amp;gidReq='.api_get_group_id();
-		$interbreadcrumb[] = array('url' => '../group/group_space.php?gidReq='.api_get_group_id(), 'name' => get_lang('GroupSpace'));
-	} else {
-		api_not_allowed(true);
-	}
+    if ($is_allowed_to_edit || GroupManager::is_user_in_group($_user['user_id'], api_get_group_id())) { // Only courseadmin or group members allowed
+        $to_group_id = api_get_group_id();
+        $req_gid = '&amp;gidReq='.api_get_group_id();
+        $interbreadcrumb[] = array('url' => '../group/group_space.php?gidReq='.api_get_group_id(), 'name' => get_lang('GroupSpace'));
+    } else {
+        api_not_allowed(true);
+    }
 } elseif ($is_allowed_to_edit || is_my_shared_folder(api_get_user_id(), $path, api_get_session_id())) {
 
     // Admin for "regular" upload, no group documents. And check if is my shared folder
-	$to_group_id = 0;
-	$req_gid = '';
+    $to_group_id = 0;
+    $req_gid = '';
 } else { // No course admin and no group member...
 	api_not_allowed(true);
 }
 
 // Group docs can only be uploaded in the group directory
 if ($to_group_id != 0 && $path == '/') {
-	$path = $group_properties['directory'];
+    $path = $group_properties['directory'];
 }
 
 // I'm in the certification module?
@@ -153,7 +153,7 @@ $is_certificate_mode = false;
 $is_certificate_array = explode('/', $path);
 array_shift($is_certificate_array);
 if ($is_certificate_array[0] == 'certificates') {
-	$is_certificate_mode = true;
+    $is_certificate_mode = true;
 }
 
 // Title of the tool
@@ -161,9 +161,9 @@ if ($to_group_id != 0) { // Add group name after for group documents
 	$add_group_to_title = ' ('.$group_properties['name'].')';
 }
 if (isset($_REQUEST['certificate'])) {
-	$nameTools = get_lang('UploadCertificate').$add_group_to_title;
+    $nameTools = get_lang('UploadCertificate').$add_group_to_title;
 } else {
-	$nameTools = get_lang('UplUploadDocument').$add_group_to_title;
+    $nameTools = get_lang('UplUploadDocument').$add_group_to_title;
 }
 
 // Breadcrumbs
@@ -175,11 +175,11 @@ if ($is_certificate_mode) {
 
 // Interbreadcrumb for the current directory root path
 if (empty($document_data['parents'])) {
-	$interbreadcrumb[] = array('url' => '#', 'name' => $document_data['title']);
+    $interbreadcrumb[] = array('url' => '#', 'name' => $document_data['title']);
 } else {
-	foreach($document_data['parents'] as $document_sub_data) {
-		$interbreadcrumb[] = array('url' => $document_sub_data['document_url'], 'name' => $document_sub_data['title']);
-	}
+    foreach ($document_data['parents'] as $document_sub_data) {
+        $interbreadcrumb[] = array('url' => $document_sub_data['document_url'], 'name' => $document_sub_data['title']);
+    }
 }
 
 $this_section = SECTION_COURSES;
@@ -198,16 +198,14 @@ if (!empty($_FILES)) {
 echo '<div class="actions">';
 // Link back to the documents overview
 if ($is_certificate_mode) {
-	echo '<a href="document.php?id='.$document_id.'&selectcat=' . $selectcat.'">'.Display::return_icon('back.png',get_lang('BackTo').' '.get_lang('CertificateOverview'),'',ICON_SIZE_MEDIUM).'</a>';
+	echo '<a href="document.php?id='.$document_id.'&selectcat=' . $selectcat.'">'.
+            Display::return_icon('back.png',get_lang('BackTo').' '.get_lang('CertificateOverview'),'',ICON_SIZE_MEDIUM).'</a>';
 } else {
-	echo '<a href="document.php?id='.$document_id.'">'.Display::return_icon('back.png',get_lang('BackTo').' '.get_lang('DocumentsOverview'),'',ICON_SIZE_MEDIUM).'</a>';
+	echo '<a href="document.php?id='.$document_id.'">'.
+            Display::return_icon('back.png',get_lang('BackTo').' '.get_lang('DocumentsOverview'),'',ICON_SIZE_MEDIUM).'</a>';
 }
 
 // Link to create a folder
-/*
-if (!isset($_GET['createdir']) && !is_my_shared_folder($_user['user_id'], $path, api_get_session_id()) && !$is_certificate_mode) {
-	echo '<a href="'.api_get_self().'?path='.$path.'&amp;createdir=1">'.Display::return_icon('new_folder.png', get_lang('CreateDir'),'',ICON_SIZE_MEDIUM).'</a>';
-}*/
 echo '</div>';
 
 // Form to select directory
@@ -216,10 +214,6 @@ if (!$is_certificate_mode) {
 	echo build_directory_selector($folders, $document_id, (isset($group_properties['directory']) ? $group_properties['directory'] : array()));
 }
 
-/*$params = Uri::course_params();
-$params['id'] = Request::get('id');
-$action = Uri::here($params, false);*/
-
 $action = api_get_self().'?'.api_get_cidreq().'&id='.$document_id;
 
 $form = new FormValidator('upload', 'POST', $action.'#tabs-2', '', 'enctype="multipart/form-data"');
@@ -237,8 +231,8 @@ $form->addElement('textarea', 'comment', get_lang('Comment'), 'wrap="virtual" st
 
 $advanced = '<a href="javascript://" onclick=" return advanced_parameters()"><span id="img_plus_and_minus"><div style="vertical-align:top;" ><img style="vertical-align:middle;" src="../img/div_show.gif" alt="" />&nbsp;'.get_lang('AdvancedParameters').'</div></span></a>';
 // Advanced parameters
-$form -> addElement('advanced_settings', $advanced);
-$form -> addElement('html', '<div id="options" style="display:none">');
+$form->addElement('advanced_settings', $advanced);
+$form->addElement('html', '<div id="options" style="display:none">');
 
 // Check box options
 $form->addElement('checkbox', 'unzip', get_lang('Options'), get_lang('Uncompress'), 'onclick="javascript: check_unzip();" value="1"');
@@ -263,7 +257,7 @@ $form->addElement('radio', 'if_exists', '', get_lang('UplOverwriteLong'), 'overw
 $form->addElement('radio', 'if_exists', '', get_lang('UplRenameLong'), 'rename');
 
 // Close the java script and avoid the footer up
-$form -> addElement('html', '</div>');
+$form->addElement('html', '</div>');
 
 // Button upload document
 $form->addElement('style_submit_button', 'submitDocument', get_lang('SendDocument'), 'class="upload"');

+ 4 - 2
main/forum/forumfunction.inc.php

@@ -298,13 +298,15 @@ function show_add_forum_form($inputvalues = array(), $lp_id)
     $form->addRule('forum_title', get_lang('ThisFieldIsRequired'), 'required');
     $form->addRule('forum_category', get_lang('ThisFieldIsRequired'), 'required');
 
+    $defaultSettingAllowNewThreads = api_get_default_tool_setting('forum', 'allow_new_threads', 0);
+
     // Settings the defaults
     if (empty($inputvalues) || !is_array($inputvalues)) {
         $defaults['allow_anonymous_group']['allow_anonymous'] = 0;
         $defaults['students_can_edit_group']['students_can_edit'] = 0;
         $defaults['approval_direct_group']['approval_direct'] = 0;
         $defaults['allow_attachments_group']['allow_attachments'] = 1;
-        $defaults['allow_new_threads_group']['allow_new_threads'] = 0;
+        $defaults['allow_new_threads_group']['allow_new_threads'] = $defaultSettingAllowNewThreads;
         $defaults['default_view_type_group']['default_view_type'] = api_get_setting('default_forum_view');
         $defaults['public_private_group_forum_group']['public_private_group_forum'] = 'public';
         if (isset($_GET['forumcategory'])) {
@@ -319,7 +321,7 @@ function show_add_forum_form($inputvalues = array(), $lp_id)
         $defaults['students_can_edit_group']['students_can_edit'] = isset($inputvalues['allow_edit']) ? $inputvalues['allow_edit'] : null;
         $defaults['approval_direct_group']['approval_direct'] = isset($inputvalues['approval_direct_post']) ? $inputvalues['approval_direct_post'] : null;
         $defaults['allow_attachments_group']['allow_attachments'] = isset($inputvalues['allow_attachments']) ? $inputvalues['allow_attachments'] : null;
-        $defaults['allow_new_threads_group']['allow_new_threads'] = isset($inputvalues['allow_new_threads']) ? $inputvalues['allow_new_threads'] : null;
+        $defaults['allow_new_threads_group']['allow_new_threads'] = isset($inputvalues['allow_new_threads']) ? $inputvalues['allow_new_threads'] : $defaultSettingAllowNewThreads;
         $defaults['default_view_type_group']['default_view_type'] = isset($inputvalues['default_view']) ? $inputvalues['default_view'] : null;
         $defaults['public_private_group_forum_group']['public_private_group_forum'] = isset($inputvalues['forum_group_public_private']) ? $inputvalues['forum_group_public_private'] : null;
         $defaults['group_forum'] = isset($inputvalues['forum_of_group']) ? $inputvalues['forum_of_group'] : null;

BIN
main/img/capture.png


BIN
main/img/icons/128/login_as.png


BIN
main/img/icons/22/login_as.png


BIN
main/img/icons/22/login_as_na.png


BIN
main/img/icons/32/import_html.png


BIN
main/img/icons/32/login_as.png


BIN
main/img/icons/48/capture.png


BIN
main/img/icons/48/import_html.png


BIN
main/img/icons/64/login_as.png


+ 50 - 41
main/inc/ajax/document.ajax.php

@@ -1,50 +1,59 @@
 <?php
 /* For licensing terms, see /license.txt */
 /**
- * Responses to AJAX calls for the document upload 
+ * Responses to AJAX calls for the document upload
  */
 require_once '../global.inc.php';
-
 require_once api_get_path(LIBRARY_PATH).'document.lib.php';
 
 $action = $_REQUEST['a'];
-switch($action) {	
-	case 'upload_file':
-		api_protect_course_script(true);		
-		//User access same as upload.php
-		$is_allowed_to_edit = api_is_allowed_to_edit(null, true);		
-		// This needs cleaning!
-		if (api_get_group_id()) {   
-		    if ($is_allowed_to_edit || GroupManager::is_user_in_group(api_get_user_id(), api_get_group_id())) { // Only courseadmin or group members allowed        
-		    } else {
-		        exit;
-		    }
-		} elseif ($is_allowed_to_edit || is_my_shared_folder(api_get_user_id(), $_POST['curdirpath'], api_get_session_id())) {
-		} else { // No course admin and no group member...
-		    exit;
-		}
-		
-		if (!empty($_FILES)) {		    
-		    require_once api_get_path(LIBRARY_PATH).'fileDisplay.lib.php';
-		    $file = $_FILES['file'];		    
-		    $result = DocumentManager::upload_document($_FILES, $_POST['curdirpath'], $file['name'], null, 0, 'overwrite', false, false);		    
-		    $json = array();
-		    $json['name'] = Display::url(api_htmlentities($file['name']), api_htmlentities($result['url']), array('target'=>'_blank'));
-		    $json['type'] = api_htmlentities($file['type']);
-		    $json['size'] = format_file_size($file['size']);    
-		    if (!empty($result) && is_array($result)) {
-		        $json['result'] = Display::return_icon('accept.png', get_lang('Uploaded'));    
-		    } else {
-		        $json['result'] = Display::return_icon('exclamation.png', get_lang('Error'));
-		    }
-		    echo json_encode($json);    
-		}
-		break;
-	case 'document_preview':
-		$course_info = api_get_course_info_by_id($_REQUEST['course_id']);		
-		if (!empty($course_info) && is_array($course_info)) {
-			echo DocumentManager::get_document_preview($course_info, false, '_blank', $_REQUEST['session_id']);
-		}
-		break;		
+switch($action) {
+    case 'upload_file':
+        api_protect_course_script(true);
+        //User access same as upload.php
+        $is_allowed_to_edit = api_is_allowed_to_edit(null, true);
+        // This needs cleaning!
+        if (api_get_group_id()) {
+            // Only courseadmin or group members allowed
+            if ($is_allowed_to_edit || GroupManager::is_user_in_group(api_get_user_id(), api_get_group_id())) {
+            } else {
+                exit;
+            }
+        } elseif ($is_allowed_to_edit || is_my_shared_folder(api_get_user_id(), $_POST['curdirpath'], api_get_session_id())) {
+        } else { // No course admin and no group member...
+            exit;
+        }
+
+        if (!empty($_FILES)) {
+            require_once api_get_path(LIBRARY_PATH).'fileDisplay.lib.php';
+            $file = $_FILES['file'];
+            $result = DocumentManager::upload_document(
+                $_FILES,
+                $_POST['curdirpath'],
+                $file['name'],
+                null,
+                0,
+                'overwrite',
+                false,
+                false
+            );
+            $json = array();
+            $json['name'] = Display::url(api_htmlentities($file['name']), api_htmlentities($result['url']), array('target'=>'_blank'));
+            $json['type'] = api_htmlentities($file['type']);
+            $json['size'] = format_file_size($file['size']);
+            if (!empty($result) && is_array($result)) {
+                $json['result'] = Display::return_icon('accept.png', get_lang('Uploaded'));
+            } else {
+                $json['result'] = Display::return_icon('exclamation.png', get_lang('Error'));
+            }
+            echo json_encode($json);
+        }
+        break;
+    case 'document_preview':
+        $course_info = api_get_course_info_by_id($_REQUEST['course_id']);
+        if (!empty($course_info) && is_array($course_info)) {
+            echo DocumentManager::get_document_preview($course_info, false, '_blank', $_REQUEST['session_id']);
+        }
+        break;
 }
-exit;
+exit;

+ 2 - 0
main/inc/lib/add_course.lib.inc.php

@@ -2391,6 +2391,8 @@ function fill_db_course($course_id, $course_repository, $language, $fill_with_ex
     Database::query("INSERT INTO $TABLESETTING (c_id, variable,value,category) VALUES ($course_id, 'enable_lp_auto_launch',0,'learning_path')");
     Database::query("INSERT INTO $TABLESETTING (c_id, variable,value,category) VALUES ($course_id, 'pdf_export_watermark_text','','learning_path')");
     Database::query("INSERT INTO $TABLESETTING (c_id, variable,value,category) VALUES ($course_id, 'allow_public_certificates','','certificates')");
+    Database::query("INSERT INTO $TABLESETTING (c_id, variable, value) VALUES ($course_id, 'documents_default_visibility', 'visible')");
+
 
 
     /* Course homepage tools for platform admin only */

+ 1 - 1
main/inc/lib/attendance.lib.php

@@ -710,7 +710,7 @@ class Attendance
 		$color_bar = '';
 
 		if ($faults_porcent > 25  ) {
-			$color_bar = '#F11';
+			$color_bar = '#f28989';
 		} else if ($faults_porcent > 10) {
 			$color_bar = '#F90';
 		}

+ 106 - 13
main/inc/lib/course.lib.php

@@ -297,12 +297,12 @@ class CourseManager {
         $sql_delete_mail_queue = "DELETE FROM ".Database::get_course_table(TABLE_FORUM_MAIL_QUEUE)." WHERE c_id = $course_id AND user_id IN (".$user_ids.")";
         Database::query($sql_delete_mail_queue);
 
-
         // Unsubscribe user from the course.
         if (!empty($session_id)) {
             // Delete in table session_rel_course_rel_user
-            Database::query("DELETE FROM ".Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER)."
-                    WHERE id_session ='".$session_id."' AND course_code = '".Database::escape_string($_SESSION['_course']['id'])."' AND id_user IN ($user_ids)");
+            $sql = "DELETE FROM ".Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER)."
+                    WHERE id_session ='".$session_id."' AND course_code = '".Database::escape_string($course_info['code'])."' AND id_user IN ($user_ids)";
+            Database::query($sql);
 
             foreach ($user_id as $uid) {
                 // check if a user is register in the session with other course
@@ -316,20 +316,26 @@ class CourseManager {
             }
 
             // Update the table session
-            $row = Database::fetch_array(Database::query("SELECT COUNT(*) FROM ".Database::get_main_table(TABLE_MAIN_SESSION_USER)."
-                    WHERE id_session = '".$session_id."' AND relation_type<>".SESSION_RELATION_TYPE_RRHH."  "));
-            $count = $row[0]; // number of users by session
-            $result = Database::query("UPDATE ".Database::get_main_table(TABLE_MAIN_SESSION)." SET nbr_users = '$count'
-                    WHERE id = '".$session_id."'");
+            $sql = "SELECT COUNT(*) FROM ".Database::get_main_table(TABLE_MAIN_SESSION_USER)."
+                    WHERE id_session = '".$session_id."' AND relation_type<>".SESSION_RELATION_TYPE_RRHH;
+            $row = Database::fetch_array(Database::query($sql));
+            $count = $row[0];
+            // number of users by session
+            $sql = "UPDATE ".Database::get_main_table(TABLE_MAIN_SESSION)." SET nbr_users = '$count'
+                    WHERE id = '".$session_id."'";
+            Database::query($sql);
 
             // Update the table session_rel_course
-            $row = Database::fetch_array(@Database::query("SELECT COUNT(*) FROM ".Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER)." WHERE id_session = '$session_id' AND course_code = '$course_code' AND status<>2" ));
-            $count = $row[0]; // number of users by session and course
-            $result = @Database::query("UPDATE ".Database::get_main_table(TABLE_MAIN_SESSION_COURSE)." SET nbr_users = '$count' WHERE id_session = '$session_id' AND course_code = '$course_code' ");
+            $sql = "SELECT COUNT(*) FROM ".Database::get_main_table(TABLE_MAIN_SESSION_COURSE_USER)." WHERE id_session = '$session_id' AND course_code = '$course_code' AND status<>2";
+            $row = Database::fetch_array(@Database::query($sql));
+            $count = $row[0];
+            // number of users by session and course
+            Database::query("UPDATE ".Database::get_main_table(TABLE_MAIN_SESSION_COURSE)." SET nbr_users = '$count' WHERE id_session = '$session_id' AND course_code = '$course_code'");
 
         } else {
-            Database::query("DELETE FROM ".Database::get_main_table(TABLE_MAIN_COURSE_USER)."
-                    WHERE user_id IN (".$user_ids.") AND relation_type<>".COURSE_RELATION_TYPE_RRHH." AND course_code = '".$course_code."'");
+            $sql = "DELETE FROM ".Database::get_main_table(TABLE_MAIN_COURSE_USER)."
+                    WHERE user_id IN (".$user_ids.") AND relation_type<>".COURSE_RELATION_TYPE_RRHH." AND course_code = '".$course_code."'";
+            Database::query($sql);
 
             // add event to system log
             $user_id = api_get_user_id();
@@ -4085,4 +4091,91 @@ class CourseManager {
             }
         }
     }
+
+    /**
+     * Course available settings variables see c_course_setting table
+     * @return array
+     */
+    public static function getCourseSettingVariables()
+    {
+        return array(
+            // Get allow_learning_path_theme from table
+            'allow_learning_path_theme',
+            // Get allow_open_chat_window from table
+            'allow_open_chat_window',
+            'allow_public_certificates',
+            // Get allow_user_edit_agenda from table
+            'allow_user_edit_agenda',
+            // Get allow_user_edit_announcement from table
+            'allow_user_edit_announcement',
+            // Get allow_user_image_forum from table
+            'allow_user_image_forum',
+            //Get allow show user list
+            'allow_user_view_user_list',
+            // Get course_theme from table
+            'course_theme',
+            //Get allow show user list
+            'display_info_advance_inside_homecourse',
+            'documents_default_visibility',
+            // Get send_mail_setting (work)from table
+            'email_alert_manager_on_new_doc',
+            // Get send_mail_setting (work)from table
+            'email_alert_manager_on_new_quiz',
+            // Get send_mail_setting (dropbox) from table
+            'email_alert_on_new_doc_dropbox',
+            'email_alert_students_on_new_homework',
+            // Get send_mail_setting (auth)from table
+            'email_alert_to_teacher_on_new_user_in_course',
+            'enable_lp_auto_launch',
+            'pdf_export_watermark_text'
+        );
+    }
+
+    /**
+     * @param string $variable
+     * @param string $value
+     * @param int $courseId
+     * @return bool
+     */
+    public static function saveCourseConfigurationSetting($variable, $value, $courseId)
+    {
+        $settingList = self::getCourseSettingVariables();
+        if (!in_array($variable, $settingList)) {
+            return false;
+        }
+
+        $courseSettingTable = Database::get_course_table(TABLE_COURSE_SETTING);
+
+        if (self::hasCourseSetting($variable, $courseId)) {
+            // Update
+            Database::update(
+                $courseSettingTable,
+                array('value' => $value),
+                array('variable = ? AND c_id = ?' => array($variable, $courseId))
+            );
+        } else {
+            // Create
+            Database::insert(
+                $courseSettingTable,
+                array('value' => $value, 'c_id' => $courseId, 'variable' => $variable)
+            );
+        }
+        return true;
+    }
+
+    /**
+     * Check if course setting exists
+     * @param string $variable
+     * @param int $courseId
+     * @return bool
+     */
+    public static function hasCourseSetting($variable, $courseId)
+    {
+        $courseSetting = Database::get_course_table(TABLE_COURSE_SETTING);
+        $courseId = intval($courseId);
+        $variable = Database::escape_string($variable);
+        $sql = "SELECT variable FROM $courseSetting WHERE c_id = $courseId AND variable = '$variable'";
+        $result = Database::query($sql);
+        return Database::num_rows($result) > 0;
+    }
 }

+ 143 - 48
main/inc/lib/document.lib.php

@@ -14,16 +14,21 @@
 /**
  * Code
  */
-class DocumentManager {
-
-    private function __construct() {
+class DocumentManager
+{
+    /**
+     *
+     */
+    private function __construct()
+    {
 
     }
 
     /**
      * @return the document folder quota for the current course, in bytes, or the default quota
      */
-    public static function get_course_quota($course_code = null) {
+    public static function get_course_quota($course_code = null)
+    {
         if (empty($course_code)) {
             $course_info = api_get_course_info();
         } else {
@@ -465,7 +470,14 @@ class DocumentManager {
      * @param boolean $can_see_invisible
      * @return array with all document data
      */
-    public static function get_all_document_data($_course, $path = '/', $to_group_id = 0, $to_user_id = NULL, $can_see_invisible = false, $search = false) {
+    public static function get_all_document_data(
+        $_course,
+        $path = '/',
+        $to_group_id = 0,
+        $to_user_id = NULL,
+        $can_see_invisible = false,
+        $search = false
+    ) {
         $TABLE_ITEMPROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
         $TABLE_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
 
@@ -477,22 +489,22 @@ class DocumentManager {
             $to_value = $to_group_id;
         }
 
-        //escape underscores in the path so they don't act as a wildcard
+        // Escape underscores in the path so they don't act as a wildcard
         $path = Database::escape_string(str_replace('_', '\_', $path));
         $to_user_id = Database::escape_string($to_user_id);
         $to_value = Database::escape_string($to_value);
 
         $visibility_bit = ' <> 2';
 
-        //the given path will not end with a slash, unless it's the root '/'
-        //so no root -> add slash
+        // The given path will not end with a slash, unless it's the root '/'
+        // so no root -> add slash
         $added_slash = ($path == '/') ? '' : '/';
 
-        //condition for the session
+        // Condition for the session
         $current_session_id = api_get_session_id();
-        $condition_session = " AND (id_session = '$current_session_id' OR id_session = '0')";
+        $condition_session = " AND (id_session = '$current_session_id' OR (id_session = '0') )";
 
-        //condition for search (get ALL folders and documents)
+        // Condition for search (get ALL folders and documents)
 
         $sql = "SELECT  docs.id,
                         docs.filetype,
@@ -506,13 +518,13 @@ class DocumentManager {
                         last.lastedit_date,
                         last.visibility,
                         last.insert_user_id
-                    FROM  " . $TABLE_ITEMPROPERTY . "  AS last INNER JOIN " . $TABLE_DOCUMENT . "  AS docs
-                        ON (docs.id = last.ref AND last.tool = '" . TOOL_DOCUMENT . "' AND docs.c_id = {$_course['real_id']} AND last.c_id = {$_course['real_id']})
+                    FROM $TABLE_ITEMPROPERTY AS last INNER JOIN $TABLE_DOCUMENT AS docs
+                        ON (docs.id = last.ref AND last.tool = '".TOOL_DOCUMENT."' AND docs.c_id = {$_course['real_id']} AND last.c_id = {$_course['real_id']})
                     WHERE
                         docs.path LIKE '" . $path . $added_slash . "%' AND
                         docs.path NOT LIKE '" . $path . $added_slash . "%/%' AND
-                        " . $to_field . " = " . $to_value . " AND
-                        last.visibility" . $visibility_bit . $condition_session;
+                        ".$to_field." = ".$to_value." AND
+                        last.visibility ".$visibility_bit.$condition_session;
         $result = Database::query($sql);
 
         $doc_list = array();
@@ -523,7 +535,7 @@ class DocumentManager {
             while ($row = Database::fetch_array($result, 'ASSOC')) {
 
                 if (api_is_coach()) {
-                    //Looking for course items that are invisible to hide it in the session
+                    // Looking for course items that are invisible to hide it in the session
                     if (in_array($row['id'], array_keys($doc_list))) {
                         if ($doc_list[$row['id']]['item_property_session_id'] == 0 && $doc_list[$row['id']]['session_id'] == 0) {
                             if ($doc_list[$row['id']]['visibility'] == 0) {
@@ -539,9 +551,8 @@ class DocumentManager {
                     $doc_list[] = $row;
                 }
 
-
                 if ($row['filetype'] == 'file' && pathinfo($row['path'], PATHINFO_EXTENSION) == 'html') {
-                    //Templates management
+                    // Templates management
                     $table_template = Database::get_main_table(TABLE_MAIN_TEMPLATES);
                     $sql_is_template = "SELECT id FROM $table_template
                                         WHERE course_code = '" . $_course['code'] . "'
@@ -560,7 +571,7 @@ class DocumentManager {
                 $ids_to_remove = array();
                 $my_repeat_ids = $temp = array();
 
-                //Selecting repetead ids
+                // Selecting repetead ids
                 foreach ($doc_list as $row) {
                     if (in_array($row['id'], array_keys($temp))) {
                         $my_repeat_ids[] = $row['id'];
@@ -568,7 +579,7 @@ class DocumentManager {
                     $temp[$row['id']] = $row;
                 }
                 //@todo use the DocumentManager::is_visible function
-                //Checking disponibility in a session
+                // Checking disponibility in a session
                 foreach ($my_repeat_ids as $id) {
                     foreach ($doc_list as $row) {
                         if ($id == $row['id']) {
@@ -597,7 +608,7 @@ class DocumentManager {
                     }
                 }
 
-                //Checking parents visibility
+                // Checking parents visibility
                 $final_document_data = array();
                 foreach ($document_data as $row) {
                     $is_visible = DocumentManager::check_visibility_tree($row['id'], $_course['code'], $current_session_id, api_get_user_id());
@@ -1057,14 +1068,10 @@ class DocumentManager {
             $row['url'] = api_get_path(WEB_CODE_PATH) . 'document/showinframes.php?cidReq=' . $course_code . '&id=' . $id;
             $row['document_url'] = api_get_path(WEB_CODE_PATH) . 'document/document.php?cidReq=' . $course_code . '&id=' . $id;
             $row['absolute_path'] = api_get_path(SYS_COURSE_PATH) . $course_info['path'] . '/document' . $row['path'];
-
             $row['absolute_path_from_document'] = '/document' . $row['path'];
 
             $pathinfo = pathinfo($row['path']);
-
             $row['absolute_parent_path'] = api_get_path(SYS_COURSE_PATH) . $course_info['path'] . '/document' . $pathinfo['dirname'] . '/';
-
-
             $row['direct_url'] = $www . $path;
 
             if (dirname($row['path']) == '.') {
@@ -1092,12 +1099,6 @@ class DocumentManager {
                     if (!empty($parent_id)) {
                         $sub_document_data = self::get_document_data_by_id($parent_id, $course_code, false);
                         //@todo add visibility here
-
-                        /* $sub_visibility    = self::is_visible_by_id($parent_id, $course_info, api_get_session_id(), api_get_user_id());
-                          if ($visibility && $sub_visibility == false) {
-                          $visibility = false;
-                          }
-                         */
                         $parents[] = $sub_document_data;
                     }
                 }
@@ -2219,6 +2220,10 @@ class DocumentManager {
         return $return;
     }
 
+    /**
+     * @param int $document_id
+     * @param string $course_code
+     */
     public static function export_to_pdf($document_id, $course_code) {
         require_once api_get_path(LIBRARY_PATH) . 'pdf.lib.php';
         $course_data = api_get_course_info($course_code);
@@ -2231,17 +2236,26 @@ class DocumentManager {
     /**
      * Uploads a document
      *
-     * @param array     the $_FILES variable
-     * @param string    $path
-     * @param string    title
-     * @param string    comment
-     * @param int       unzip or not the file
-     * @param int       if_exists overwrite, rename or warn if exists (default)
-     * @param bool      index document (search xapian module)
-     * @param bool      print html messages
-     * @return unknown_type
+     * @param array $files the $_FILES variable
+     * @param string $path
+     * @param string $title
+     * @param string $comment
+     * @param int $unzip unzip or not the file
+     * @param int $if_exists if_exists overwrite, rename or warn if exists (default)
+     * @param bool $index_document index document (search xapian module)
+     * @param bool $show_output print html messages
+     * @return array|bool
      */
-    public static function upload_document($files, $path, $title = null, $comment = null, $unzip = 0, $if_exists = null, $index_document = false, $show_output = false) {
+    public static function upload_document(
+        $files,
+        $path,
+        $title = null,
+        $comment = null,
+        $unzip = 0,
+        $if_exists = null,
+        $index_document = false,
+        $show_output = false
+    ) {
         require_once api_get_path(LIBRARY_PATH) . 'fileUpload.lib.php';
 
         $course_info = api_get_course_info();
@@ -2254,7 +2268,18 @@ class DocumentManager {
 
             if ($upload_ok) {
                 // File got on the server without problems, now process it
-                $new_path = handle_uploaded_document($course_info, $files['file'], $base_work_dir, $path, api_get_user_id(), api_get_group_id(), null, $unzip, $if_exists, $show_output);
+                $new_path = handle_uploaded_document(
+                    $course_info,
+                    $files['file'],
+                    $base_work_dir,
+                    $path,
+                    api_get_user_id(),
+                    api_get_group_id(),
+                    null,
+                    $unzip,
+                    $if_exists,
+                    $show_output
+                );
 
                 if ($new_path) {
                     $docid = DocumentManager::get_document_id($course_info, $new_path);
@@ -2285,12 +2310,6 @@ class DocumentManager {
                         Display::display_confirmation_message(get_lang('UplUploadSucceeded') . '<br />', false);
                     }
 
-                    /* // Check for missing images in html files
-                      $missing_files = check_for_missing_files($base_work_dir.$new_path);
-                      if ($missing_files && $show_output) {
-                      // Show a form to upload the missing files
-                      Display::display_normal_message(build_missing_files_form($missing_files, $path, $files['file']['name']), false);
-                      } */
                     if ($index_document) {
                         self::index_document($docid, $course_info['code'], null, $_POST['language'], $_REQUEST, $if_exists);
                     }
@@ -2956,6 +2975,13 @@ class DocumentManager {
         return $return;
     }
 
+    /**
+     * @param int $doc_id
+     * @param string $course_code
+     * @param int $session_id
+     * @param int $user_id
+     * @return bool
+     */
     public static function check_visibility_tree($doc_id, $course_code, $session_id, $user_id) {
         $document_data = self::get_document_data_by_id($doc_id, $course_code);
 
@@ -3180,10 +3206,18 @@ class DocumentManager {
         return true;
     }
 
+    /**
+     * @return array
+     */
     public static function get_web_odf_extension_list() {
         return array('ods', 'odt');
     }
 
+    /**
+     * @param $path
+     * @param bool $is_certificate_mode
+     * @return bool
+     */
     public static function is_folder_to_avoid($path, $is_certificate_mode = false) {
         $folders_to_avoid = array(
             '/HotPotatoes_files',
@@ -3230,6 +3264,9 @@ class DocumentManager {
         return in_array($path, $folders_to_avoid);
     }
 
+    /**
+     * @return array
+     */
     static function get_system_folders() {
         $system_folders = array(
             '/certificates',
@@ -3244,6 +3281,64 @@ class DocumentManager {
         return $system_folders;
     }
 
+    /**
+     * @param string $courseCode
+     * @return string 'visible' or 'invisible' string
+     */
+    public static function getDocumentDefaultVisibility($courseCode)
+    {
+        $settings = api_get_setting('tool_visible_by_default_at_creation');
+
+        $defaultVisibility = 'visible';
+
+        if (isset($settings['documents'])) {
+            $portalDefaultVisibility =  'invisible';
+            if ($settings['documents'] == 'true') {
+                $portalDefaultVisibility = 'visible';
+            }
+
+            $defaultVisibility = $portalDefaultVisibility;
+        }
+
+        if (api_get_setting('documents_default_visibility_defined_in_course') == 'true') {
+            $courseVisibility = api_get_course_setting('documents_default_visibility', $courseCode);
+            if (!empty($courseVisibility) && in_array($courseVisibility, array('visible', 'invisible'))) {
+                $defaultVisibility = $courseVisibility;
+            }
+        }
+        return $defaultVisibility;
+    }
+
+    /**
+     * @param array $courseInfo
+     * @param int $id doc id
+     * @param string $visibility visible/invisible
+     * @param int $userId
+     */
+    public static function updateVisibilityFromAllSessions($courseInfo, $id, $visibility, $userId)
+    {
+        $sessionList = SessionManager::get_session_by_course($courseInfo['code']);
+
+        if (!empty($sessionList)) {
+            foreach ($sessionList as $session) {
+                $sessionId = $session['id'];
+                api_item_property_update(
+                    $courseInfo,
+                    TOOL_DOCUMENT,
+                    $id,
+                    $visibility,
+                    $userId,
+                    null,
+                    null,
+                    null,
+                    null,
+                    $sessionId
+                );
+            }
+        }
+
+    }
+
 }
 
 //end class DocumentManager

+ 0 - 36
main/inc/lib/events.lib.inc.php

@@ -238,42 +238,6 @@ function event_download($doc_url) {
     return 1;
 }
 
-/**
- * @param doc_id id of document (id in mainDb.document table)
- * @author Sebastien Piraux <piraux_seb@hotmail.com>
- * @desc Record information for upload event
- * used in the works tool to record informations when
- * an user upload 1 work
- */
-function event_upload($doc_id) {
-    global $_user;
-    global $_cid;
-    global $TABLETRACK_UPLOADS;
-
-    $reallyNow = api_get_utc_datetime();
-    if (isset($_user['user_id']) && $_user['user_id']!='') {
-        $user_id = "'".$_user['user_id']."'";
-    } else {
-        $user_id = "0"; // anonymous
-    }
-    $sql = "INSERT INTO ".$TABLETRACK_UPLOADS."
-        		( upload_user_id,
-        		  upload_cours_id,
-        		  upload_work_id,
-        		  upload_date,
-        		  upload_session_id
-        		)
-        		VALUES (
-        		 ".$user_id.",
-        		 '".$_cid."',
-        		 '".$doc_id."',
-        		 '".$reallyNow."',
-        		 '".api_get_session_id()."'
-        		)";
-    $res = Database::query($sql);
-    return 1;
-}
-
 /**
  * @param link_id (id in coursDb liens table)
  * @author Sebastien Piraux <piraux_seb@hotmail.com>

+ 17 - 5
main/inc/lib/export.lib.inc.php

@@ -172,7 +172,8 @@ class Export {
      * 
      * @param array table in array format to be read with the HTML_table class 
      */
-    public static function export_table_pdf($data, $params = array()) {        
+    public static function export_table_pdf($data, $params = array())
+    {
         $table_html = self::convert_array_to_html($data, $params);          
         $params['format'] = isset($params['format']) ? $params['format'] : 'A4';
         $params['orientation'] = isset($params['orientation']) ? $params['orientation'] : 'P';
@@ -180,16 +181,27 @@ class Export {
         $pdf = new PDF($params['format'], $params['orientation'], $params); 
         $pdf->html_to_pdf_with_template($table_html);
     }
-    
-    public static function export_html_to_pdf($html, $params = array()) {        
+
+    /**
+     * @param string $html
+     * @param array $params
+     */
+    public static function export_html_to_pdf($html, $params = array())
+    {
         $params['format'] = isset($params['format']) ? $params['format'] : 'A4';
         $params['orientation'] = isset($params['orientation']) ? $params['orientation'] : 'P';
         
         $pdf = new PDF($params['format'], $params['orientation'], $params); 
         $pdf->html_to_pdf_with_template($html);        
     }
-    
-    public static function convert_array_to_html($data, $params = array()) {        
+
+    /**
+     * @param array $data
+     * @param array $params
+     * @return string
+     */
+    public static function convert_array_to_html($data, $params = array())
+    {
         $headers = $data[0];
         unset($data[0]); 
        

+ 29 - 13
main/inc/lib/fileUpload.lib.php

@@ -176,15 +176,28 @@ function process_uploaded_file($uploaded_file, $show_output = true) {
  * @param boolean Optional output parameter. So far only use for unzip_uploaded_document function. If no output wanted on success, set to false.
  * @return path of the saved file
  */
-function handle_uploaded_document($_course, $uploaded_file, $base_work_dir, $upload_path, $user_id, $to_group_id = 0, $to_user_id = null, $unzip = 0, $what_if_file_exists = '', $output = true) {
-	if (!$user_id) die('Not a valid user.');
+function handle_uploaded_document(
+    $_course,
+    $uploaded_file,
+    $base_work_dir,
+    $upload_path,
+    $user_id,
+    $to_group_id = 0,
+    $to_user_id = null,
+    $unzip = 0,
+    $what_if_file_exists = '',
+    $output = true
+) {
+	if (!$user_id) {
+        die('Not a valid user.');
+    }
 	// Strip slashes
 	$uploaded_file['name'] = stripslashes($uploaded_file['name']);
 	// Add extension to files without one (if possible)
 	$uploaded_file['name'] = add_ext_on_mime($uploaded_file['name'], $uploaded_file['type']);
 	$current_session_id    = api_get_session_id();
 
-    //Just in case process_uploaded_file is not called
+    // Just in case process_uploaded_file is not called
     $max_filled_space = DocumentManager::get_course_quota();
 
 	// Check if there is enough space to save the file
@@ -211,22 +224,18 @@ function handle_uploaded_document($_course, $uploaded_file, $base_work_dir, $upl
 		// No "dangerous" files
 		$clean_name = disable_dangerous_file($clean_name);
 
+        // Checking file extension
 		if (!filter_extension($clean_name)) {
 		    if ($output) {
                 Display::display_error_message(get_lang('UplUnableToSaveFileFilteredExtension'));
 		    }
 			return false;
 		} else {
-			// Extension is good
-			//echo '<br />clean name = '.$clean_name;
-			//echo '<br />upload_path = '.$upload_path;
 			// If the upload path differs from / (= root) it will need a slash at the end
 			if ($upload_path != '/') {
 				$upload_path = $upload_path.'/';
 			}
-			//echo '<br />upload_path = '.$upload_path;
 			$file_path = $upload_path.$clean_name;
-			//echo '<br />file path = '.$file_path;
 			// Full path to where we want to store the file with trailing slash
 			$where_to_save = $base_work_dir.$upload_path;
 			// At least if the directory doesn't exist, tell so
@@ -236,10 +245,9 @@ function handle_uploaded_document($_course, $uploaded_file, $base_work_dir, $upl
 				}
 				return false;
 			}
-			//echo '<br />where to save = '.$where_to_save;
 			// Full path of the destination
 			$store_path = $where_to_save.$clean_name;
-			//echo '<br />store path = '.$store_path;
+
 			// Name of the document without the extension (for the title)
 			$document_name = get_document_title($uploaded_file['name']);
 			// Size of the uploaded file (in bytes)
@@ -260,13 +268,13 @@ function handle_uploaded_document($_course, $uploaded_file, $base_work_dir, $upl
 							$document_id = DocumentManager::get_document_id($_course, $file_path);
 
 							if (is_numeric($document_id)) {
-								// Update filesize
+								// Update file size
 								update_existing_document($_course, $document_id, $uploaded_file['size']);
 
 								// Update document item_property
 								api_item_property_update($_course, TOOL_DOCUMENT, $document_id, 'DocumentUpdated', $user_id, $to_group_id, $to_user_id, null, null, $current_session_id);
 
-                                //Redo visibility
+                                // Redo visibility
                                 api_set_default_visibility(TOOL_DOCUMENT, $document_id);
 							}
 							// If the file is in a folder, we need to update all parent folders
@@ -274,7 +282,7 @@ function handle_uploaded_document($_course, $uploaded_file, $base_work_dir, $upl
 							// Display success message with extra info to user
 							if ($output) {
 								Display::display_confirmation_message(get_lang('UplUploadSucceeded').'<br />'.$file_path .' '. get_lang('UplFileOverwritten'), false);
-							}
+        					}
 							return $file_path;
 						} else {
 							// Put the document data in the database
@@ -282,6 +290,9 @@ function handle_uploaded_document($_course, $uploaded_file, $base_work_dir, $upl
 							if ($document_id) {
 								// Put the document in item_property update
 								api_item_property_update($_course, TOOL_DOCUMENT, $document_id, 'DocumentAdded', $user_id, $to_group_id, $to_user_id, null, null, $current_session_id);
+
+                                // Redo visibility
+                                api_set_default_visibility(TOOL_DOCUMENT, $document_id);
 							}
 							// If the file is in a folder, we need to update all parent folders
 							item_property_update_on_folder($_course, $upload_path, $user_id);
@@ -314,6 +325,9 @@ function handle_uploaded_document($_course, $uploaded_file, $base_work_dir, $upl
 						if ($document_id) {
 							// Update document item_property
 							api_item_property_update($_course, TOOL_DOCUMENT, $document_id, 'DocumentAdded', $user_id, $to_group_id, $to_user_id, null, null, $current_session_id);
+
+                            // Redo visibility
+                            api_set_default_visibility(TOOL_DOCUMENT, $document_id);
 						}
 						// If the file is in a folder, we need to update all parent folders
 						item_property_update_on_folder($_course, $upload_path, $user_id);
@@ -347,6 +361,8 @@ function handle_uploaded_document($_course, $uploaded_file, $base_work_dir, $upl
 							if ($document_id) {
 								// Update document item_property
 								api_item_property_update($_course, TOOL_DOCUMENT, $document_id, 'DocumentAdded', $user_id, $to_group_id, $to_user_id, null, null, $current_session_id);
+                                // Redo visibility
+                                api_set_default_visibility(TOOL_DOCUMENT, $document_id);
 							}
 							// If the file is in a folder, we need to update all parent folders
 							item_property_update_on_folder($_course,$upload_path,$user_id);

+ 11 - 0
main/inc/lib/jcapture/.settings/org.eclipse.jdt.core.prefs

@@ -0,0 +1,11 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6

+ 23 - 0
main/inc/lib/jcapture/JCaptureApplet.html

@@ -0,0 +1,23 @@
+<!-- WARNING: Eclipse auto-generated file.
+              Any modifications will be overwritten. -->
+<html>
+    <body>
+    
+<script>
+    function llamarAplett()
+    {
+        document.write("Prueba de sonido");
+        
+        alert("Prueba de sonido;")
+    }
+</script>    
+    
+    <input type="button" onclick="llamarAplett();"/>   
+    
+<!--     
+    <applet code='com.hammurapi.jcapture.JCaptureApplet.class' archive='lib\jcapture.jar' width='20' height='20'>
+        <param name='outputDir' value='C:\_temp'>
+    </applet>
+-->
+    </body>
+</html>

+ 48 - 0
main/inc/lib/jcapture/action.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * JCapture plugin
+ *
+ * @author Pavel Vlasov
+ */
+ 
+if (!defined('DOKU_INC')) die();
+if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
+require_once (DOKU_PLUGIN . 'action.php');
+ 
+class action_plugin_jcapture extends DokuWiki_Action_Plugin {
+ 
+  /**
+   * return some info
+   */
+  function getInfo(){
+    return array(
+                 'author' => 'Pavel Vlasov',
+                 'email'  => 'Pavel.Vlasov@hammurapi.com',
+                 'name'   => 'JCapture',
+                 'desc'   => 'Plugin for making screen captures.',
+                 'url'    => 'http://www.hammurapi.com/dokuwiki/doku.php/products:jcapture:start',
+                 );
+  }
+
+    /**
+     * Register the eventhandlers
+     */
+    function register(&$controller) {
+        $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'insert_button', array ());
+    }
+ 
+    /**
+     * Inserts the toolbar button
+     */
+    function insert_button(& $event, $param) {
+        $event->data[] = array (
+            'type' => 'JCapture',
+            'title' => 'Screen capture',
+            'icon' => '../../plugins/jcapture/camera.png',
+            'open' => '<abutton>',
+            'close' => '</abutton>',
+        );
+    }
+ 
+}
+

+ 99 - 0
main/inc/lib/jcapture/applet.php

@@ -0,0 +1,99 @@
+<?php 
+
+	//fix for Opera XMLHttpRequests
+	if(!count($_POST) && $HTTP_RAW_POST_DATA){
+	  parse_str($HTTP_RAW_POST_DATA, $_POST);
+	}
+	
+	if(!defined('DOKU_INC')) define('DOKU_INC',dirname(__FILE__).'/../../../');
+	require_once(DOKU_INC.'inc/init.php');
+	require_once(DOKU_INC.'inc/common.php');
+	require_once(DOKU_INC.'inc/pageutils.php');
+	require_once(DOKU_INC.'inc/auth.php');
+	//close sesseion
+	session_write_close();
+	
+	header('Content-Type: text/html; charset=utf-8');
+	
+	$hostName = "http".($_SERVER['HTTPS'] ? 's' : null).'://'.$_SERVER['HTTP_HOST']; 
+	
+	$imageFormat = "PNG";
+
+	$cookies;
+	foreach (array_keys($_COOKIE) as $cookieName) {
+		$cookies.=bin2hex($cookieName)."=".bin2hex($_COOKIE[$cookieName]).";";
+	} 
+	
+	$pageName = $_GET["pageName"];
+	$edid = $_GET["edid"];
+?>
+
+<script language="JavaScript" type="text/javascript"><!--
+    var _info = navigator.userAgent;
+    var _ns = false;
+    var _ns6 = false;
+    var _ie = (_info.indexOf("MSIE") > 0 && _info.indexOf("Win") > 0 && _info.indexOf("Windows 3.1") < 0);
+//--></script>
+    <comment>
+        <script language="JavaScript" type="text/javascript"><!--
+        var _ns = (navigator.appName.indexOf("Netscape") >= 0 && ((_info.indexOf("Win") > 0 && _info.indexOf("Win16") < 0 && java.lang.System.getProperty("os.version").indexOf("3.5") < 0) || (_info.indexOf("Sun") > 0) || (_info.indexOf("Linux") > 0) || (_info.indexOf("AIX") > 0) || (_info.indexOf("OS/2") > 0) || (_info.indexOf("IRIX") > 0)));
+        var _ns6 = ((_ns == true) && (_info.indexOf("Mozilla/5") >= 0));
+//--></script>
+    </comment>
+
+<script language="JavaScript" type="text/javascript"><!--
+    if (_ie == true) document.writeln('<object classid="clsid:CAFEEFAC-0017-0000-0020-ABCDEFFEDCBA" NAME = "jCapture"  WIDTH = "1" HEIGHT = "1" codebase="http://java.sun.com/update/1.7.0/jinstall-1_7_0-windows-i586.cab#Version=7,0,0,0"><xmp>');
+    else if (_ns == true && _ns6 == false) document.writeln('<embed ' +
+	    'type="application/x-java-applet;jpi-version=1.7.0" \
+            ID = "jCaptureApplet" \
+            scriptable = "true" \
+            mayscript = "true" \
+            WIDTH = "1"
+            JAVA_CODEBASE = "/somenonexistingcodebase" \
+            HEIGHT = "1"
+            CODE = "com.hammurapi.jcapture.JCaptureApplet.class" \
+            ARCHIVE = "<?php echo DOKU_BASE; ?>lib/plugins/jcapture/lib/jcapture.jar" \
+            NAME = "jCapture" \
+            dokuBase ="<?php echo bin2hex(DOKU_BASE); ?>" \
+            sectok ="<?php echo getSecurityToken(); ?>" \
+            cookies ="<?php echo $cookies; ?>" \
+            authtok = "<?php echo auth_createToken(); ?>" \
+            pageName = "<?php echo $pageName; ?>" \
+            edid = "<?php echo $edid; ?>" \
+            host ="<?php echo $hostName; ?>" ' +
+	    'scriptable=false ' +
+	    'pluginspage="http://java.sun.com/products/plugin/index.html#download"><xmp>');
+//--></script>
+<applet id="jCaptureApplet" CODE = "com.hammurapi.jcapture.JCaptureApplet.class" WIDTH="1" HEIGHT="1" ARCHIVE = "<?php echo DOKU_BASE; ?>lib/plugins/jcapture/lib/jcapture.jar" NAME = "jCapture"></xmp>
+    <PARAM NAME = CODE VALUE = "com.hammurapi.jcapture.JCaptureApplet.class" >
+    <PARAM NAME = ARCHIVE VALUE = "<?php echo DOKU_BASE; ?>lib/plugins/jcapture/lib/jcapture.jar" >
+    <PARAM NAME = NAME VALUE = "jCapture" >
+    <param name="type" value="application/x-java-applet;jpi-version=1.7.0">
+    <param name="scriptable" value="true">
+    <param name="mayscript" value="true">
+    <PARAM NAME = "dokuBase" VALUE="<?php echo bin2hex(DOKU_BASE); ?>">
+    <PARAM NAME = "sectok" VALUE="<?php echo getSecurityToken(); ?>">
+    <PARAM NAME = "cookies" VALUE="<?php echo $cookies; ?>">
+    <PARAM NAME = "host" VALUE="<?php echo $hostName; ?>">
+    <PARAM NAME = "pageName" VALUE="<?php echo $pageName; ?>">
+    <PARAM NAME = "edid" VALUE="<?php echo $edid; ?>">
+    <PARAM NAME = CODEBASE VALUE = "/somenonexistingcodebase" >    
+    <PARAM NAME = "authtok" VALUE="<?php echo auth_createToken(); ?>">
+
+Java 2 Standard Edition v 1.7 or above is required for this applet.<br/>
+		Download it from <a href="http://java.sun.com">http://java.sun.com</a>.
+</applet>
+</embed>
+</object>
+
+<!--
+<APPLET CODE = "com.hammurapi.jcapture.JCaptureApplet.class" ARCHIVE = "<?php echo DOKU_BASE; ?>lib/plugins/jcapture/lib/jcapture.jar" NAME = "jCapture">
+<PARAM NAME = "dokuBase" VALUE="<?php echo bin2hex(DOKU_BASE); ?>">
+<PARAM NAME = "sectok" VALUE="<?php echo getSecurityToken(); ?>">
+<PARAM NAME = "cookies" VALUE="<?php echo $cookies; ?>">
+<PARAM NAME = "host" VALUE="<?php echo $hostName; ?>">
+Java 2 Standard Edition v 1.7 or above is required for this applet.<br/>
+		Download it from <a href="http://java.sun.com">http://java.sun.com</a>.
+
+</APPLET>
+-->

BIN
main/inc/lib/jcapture/camera.png


+ 21 - 0
main/inc/lib/jcapture/jcapture.jnlp

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jnlp spec="1.0+" codebase="" href="">
+    <information>
+        <title>jCapture</title>
+        <vendor>Hammurapi Group</vendor>
+    </information>
+    
+    <resources>
+        <!-- Application Resources -->
+        <j2se version="1.7+" href="http://java.sun.com/products/autodl/j2se" />
+        <jar href="lib/jcapture.jar" main="true" />
+    </resources>
+    
+    <applet-desc 
+         name="jCapture Applet"
+         main-class="com.hammurapi.jcapture.JCaptureApplet"
+         width="300"
+         height="300">
+     </applet-desc>
+     <update check="background"/>
+</jnlp>                                   

BIN
main/inc/lib/jcapture/lib/apache-mime4j-0.6.jar


BIN
main/inc/lib/jcapture/lib/commons-codec-1.3.jar


BIN
main/inc/lib/jcapture/lib/commons-logging-1.1.1.jar


BIN
main/inc/lib/jcapture/lib/httpclient-4.0.1.jar


BIN
main/inc/lib/jcapture/lib/httpcore-4.0.1.jar


BIN
main/inc/lib/jcapture/lib/httpmime-4.0.1.jar


BIN
main/inc/lib/jcapture/lib/jcapture.jar


BIN
main/inc/lib/jcapture/lib/transform-3.0.2.jar


+ 1 - 0
main/inc/lib/jcapture/license.txt

@@ -0,0 +1 @@
+GPL v 2

+ 19 - 0
main/inc/lib/jcapture/script.js

@@ -0,0 +1,19 @@
+function addBtnActionJCapture($btn, props, edid) {
+	$btn.click(function() {
+		var appletDiv = document.getElementById("jCaptureAppletDiv");
+		if (appletDiv==null) {
+	        	var oNewDiv = document.createElement("div");
+		        oNewDiv.id="jCaptureAppletDiv";
+		        //oNewDiv.style.display='none';
+		        document.body.appendChild(oNewDiv);
+			jQuery("#jCaptureAppletDiv").load(DOKU_BASE+"lib/plugins/jcapture/applet.php?edid="+edid+"&pageName="+document.forms['dw__editform'].elements['id'].value);
+		} else {
+	    		document.getElementById("jCaptureApplet").showCaptureFrame();
+		}	    
+        	return false;
+	});
+ 
+	return true;
+}
+
+

+ 1 - 0
main/inc/lib/jcapture/src/META-INF/services/com.hammurapi.jcapture.VideoEncoder

@@ -0,0 +1 @@
+com.hammurapi.jcapture.SwfEncoder

Some files were not shown because too many files changed in this diff