From 147c7b46327c708bea86eefea7344c8cd8eb1bd7 Mon Sep 17 00:00:00 2001 From: greenmarine Date: Thu, 11 Feb 2010 15:54:58 +0000 Subject: [PATCH] commit v0.2 of Nexuiz Demo Recorder git-svn-id: svn://svn.icculus.org/nexuiz/trunk@8633 f962a42d-fe04-0410-a3ab-8c8b0445ebaa --- misc/tools/NexuizDemoRecorder/pom.xml | 83 ++ .../application/DemoRecorderApplication.java | 444 +++++++ .../application/DemoRecorderException.java | 13 + .../application/DemoRecorderUtils.java | 95 ++ .../application/NDRPreferences.java | 66 + .../application/RecorderJobPoolExecutor.java | 48 + .../application/democutter/DemoCutter.java | 220 ++++ .../democutter/DemoCutterException.java | 14 + .../democutter/DemoCutterUtils.java | 40 + .../application/democutter/DemoPacket.java | 102 ++ .../application/jobs/EncoderJob.java | 24 + .../application/jobs/RecordJob.java | 636 ++++++++++ .../application/jobs/RecordsDoneJob.java | 17 + .../application/plugins/EncoderPlugin.java | 69 + .../plugins/EncoderPluginException.java | 14 + .../com/nexuiz/demorecorder/main/Driver.java | 18 + .../demorecorder/ui/DemoRecorderUI.java | 20 + .../demorecorder/ui/swinggui/JobDialog.java | 737 +++++++++++ .../ui/swinggui/NexuizUserDirFilter.java | 31 + .../ui/swinggui/PreferencesDialog.java | 195 +++ .../ui/swinggui/RecordJobTemplate.java | 113 ++ .../demorecorder/ui/swinggui/StatusBar.java | 86 ++ .../demorecorder/ui/swinggui/SwingGUI.java | 1109 +++++++++++++++++ .../RecordJobTemplatesTableModel.java | 221 ++++ .../tablemodels/RecordJobsTableModel.java | 192 +++ .../ShowErrorDialogExceptionHandler.java | 21 + .../ui/swinggui/utils/SwingGUIUtils.java | 26 + .../ui/swinggui/utils/XProperties.java | 432 +++++++ .../src/main/resources/about.html | 21 + .../main/resources/help/DemoRecorderHelp.hs | 42 + .../resources/help/DemoRecorderHelpIndex.xml | 4 + .../resources/help/DemoRecorderHelpTOC.xml | 37 + .../main/resources/help/JHelpDev Project.xml | 32 + .../main/resources/help/JavaHelpSearch/DOCS | Bin 0 -> 2905 bytes .../resources/help/JavaHelpSearch/DOCS.TAB | 14 + .../resources/help/JavaHelpSearch/OFFSETS | Bin 0 -> 68 bytes .../resources/help/JavaHelpSearch/POSITIONS | Bin 0 -> 8799 bytes .../main/resources/help/JavaHelpSearch/SCHEMA | 2 + .../main/resources/help/JavaHelpSearch/TMAP | Bin 0 -> 14336 bytes .../src/main/resources/help/Map.jhm | 41 + .../help/html/advanced-how-it-works.html | 77 ++ .../help/html/advanced-prelim-stop.html | 23 + .../help/html/advanced-table-settings.html | 26 + .../resources/help/html/advanced-topics.html | 18 + .../resources/help/html/basic_tutorial.html | 169 +++ .../main/resources/help/html/changelog.html | 37 + .../help/html/compat-limitations.html | 55 + .../src/main/resources/help/html/credits.html | 27 + .../src/main/resources/help/html/faq.html | 100 ++ .../resources/help/html/images/create_job.gif | Bin 0 -> 9994 bytes .../help/html/images/create_template.gif | Bin 0 -> 9910 bytes .../help/html/images/customize_tables.gif | Bin 0 -> 18632 bytes .../help/html/images/edit_job_vdub.gif | Bin 0 -> 3800 bytes .../help/html/images/main_window.gif | Bin 0 -> 15843 bytes .../help/html/images/preferences_dialog.gif | Bin 0 -> 7517 bytes .../html/images/preferences_global_vdub.gif | Bin 0 -> 9368 bytes .../resources/help/html/introduction.html | 28 + .../src/main/resources/help/html/license.html | 15 + .../main/resources/help/html/open-save.html | 29 + .../help/html/plugin-architecture.html | 81 ++ .../help/html/plugin-virtualdub.html | 163 +++ .../main/resources/help/html/preferences.html | 64 + .../main/resources/help/html/templates.html | 69 + .../src/main/resources/icons/advanced.png | Bin 0 -> 838 bytes .../src/main/resources/icons/edit.png | Bin 0 -> 3331 bytes .../src/main/resources/icons/edit_add.png | Bin 0 -> 3329 bytes .../src/main/resources/icons/editclear.png | Bin 0 -> 3054 bytes .../src/main/resources/icons/editcopy.png | Bin 0 -> 3356 bytes .../src/main/resources/icons/editdelete.png | Bin 0 -> 3653 bytes .../src/main/resources/icons/exit.png | Bin 0 -> 876 bytes .../src/main/resources/icons/fileopen.png | Bin 0 -> 3483 bytes .../src/main/resources/icons/filesave.png | Bin 0 -> 3319 bytes .../src/main/resources/icons/help.png | Bin 0 -> 3567 bytes .../src/main/resources/icons/info.png | Bin 0 -> 3632 bytes .../src/main/resources/icons/package.png | Bin 0 -> 877 bytes .../src/main/resources/icons/player_pause.png | Bin 0 -> 3661 bytes .../src/main/resources/icons/player_play.png | Bin 0 -> 3638 bytes .../main/resources/icons/quick_restart.png | Bin 0 -> 3514 bytes .../resources/icons/quick_restart_blue.png | Bin 0 -> 3509 bytes .../main/resources/icons/status_unknown.png | Bin 0 -> 3406 bytes .../src/main/resources/icons/view_right_p.png | Bin 0 -> 3346 bytes .../resources/jsmooth exe project.jsmooth | 45 + 82 files changed, 6305 insertions(+) create mode 100644 misc/tools/NexuizDemoRecorder/pom.xml create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderApplication.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderException.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderUtils.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/NDRPreferences.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/RecorderJobPoolExecutor.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutter.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutterException.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutterUtils.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoPacket.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/EncoderJob.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordJob.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordsDoneJob.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/plugins/EncoderPlugin.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/plugins/EncoderPluginException.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/main/Driver.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/DemoRecorderUI.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/JobDialog.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/NexuizUserDirFilter.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/PreferencesDialog.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/RecordJobTemplate.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/StatusBar.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/SwingGUI.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/tablemodels/RecordJobTemplatesTableModel.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/tablemodels/RecordJobsTableModel.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/ShowErrorDialogExceptionHandler.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/SwingGUIUtils.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/XProperties.java create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/about.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelp.hs create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelpIndex.xml create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelpTOC.xml create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/JHelpDev Project.xml create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/DOCS create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/DOCS.TAB create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/OFFSETS create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/POSITIONS create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/SCHEMA create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/TMAP create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/Map.jhm create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-how-it-works.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-prelim-stop.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-table-settings.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-topics.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/basic_tutorial.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/changelog.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/compat-limitations.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/credits.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/faq.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/create_job.gif create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/create_template.gif create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/customize_tables.gif create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/edit_job_vdub.gif create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/main_window.gif create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/preferences_dialog.gif create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/preferences_global_vdub.gif create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/introduction.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/license.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/open-save.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/plugin-architecture.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/plugin-virtualdub.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/preferences.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/help/html/templates.html create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/advanced.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/edit.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/edit_add.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/editclear.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/editcopy.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/editdelete.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/exit.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/fileopen.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/filesave.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/help.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/info.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/package.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/player_pause.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/player_play.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/quick_restart.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/quick_restart_blue.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/status_unknown.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/icons/view_right_p.png create mode 100644 misc/tools/NexuizDemoRecorder/src/main/resources/jsmooth exe project.jsmooth diff --git a/misc/tools/NexuizDemoRecorder/pom.xml b/misc/tools/NexuizDemoRecorder/pom.xml new file mode 100644 index 000000000..712d29193 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/pom.xml @@ -0,0 +1,83 @@ + + 4.0.0 + NexuizDemoRecorder + NexuizDemoRecorder + jar + 0.2 + NexuizDemoRecorder + http://maven.apache.org + + + com.miglayout + miglayout + 3.7.2 + + + org.swinglabs + swingx + 1.6 + + + + org.swinglabs + swing-worker + + + com.jhlabs + filters + + + + + javax.help + javahelp + 2.0.02 + + + + + + src/main/resources + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.0.2 + + 1.6 + 1.6 + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + lib/ + com.nexuiz.demorecorder.main.Driver + + + + + + maven-dependency-plugin + + + package + + copy-dependencies + + + ${project.build.directory}/lib + + + + + + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderApplication.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderApplication.java new file mode 100644 index 000000000..2ca7b3d58 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderApplication.java @@ -0,0 +1,444 @@ +package com.nexuiz.demorecorder.application; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.nexuiz.demorecorder.application.jobs.EncoderJob; +import com.nexuiz.demorecorder.application.jobs.RecordJob; +import com.nexuiz.demorecorder.application.jobs.RecordsDoneJob; +import com.nexuiz.demorecorder.application.plugins.EncoderPlugin; +import com.nexuiz.demorecorder.ui.DemoRecorderUI; + +public class DemoRecorderApplication { + + public static class Preferences { + public static final String OVERWRITE_VIDEO_FILE = "Overwrite final video destination file if it exists"; + public static final String DISABLE_RENDERING = "Disable rendering while fast-forwarding"; + public static final String DISABLE_SOUND = "Disable sound while fast-forwarding"; + public static final String FFW_SPEED_FIRST_STAGE = "Fast-forward speed (first stage)"; + public static final String FFW_SPEED_SECOND_STAGE = "Fast-forward speed (second stage)"; + public static final String DO_NOT_DELETE_CUT_DEMOS = "Do not delete cut demos"; + public static final String JOB_NAME_APPEND_DUPLICATE = "Append this suffix to job-name when duplicating jobs"; + + public static final String[] PREFERENCES_ORDER = { + OVERWRITE_VIDEO_FILE, + DISABLE_RENDERING, + DISABLE_SOUND, + FFW_SPEED_FIRST_STAGE, + FFW_SPEED_SECOND_STAGE, + DO_NOT_DELETE_CUT_DEMOS, + JOB_NAME_APPEND_DUPLICATE + }; + } + + public static final String PREFERENCES_DIRNAME = "settings"; + public static final String LOGS_DIRNAME = "logs"; + public static final String PLUGINS_DIRNAME = "plugins"; + public static final String APP_PREFERENCES_FILENAME = "app_preferences.xml"; + public static final String JOBQUEUE_FILENAME = "jobs.dat"; + + public static final int STATE_WORKING = 0; + public static final int STATE_IDLE = 1; + + private RecorderJobPoolExecutor poolExecutor; + private List jobs; + private NDRPreferences preferences = null; + private List registeredUserInterfaces; + private List encoderPlugins; + private int state = STATE_IDLE; + + public DemoRecorderApplication() { + poolExecutor = new RecorderJobPoolExecutor(); + jobs = new CopyOnWriteArrayList(); + this.registeredUserInterfaces = new ArrayList(); + this.encoderPlugins = new ArrayList(); + this.getPreferences(); + this.loadPlugins(); + this.configurePlugins(); + this.loadJobQueue(); + } + + public void setPreference(String category, String preference, boolean value) { + this.preferences.setProperty(category, preference, String.valueOf(value)); + } + + public void setPreference(String category, String preference, int value) { + this.preferences.setProperty(category, preference, String.valueOf(value)); + } + + public void setPreference(String category, String preference, String value) { + this.preferences.setProperty(category, preference, value); + } + + public NDRPreferences getPreferences() { + if (this.preferences == null) { + this.preferences = new NDRPreferences(); + this.createPreferenceDefaultValues(); + File preferencesFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, APP_PREFERENCES_FILENAME); + if (preferencesFile.exists()) { + FileInputStream fis = null; + try { + fis = new FileInputStream(preferencesFile); + this.preferences.loadFromXML(fis); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Could not load the application preferences file!", e, true); + } + } + } + + return this.preferences; + } + + private void createPreferenceDefaultValues() { + this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.OVERWRITE_VIDEO_FILE, "false"); + this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_RENDERING, "true"); + this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_SOUND, "true"); + this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_FIRST_STAGE, "100"); + this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_SECOND_STAGE, "10"); + this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DO_NOT_DELETE_CUT_DEMOS, "false"); + this.preferences.setProperty(NDRPreferences.MAIN_APPLICATION, Preferences.JOB_NAME_APPEND_DUPLICATE, " duplicate"); + } + + public void savePreferences() { + File preferencesFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, APP_PREFERENCES_FILENAME); + if (!preferencesFile.exists()) { + try { + preferencesFile.createNewFile(); + } catch (IOException e) { + File parentDir = preferencesFile.getParentFile(); + if (!parentDir.exists()) { + try { + if (parentDir.mkdirs() == true) { + try { + preferencesFile.createNewFile(); + } catch (Exception ex) {} + } + } catch (Exception ex) {} + } + } + } + + if (!preferencesFile.exists()) { + DemoRecorderException ex = new DemoRecorderException("Could not create the preferences file " + preferencesFile.getAbsolutePath()); + DemoRecorderUtils.showNonCriticalErrorDialog(ex); + return; + } + + FileOutputStream fos; + try { + fos = new FileOutputStream(preferencesFile); + } catch (FileNotFoundException e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Could not create the preferences file " + preferencesFile.getAbsolutePath() + ". Unsufficient rights?", e, true); + return; + } + try { + this.preferences.storeToXML(fos, null); + } catch (IOException e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Could not create the preferences file " + preferencesFile.getAbsolutePath(), e, true); + } + } + + public List getRecordJobs() { + return new ArrayList(this.jobs); + } + + public void startRecording() { + if (this.state != STATE_WORKING) { + this.state = STATE_WORKING; + + for (RecordJob currentJob : this.jobs) { + if (currentJob.getState() == RecordJob.State.WAITING) { + this.poolExecutor.runJob(currentJob); + } + } + + //notify ourself when job is done + this.poolExecutor.runJob(new RecordsDoneJob(this)); + } + } + + public void recordSelectedJobs(List jobList) { + if (this.state == STATE_IDLE) { + this.state = STATE_WORKING; + for (RecordJob currentJob : jobList) { + if (currentJob.getState() == RecordJob.State.WAITING) { + this.poolExecutor.runJob(currentJob); + } + } + + //notify ourself when job is done + this.poolExecutor.runJob(new RecordsDoneJob(this)); + } + } + + public void executePluginForSelectedJobs(EncoderPlugin plugin, List jobList) { + if (this.state == STATE_IDLE) { + this.state = STATE_WORKING; + for (RecordJob currentJob : jobList) { + if (currentJob.getState() == RecordJob.State.DONE) { + this.poolExecutor.runJob(new EncoderJob(currentJob, plugin)); + } + } + + //notify ourself when job is done + this.poolExecutor.runJob(new RecordsDoneJob(this)); + } + } + + public void notifyAllJobsDone() { + this.state = STATE_IDLE; + + //notify all UIs + for (DemoRecorderUI currentUI : this.registeredUserInterfaces) { + currentUI.recordingFinished(); + } + } + + public synchronized void stopRecording() { + if (this.state == STATE_WORKING) { + //clear the queue of the threadpoolexecutor and add the GUI/applayer notify job again + this.poolExecutor.clearUnfinishedJobs(); + this.poolExecutor.runJob(new RecordsDoneJob(this)); + } + } + + public RecordJob createRecordJob( + String name, + File enginePath, + String engineParameters, + File demoFile, + String relativeDemoPath, + File dpVideoPath, + File videoDestination, + String executeBeforeCap, + String executeAfterCap, + float startSecond, + float endSecond + ) { + int jobIndex = -1; + if (name == null || name.equals("")) { + //we don't have a name, so use a generic one + jobIndex = this.getNewJobIndex(); + name = "Job " + jobIndex; + } else { + //just use the name and keep jobIndex at -1. Jobs with real names don't need an index + } + + + + RecordJob newJob = new RecordJob( + this, + name, + jobIndex, + enginePath, + engineParameters, + demoFile, + relativeDemoPath, + dpVideoPath, + videoDestination, + executeBeforeCap, + executeAfterCap, + startSecond, + endSecond + ); + this.jobs.add(newJob); + this.fireUserInterfaceUpdate(newJob); + + return newJob; + } + + public synchronized boolean deleteRecordJob(RecordJob job) { + if (!this.jobs.contains(job)) { + return false; + } + + //don't delete jobs that are scheduled for execution + if (this.poolExecutor.getJobList().contains(job)) { + return false; + } + + this.jobs.remove(job); + return true; + } + + public void addUserInterfaceListener(DemoRecorderUI ui) { + this.registeredUserInterfaces.add(ui); + } + + /** + * Makes sure that all registered user interfaces can update their view/display. + * @param job either a job that's new to the UI, or one the UI already knows but of which details changed + */ + public void fireUserInterfaceUpdate(RecordJob job) { + for (DemoRecorderUI ui : this.registeredUserInterfaces) { + ui.RecordJobPropertiesChange(job); + } + } + + public int getNewJobIndex() { + int jobIndex; + if (this.jobs.size() == 0) { + jobIndex = 1; + } else { + int greatestIndex = -1; + for (RecordJob j : this.jobs) { + if (j.getJobIndex() > greatestIndex) { + greatestIndex = j.getJobIndex(); + } + } + if (greatestIndex == -1) { + jobIndex = 1; + } else { + jobIndex = greatestIndex + 1; + } + } + + return jobIndex; + } + + private void loadJobQueue() { + File defaultFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, JOBQUEUE_FILENAME); + this.loadJobQueue(defaultFile); + } + + @SuppressWarnings("unchecked") + public void loadJobQueue(File path) { + if (!path.exists()) { + return; + } + + try { + FileInputStream fin = new FileInputStream(path); + ObjectInputStream ois = new ObjectInputStream(fin); + this.jobs = (List) ois.readObject(); + for (RecordJob currentJob : this.jobs) { + currentJob.setAppLayer(this); + } + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Could not load the job queue file " + path.getAbsolutePath(), e, true); + } + + } + + public void saveJobQueue() { + File defaultFile = DemoRecorderUtils.computeLocalFile(PREFERENCES_DIRNAME, JOBQUEUE_FILENAME); + this.saveJobQueue(defaultFile); + } + + public void saveJobQueue(File path) { + if (!path.exists()) { + try { + path.createNewFile(); + } catch (IOException e) { + File parentDir = path.getParentFile(); + if (!parentDir.exists()) { + try { + if (parentDir.mkdirs() == true) { + try { + path.createNewFile(); + } catch (Exception ex) {} + } + } catch (Exception ex) {} + } + } + } + + String exceptionMessage = "Could not save the job queue file " + path.getAbsolutePath(); + + if (!path.exists()) { + DemoRecorderException ex = new DemoRecorderException(exceptionMessage); + DemoRecorderUtils.showNonCriticalErrorDialog(ex); + return; + } + + //make sure that for the next start of the program the state is set to waiting again + for (RecordJob job : this.jobs) { + if (job.getState() == RecordJob.State.PROCESSING) { + job.setState(RecordJob.State.WAITING); + } + job.setAppLayer(null); //we don't want to serialize the app layer! + } + + try { + FileOutputStream fout = new FileOutputStream(path); + ObjectOutputStream oos = new ObjectOutputStream(fout); + oos.writeObject(this.jobs); + oos.close(); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog(exceptionMessage, e, true); + } + + //we sometimes also save the jobqueue and don't exit the program, so restore the applayer again + for (RecordJob job : this.jobs) { + job.setAppLayer(this); + } + } + + public void shutDown() { + this.poolExecutor.shutDown(); + this.savePreferences(); + this.saveJobQueue(); + } + + public int getState() { + return this.state; + } + + private void loadPlugins() { + File pluginDir = DemoRecorderUtils.computeLocalFile(PLUGINS_DIRNAME, ""); + + if (!pluginDir.exists()) { + pluginDir.mkdir(); + } + + File[] jarFiles = pluginDir.listFiles(); + + List urlList = new ArrayList(); + for (File f : jarFiles) { + try { + urlList.add(f.toURI().toURL()); + } catch (MalformedURLException ex) {} + } + ClassLoader parentLoader = Thread.currentThread().getContextClassLoader(); + URL[] urls = new URL[urlList.size()]; + urls = urlList.toArray(urls); + URLClassLoader classLoader = new URLClassLoader(urls, parentLoader); + + ServiceLoader loader = ServiceLoader.load(EncoderPlugin.class, classLoader); + for (EncoderPlugin implementation : loader) { + this.encoderPlugins.add(implementation); + } + } + + private void configurePlugins() { + for (EncoderPlugin plugin : this.encoderPlugins) { + plugin.setApplicationLayer(this); + Properties pluginPreferences = plugin.getGlobalPreferences(); + for (Object preference : pluginPreferences.keySet()) { + String preferenceString = (String) preference; + + if (this.preferences.getProperty(plugin.getName(), preferenceString) == null) { + String defaultValue = pluginPreferences.getProperty(preferenceString); + this.preferences.setProperty(plugin.getName(), preferenceString, defaultValue); + } + } + } + } + + public List getEncoderPlugins() { + return encoderPlugins; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderException.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderException.java new file mode 100644 index 000000000..f9318f456 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderException.java @@ -0,0 +1,13 @@ +package com.nexuiz.demorecorder.application; + +public class DemoRecorderException extends RuntimeException { + + private static final long serialVersionUID = 965053013957793155L; + public DemoRecorderException(String message) { + super(message); + } + public DemoRecorderException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderUtils.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderUtils.java new file mode 100644 index 000000000..e46e7edec --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/DemoRecorderUtils.java @@ -0,0 +1,95 @@ +package com.nexuiz.demorecorder.application; + +import java.io.File; +import java.io.IOException; + +import org.jdesktop.swingx.JXErrorPane; +import org.jdesktop.swingx.error.ErrorInfo; + +public class DemoRecorderUtils { + + public static void showNonCriticalErrorDialog(Throwable e) { + if (!(e instanceof DemoRecorderException)) { + e = new DemoRecorderException("Internal error", e); + } + ErrorInfo info = new ErrorInfo("Error occurred", e.getMessage(), null, null, e, null, null); + JXErrorPane.showDialog(null, info); + } + + /** + * Shows an error dialog that contains the stack trace, catching the exception so that the program flow + * won't be interrupted. + * This method will maybe wrap e in a DemoRecorderException with the given message. + * @param customMessage + * @param e + * @param wrapException set to true if Exception should be wrapped into a DemoRecorderException + */ + public static void showNonCriticalErrorDialog(String customMessage, Throwable e, boolean wrapException) { + Throwable ex = e; + if (wrapException && !(e instanceof DemoRecorderException)) { + ex = new DemoRecorderException(customMessage, e); + } + + ErrorInfo info = new ErrorInfo("Error occurred", ex.getMessage(), null, null, ex, null, null); + JXErrorPane.showDialog(null, info); + } + + public static File computeLocalFile(String subDir, String fileName) { + String path = System.getProperty("user.dir"); + if (subDir != null && !subDir.equals("")) { + path += File.separator + subDir; + } + path += File.separator + fileName; + return new File(path); + } + + /** + * Returns just the name of the file for a given File. E.g. if the File points to + * /home/someuser/somedir/somefile.end the function will return "somefile.end" + * @param file + * @return just the name of the file + */ + public static String getJustFileNameOfPath(File file) { + String fileString = file.getAbsolutePath(); + int lastIndex = fileString.lastIndexOf(File.separator); + String newString = fileString.substring(lastIndex+1, fileString.length()); + return newString; + } + + /** + * Attempts to create an empty file (unless it already exists), including the creation + * of parent directories. If it succeeds to do so (or if the file already existed), true + * will be returned. Otherwise false will be returned + * @param file the file to be created + * @return true if file already existed or could successfully created, false otherwise + */ + public static boolean attemptFileCreation(File file) { + if (!file.exists()) { + try { + file.createNewFile(); + return true; + } catch (IOException e) { + File parentDir = file.getParentFile(); + if (!parentDir.exists()) { + try { + if (parentDir.mkdirs() == true) { + try { + file.createNewFile(); + return true; + } catch (Exception ex) {} + } + } catch (Exception ex) {} + } + return false; + } + } else { + return true; + } + } + + public static final String getFileExtension(File file) { + String fileName = file.getAbsolutePath(); + String ext = (fileName.lastIndexOf(".") == -1) ? "" : fileName.substring(fileName.lastIndexOf(".") + 1,fileName.length()); + return ext; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/NDRPreferences.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/NDRPreferences.java new file mode 100644 index 000000000..b599e4aea --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/NDRPreferences.java @@ -0,0 +1,66 @@ +package com.nexuiz.demorecorder.application; + +import java.util.Properties; + +/** + * Class that stores the application and global plug-in preferences of the Nexuiz + * Demo Recorder application. Set and Get property methods have been modified to + * now supply a category. + */ +public class NDRPreferences extends Properties { + + private static final long serialVersionUID = 4363913054294979418L; + private static final String CONCATENATOR = "."; + /** + * Category that defines a setting to be a setting of the NDR application itself + * (and not of one of the plugins). + */ + public static final String MAIN_APPLICATION = "NDR"; + + /** + * Searches for the property with the specified key in this property list. + * If the key is not found in this property list, the default property list, + * and its defaults, recursively, are then checked. The method returns + * null if the property is not found. + * + * @param category the category of the setting + * @param key the property key. + * @return the value in this property list with the specified category+key value. + */ + public String getProperty(String category, String key) { + return getProperty(getConcatenatedKey(category, key)); + } + + /** + * Calls the Hashtable method put. Provided for + * parallelism with the getProperty method. Enforces use of + * strings for property keys and values. The value returned is the + * result of the Hashtable call to put. + * + * @param category the category of the setting + * @param key the key to be placed into this property list. + * @param value the value corresponding to key. + * @return the previous value of the specified key in this property + * list, or null if it did not have one. + */ + public void setProperty(String category, String key, String value) { + setProperty(getConcatenatedKey(category, key), value); + } + + /** + * Returns only the category of a key that is a concatenated string of category and key. + * @param concatenatedString + * @return + */ + public static String getCategory(String concatenatedString) { + return concatenatedString.substring(0, concatenatedString.indexOf(CONCATENATOR)); + } + + public static String getKey(String concatenatedString) { + return concatenatedString.substring(concatenatedString.indexOf(CONCATENATOR) + 1, concatenatedString.length()); + } + + public static String getConcatenatedKey(String category, String key) { + return category + CONCATENATOR + key; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/RecorderJobPoolExecutor.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/RecorderJobPoolExecutor.java new file mode 100644 index 000000000..7f4181f68 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/RecorderJobPoolExecutor.java @@ -0,0 +1,48 @@ +package com.nexuiz.demorecorder.application; + + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import com.nexuiz.demorecorder.application.jobs.RecordJob; + +public class RecorderJobPoolExecutor { + + private int poolSize = 1; + private int maxPoolSize = 1; + private long keepAliveTime = 10; + private ThreadPoolExecutor threadPool = null; + private ArrayBlockingQueue queue = null; + + public RecorderJobPoolExecutor() { + queue = new ArrayBlockingQueue(99999); + threadPool = new ThreadPoolExecutor(poolSize, maxPoolSize, keepAliveTime, TimeUnit.SECONDS, queue); + } + + public void runJob(Runnable task) { + threadPool.execute(task); + } + + public void clearUnfinishedJobs() { + threadPool.getQueue().clear(); + } + + public void shutDown() { + threadPool.shutdownNow(); + } + + public synchronized List getJobList() { + List list = new ArrayList(); + for (Runnable job : this.queue) { + try { + RecordJob j = (RecordJob)job; + list.add(j); + } catch (ClassCastException e) {} + } + + return list; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutter.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutter.java new file mode 100644 index 000000000..0aff553fd --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutter.java @@ -0,0 +1,220 @@ +package com.nexuiz.demorecorder.application.democutter; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +public class DemoCutter { + + private static final byte CDTRACK_SEPARATOR = 0x0A; + + private DataInputStream inStream; + private DataOutputStream outStream; + private File inFile; + private File outFile; + + /** + * Calls the cutDemo method with reasonable default values for the second and first fast-forward stage. + * @param inFile @see other cutDemo method + * @param outFile @see other cutDemo method + * @param startTime @see other cutDemo method + * @param endTime @see other cutDemo method + * @param injectAtStart @see other cutDemo method + * @param injectBeforeCap @see other cutDemo method + * @param injectAfterCap @see other cutDemo method + */ + public void cutDemo(File inFile, File outFile, float startTime, float endTime, String injectAtStart, String injectBeforeCap, String injectAfterCap) { + this.cutDemo(inFile, outFile, startTime, endTime, injectAtStart, injectBeforeCap, injectAfterCap, 100, 10); + } + + /** + * Cuts the demo by injecting a 2-phase fast forward command until startTime is reached, then injects the cl_capturevideo 1 command + * and once endTime is reached the cl_capturevideo 0 command is injected. + * @param inFile the original demo file + * @param outFile the new cut demo file + * @param startTime when to start capturing (use the gametime in seconds) + * @param endTime when to stop capturing + * @param injectAtStart a String that will be injected right at the beginning of the demo + * can be anything that would make sense and can be parsed by DP's console + * @param injectBeforeCap a String that will be injected 5 seconds before capturing starts + * @param injectAfterCap a String that will be injected shortly after capturing ended + * @param ffwSpeedFirstStage fast-forward speed at first stage, when the startTime is still about a minute away (use high values, e.g. 100) + * @param ffwSpeedSecondStage fast-forward speed when coming a few seconds close to startTime, use lower values e.g. 5 or 10 + */ + public void cutDemo(File inFile, File outFile, float startTime, float endTime, String injectAtStart, String injectBeforeCap, String injectAfterCap, int ffwSpeedFirstStage, int ffwSpeedSecondStage) { + this.inFile = inFile; + this.outFile = outFile; + this.prepareStreams(); + this.readCDTrack(); + injectAfterCap = this.checkInjectString(injectAfterCap); + injectAtStart = this.checkInjectString(injectAtStart); + injectBeforeCap = this.checkInjectString(injectBeforeCap); + + byte[] data; + float svctime = -1; + boolean firstLoop = true; + String injectBuffer = ""; + int demoStarted = 0; + boolean endIsReached = false; + boolean finalInjectionDone = false; + boolean disconnectIssued = false; + int svcLoops = 0; + float firstSvcTime = -1; + float lastSvcTime = -1; + + try { + while (true) { + DemoPacket demoPacket = new DemoPacket(this.inStream); + if (demoPacket.isEndOfFile()) { + break; + } + + if (demoPacket.isClientToServerPacket()) { + try { + this.outStream.write(demoPacket.getOriginalLengthAsByte()); + this.outStream.write(demoPacket.getAngles()); + this.outStream.write(demoPacket.getOriginalData()); + } catch (IOException e) { + throw new DemoCutterException("Unexpected I/O Exception occurred when writing to the cut demo", e); + } + + continue; + } + + if (demoPacket.getSvcTime() != -1) { + svctime = demoPacket.getSvcTime(); + } + + if (svctime != -1) { + if (firstSvcTime == -1) { + firstSvcTime = svctime; + } + lastSvcTime = svctime; + + if (firstLoop) { + injectBuffer = "\011\n" + injectAtStart + ";slowmo " + ffwSpeedFirstStage + "\n\000"; + firstLoop = false; + } + if (demoStarted < 1 && svctime > (startTime - 50)) { + if (svcLoops == 0) { + //make sure that for short demos (duration less than 50 sec) + //the injectAtStart is still honored + injectBuffer = "\011\n" + injectAtStart + ";slowmo " + ffwSpeedSecondStage + "\n\000"; + } else { + injectBuffer = "\011\nslowmo " + ffwSpeedSecondStage + "\n\000"; + } + + demoStarted = 1; + } + if (demoStarted < 2 && svctime > (startTime - 5)) { + injectBuffer = "\011\nslowmo 1;" + injectBeforeCap +"\n\000"; + demoStarted = 2; + } + if (demoStarted < 3 && svctime > startTime) { + injectBuffer = "\011\ncl_capturevideo 1\n\000"; + demoStarted = 3; + } + if (!endIsReached && svctime > endTime) { + injectBuffer = "\011\ncl_capturevideo 0\n\000"; + endIsReached = true; + } + if (endIsReached && !finalInjectionDone && svctime > (endTime + 1)) { + injectBuffer = "\011\n" + injectAfterCap + "\n\000"; + finalInjectionDone = true; + } + if (finalInjectionDone && !disconnectIssued && svctime > (endTime + 2)) { + injectBuffer = "\011\ndisconnect\n\000"; + disconnectIssued = true; + } + svcLoops++; + } + + byte[] injectBufferAsBytes = null; + try { + injectBufferAsBytes = injectBuffer.getBytes("US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new DemoCutterException("Could not convert String to bytes using US-ASCII charset!", e); + } + + data = demoPacket.getOriginalData(); + if ((injectBufferAsBytes.length + data.length) < 65536) { + data = DemoCutterUtils.mergeByteArrays(injectBufferAsBytes, data); + injectBuffer = ""; + } + + byte[] newLengthLittleEndian = DemoCutterUtils.convertLittleEndian(data.length); + try { + this.outStream.write(newLengthLittleEndian); + this.outStream.write(demoPacket.getAngles()); + this.outStream.write(data); + } catch (IOException e) { + throw new DemoCutterException("Unexpected I/O Exception occurred when writing to the cut demo", e); + } + + } + + if (startTime < firstSvcTime) { + throw new DemoCutterException("Start time for the demo is " + startTime + ", but demo doesn't start before " + firstSvcTime); + } + if (endTime > lastSvcTime) { + throw new DemoCutterException("End time for the demo is " + endTime + ", but demo already stops at " + lastSvcTime); + } + } catch (DemoCutterException e) { + throw e; + } catch (Throwable e) { + throw new DemoCutterException("Internal error in demo cutter sub-route (invalid demo file?)", e); + } finally { + try { + this.outStream.close(); + this.inStream.close(); + } catch (IOException e) {} + } + } + + + + /** + * Seeks forward in the inStream until CDTRACK_SEPARATOR byte was reached. + * All the content is copied to the outStream. + */ + private void readCDTrack() { + byte lastByte; + try { + while ((lastByte = inStream.readByte()) != CDTRACK_SEPARATOR) { + this.outStream.write(lastByte); + } + this.outStream.write(CDTRACK_SEPARATOR); + } catch (EOFException e) { + throw new DemoCutterException("Unexpected EOF occurred when reading CD track of demo " + inFile.getPath(), e); + } + catch (IOException e) { + throw new DemoCutterException("Unexpected I/O Exception occurred when reading CD track of demo " + inFile.getPath(), e); + } + } + + private void prepareStreams() { + try { + this.inStream = new DataInputStream(new FileInputStream(this.inFile)); + } catch (FileNotFoundException e) { + throw new DemoCutterException("Could not open demo file " + inFile.getPath(), e); + } + + try { + this.outStream = new DataOutputStream(new FileOutputStream(this.outFile)); + } catch (FileNotFoundException e) { + throw new DemoCutterException("Could not open demo file " + outFile.getPath(), e); + } + } + + private String checkInjectString(String injectionString) { + while (injectionString.endsWith(";") || injectionString.endsWith("\n")) { + injectionString = injectionString.substring(0, injectionString.length()-1); + } + return injectionString; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutterException.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutterException.java new file mode 100644 index 000000000..6b6666b51 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutterException.java @@ -0,0 +1,14 @@ +package com.nexuiz.demorecorder.application.democutter; + +public class DemoCutterException extends RuntimeException { + + private static final long serialVersionUID = -1419472153834762285L; + + public DemoCutterException(String message) { + super(message); + } + public DemoCutterException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutterUtils.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutterUtils.java new file mode 100644 index 000000000..dd8f9ca45 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoCutterUtils.java @@ -0,0 +1,40 @@ +package com.nexuiz.demorecorder.application.democutter; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + + +public class DemoCutterUtils { + + public static float byteArrayToFloat(byte[] array) { + byte[] tmp = new byte[4]; + System.arraycopy(array, 0, tmp, 0, 4); + int accum = 0; + int i = 0; + for (int shiftBy = 0; shiftBy < 32; shiftBy += 8) { + accum |= ((long) (tmp[i++] & 0xff)) << shiftBy; + } + return Float.intBitsToFloat(accum); + } + + public static byte[] convertLittleEndian(int i) { + ByteBuffer bb = ByteBuffer.allocate(4); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.putInt(i); + return bb.array(); + } + + public static byte[] mergeByteArrays(byte[] array1, byte[] array2) { + ByteBuffer bb = ByteBuffer.allocate(array1.length + array2.length); + bb.put(array1); + bb.put(array2); + return bb.array(); + } + + public static int convertLittleEndian(byte[] b) { + ByteBuffer bb = ByteBuffer.allocate(4); + bb.order(ByteOrder.LITTLE_ENDIAN); + bb.put(b); + bb.position(0); + return bb.getInt(); + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoPacket.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoPacket.java new file mode 100644 index 000000000..dcca8219b --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/democutter/DemoPacket.java @@ -0,0 +1,102 @@ +package com.nexuiz.demorecorder.application.democutter; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.nio.ByteBuffer; + + +public class DemoPacket { + + private static final int DEMOMSG_CLIENT_TO_SERVER = 0x80000000; + + private DataInputStream inStream = null; + private boolean isEndOfFile = false; + private byte[] buffer = new byte[4]; //contains packet length + private byte[] angles = new byte[12]; + private byte[] data; + private int packetLength; + private boolean isClientToServer = false; + private float svcTime = -1; + + public DemoPacket(DataInputStream inStream) { + this.inStream = inStream; + + try { + inStream.readFully(buffer); + } catch (EOFException e) { + this.isEndOfFile = true; + return; + } catch (IOException e) { + throw new DemoCutterException("Unexpected I/O Exception occurred when processing demo"); + } + + packetLength = DemoCutterUtils.convertLittleEndian(buffer); + if ((packetLength & DEMOMSG_CLIENT_TO_SERVER) != 0) { + packetLength = packetLength & ~DEMOMSG_CLIENT_TO_SERVER; + + this.isClientToServer = true; + this.readAnglesAndData(); + return; + } + + this.readAnglesAndData(); + + // extract svc_time + this.readSvcTime(); + + } + + public boolean isEndOfFile() { + return this.isEndOfFile; + } + + public boolean isClientToServerPacket() { + return this.isClientToServer; + } + + public byte[] getOriginalLengthAsByte() { + return this.buffer; + } + + public byte[] getAngles() { + return this.angles; + } + + public byte[] getOriginalData() { + return this.data; + } + + public float getSvcTime() { + return this.svcTime; + } + + private void readAnglesAndData() { + // read angles + try { + inStream.readFully(angles); + } catch (EOFException e) { + throw new DemoCutterException("Invalid Demo Packet"); + } catch (IOException e) { + throw new DemoCutterException("Unexpected I/O Exception occurred when processing demo"); + } + + // read data + data = new byte[packetLength]; + try { + inStream.readFully(data); + } catch (EOFException e) { + throw new DemoCutterException("Invalid Demo Packet"); + } catch (IOException e) { + throw new DemoCutterException("Unexpected I/O Exception occurred when processing demo"); + } + } + + private void readSvcTime() { + if (data[0] == 0x07) { + ByteBuffer bb = ByteBuffer.allocate(4); + bb.put(data, 1, 4); + byte[] array = bb.array(); + this.svcTime = DemoCutterUtils.byteArrayToFloat(array); + } + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/EncoderJob.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/EncoderJob.java new file mode 100644 index 000000000..430c98890 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/EncoderJob.java @@ -0,0 +1,24 @@ +package com.nexuiz.demorecorder.application.jobs; + +import com.nexuiz.demorecorder.application.plugins.EncoderPlugin; + +/** + * Job for the ThreadPoolExecutor that will just call the encoder-plugin's execute + * method. + */ +public class EncoderJob implements Runnable { + + private RecordJob job; + private EncoderPlugin plugin; + + public EncoderJob(RecordJob job, EncoderPlugin plugin) { + this.job = job; + this.plugin = plugin; + } + + @Override + public void run() { + this.job.executePlugin(this.plugin); + } + +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordJob.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordJob.java new file mode 100644 index 000000000..429ff9829 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordJob.java @@ -0,0 +1,636 @@ +package com.nexuiz.demorecorder.application.jobs; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; +import com.nexuiz.demorecorder.application.DemoRecorderException; +import com.nexuiz.demorecorder.application.DemoRecorderUtils; +import com.nexuiz.demorecorder.application.NDRPreferences; +import com.nexuiz.demorecorder.application.DemoRecorderApplication.Preferences; +import com.nexuiz.demorecorder.application.democutter.DemoCutter; +import com.nexuiz.demorecorder.application.democutter.DemoCutterException; +import com.nexuiz.demorecorder.application.plugins.EncoderPlugin; +import com.nexuiz.demorecorder.application.plugins.EncoderPluginException; + +public class RecordJob implements Runnable, Serializable { + + private static final long serialVersionUID = -4585637490345587912L; + + public enum State { + WAITING, PROCESSING, ERROR, ERROR_PLUGIN, DONE + } + + public static final String CUT_DEMO_FILE_SUFFIX = "_autocut"; + public static final String CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE = "autocap"; + public static final String CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE = "1234567"; + protected static final String[] VIDEO_FILE_ENDINGS = {"avi", "ogv"}; + + private DemoRecorderApplication appLayer; + protected String jobName; + private int jobIndex; + protected File enginePath; + protected String engineParameters; + protected File demoFile; + protected String relativeDemoPath; + protected File dpVideoPath; + protected File videoDestination; + protected String executeBeforeCap; + protected String executeAfterCap; + protected float startSecond; + protected float endSecond; + protected State state = State.WAITING; + protected DemoRecorderException lastException = null; + + /** + * Points to the actual final file, including possible suffixes, e.g. _copy1, and the actualy ending + */ + protected File actualVideoDestination = null; + /** + * Map that identifies the plug-in by its name (String) and maps to the plug-in's job-specific settings + */ + protected Map encoderPluginSettings = new HashMap(); + + private List cleanUpFiles = null; + + public RecordJob( + DemoRecorderApplication appLayer, + String jobName, + int jobIndex, + File enginePath, + String engineParameters, + File demoFile, + String relativeDemoPath, + File dpVideoPath, + File videoDestination, + String executeBeforeCap, + String executeAfterCap, + float startSecond, + float endSecond + ) { + this.appLayer = appLayer; + this.jobName = jobName; + this.jobIndex = jobIndex; + + this.setEnginePath(enginePath); + this.setEngineParameters(engineParameters); + this.setDemoFile(demoFile); + this.setRelativeDemoPath(relativeDemoPath); + this.setDpVideoPath(dpVideoPath); + this.setVideoDestination(videoDestination); + this.setExecuteBeforeCap(executeBeforeCap); + this.setExecuteAfterCap(executeAfterCap); + this.setStartSecond(startSecond); + this.setEndSecond(endSecond); + } + + public RecordJob(){} + + /** + * Constructor that can be used by other classes such as job templates. Won't throw exceptions + * as it won't check the input for validity. + */ + protected RecordJob( + File enginePath, + String engineParameters, + File demoFile, + String relativeDemoPath, + File dpVideoPath, + File videoDestination, + String executeBeforeCap, + String executeAfterCap, + float startSecond, + float endSecond + ) { + this.jobIndex = -1; + this.enginePath = enginePath; + this.engineParameters = engineParameters; + this.demoFile = demoFile; + this.relativeDemoPath = relativeDemoPath; + this.dpVideoPath = dpVideoPath; + this.videoDestination = videoDestination; + this.executeBeforeCap = executeBeforeCap; + this.executeAfterCap = executeAfterCap; + this.startSecond = startSecond; + this.endSecond = endSecond; + } + + public void execute() { + if (this.state == State.PROCESSING) { + return; + } + boolean errorOccurred = false; + this.setState(State.PROCESSING); + this.appLayer.fireUserInterfaceUpdate(this); + cleanUpFiles = new ArrayList(); + + File cutDemo = computeCutDemoFile(); + cutDemo.delete(); //delete possibly old cutDemoFile + + EncoderPlugin recentEncoder = null; + + try { + this.cutDemo(cutDemo); + this.removeOldAutocaps(); + this.recordClip(cutDemo); + this.moveRecordedClip(); + for (EncoderPlugin plugin : this.appLayer.getEncoderPlugins()) { + recentEncoder = plugin; + plugin.executeEncoder(this); + } + } catch (DemoRecorderException e) { + errorOccurred = true; + this.lastException = e; + this.setState(State.ERROR); + } catch (EncoderPluginException e) { + errorOccurred = true; + this.lastException = new DemoRecorderException("Encoder plug-in " + recentEncoder.getName() + " failed: " + + e.getMessage(), e); + this.setState(State.ERROR_PLUGIN); + } catch (Exception e) { + errorOccurred = true; + this.lastException = new DemoRecorderException("Executing job failed, click on details for more info", e); + } finally { + NDRPreferences preferences = this.appLayer.getPreferences(); + if (!Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DO_NOT_DELETE_CUT_DEMOS))) { + cleanUpFiles.add(cutDemo); + } + if (!errorOccurred) { + this.setState(State.DONE); + } + this.cleanUpFiles(); + this.appLayer.fireUserInterfaceUpdate(this); + this.appLayer.saveJobQueue(); + } + } + + /** + * Will execute just the specified encoder plug-in on an already "done" job. + * @param pluginName + */ + public void executePlugin(EncoderPlugin plugin) { + if (this.getState() != State.DONE) { + return; + } + this.setState(State.PROCESSING); + this.appLayer.fireUserInterfaceUpdate(this); + + try { + plugin.executeEncoder(this); + this.setState(State.DONE); + } catch (EncoderPluginException e) { + this.lastException = new DemoRecorderException("Encoder plug-in " + plugin.getName() + " failed: " + + e.getMessage(), e); + this.setState(State.ERROR_PLUGIN); + } + + this.appLayer.fireUserInterfaceUpdate(this); + } + + private void cleanUpFiles() { + try { + for (File f : this.cleanUpFiles) { + f.delete(); + } + } catch (Exception e) {} + + } + + private void moveRecordedClip() { + //1. Figure out whether the file is .avi or .ogv + File sourceFile = null; + for (String videoExtension : VIDEO_FILE_ENDINGS) { + String fileString = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE + + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + "." + videoExtension; + File videoFile = new File(fileString); + if (videoFile.exists()) { + sourceFile = videoFile; + break; + } + } + + if (sourceFile == null) { + String p = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE + + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE; + throw new DemoRecorderException("Could not locate the expected video file being generated by Nexuiz (should have been at " + + p + ".avi/.ogv"); + } + cleanUpFiles.add(sourceFile); + + File destinationFile = null; + NDRPreferences preferences = this.appLayer.getPreferences(); + String sourceFileExtension = DemoRecorderUtils.getFileExtension(sourceFile); + String destinationFilePath = this.videoDestination + "." + sourceFileExtension; + destinationFile = new File(destinationFilePath); + if (destinationFile.exists()) { + if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.OVERWRITE_VIDEO_FILE))) { + if (!destinationFile.delete()) { + throw new DemoRecorderException("Could not delete the existing video destinatin file " + destinationFile.getAbsolutePath() + + " (application setting to overwrite existing video files is enabled!)"); + } + } else { + destinationFilePath = this.videoDestination + "_copy" + this.getVideoDestinationCopyNr(sourceFileExtension) + "." + sourceFileExtension; + destinationFile = new File(destinationFilePath); + } + } + + //finally move the file + if (!sourceFile.renameTo(destinationFile)) { + cleanUpFiles.add(destinationFile); + throw new DemoRecorderException("Could not move the video file from " + sourceFile.getAbsolutePath() + + " to " + destinationFile.getAbsolutePath()); + } + + this.actualVideoDestination = destinationFile; + } + + /** + * As destination video files, e.g. "test"[.avi] can already exist, we have to save the + * the video file to a file name such as test_copy1 or test_copy2. + * This function will figure out what the number (1, 2....) is. + * @return + */ + private int getVideoDestinationCopyNr(String sourceFileExtension) { + int i = 1; + File lastFile; + while (true) { + lastFile = new File(this.videoDestination + "_copy" + i + "." + sourceFileExtension); + if (!lastFile.exists()) { + break; + } + + i++; + } + return i; + } + + private File computeCutDemoFile() { + String origFileString = this.demoFile.getAbsolutePath(); + int lastIndex = origFileString.lastIndexOf(File.separator); + String autoDemoFileName = origFileString.substring(lastIndex+1, origFileString.length()); + //strip .dem ending + autoDemoFileName = autoDemoFileName.substring(0, autoDemoFileName.length()-4); + autoDemoFileName = autoDemoFileName + CUT_DEMO_FILE_SUFFIX + ".dem"; + String finalString = origFileString.substring(0, lastIndex) + File.separator + autoDemoFileName; + File f = new File(finalString); + + return f; + } + + private void cutDemo(File cutDemo) { + String injectAtStart = ""; + String injectBeforeCap = ""; + String injectAfterCap = ""; + + NDRPreferences preferences = this.appLayer.getPreferences(); + if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_RENDERING))) { + injectAtStart += "r_render 0;"; + injectBeforeCap += "r_render 1;"; + } + if (Boolean.valueOf(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.DISABLE_SOUND))) { + injectAtStart += "set _volume $volume;volume 0;"; + injectBeforeCap += "set volume $_volume;"; + } + injectBeforeCap += this.executeBeforeCap + "\n"; + injectBeforeCap += "set _cl_capturevideo_nameformat $cl_capturevideo_nameformat;set _cl_capturevideo_number $cl_capturevideo_number;"; + injectBeforeCap += "cl_capturevideo_nameformat " + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE + ";"; + injectBeforeCap += "cl_capturevideo_number " + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + ";"; + + injectAfterCap += this.executeAfterCap + "\n"; + injectAfterCap += "cl_capturevideo_nameformat $_cl_capturevideo_nameformat;cl_capturevideo_number $_cl_capturevideo_number;"; + + + DemoCutter cutter = new DemoCutter(); + int fwdSpeedFirstStage, fwdSpeedSecondStage; + try { + fwdSpeedFirstStage = Integer.parseInt(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_FIRST_STAGE)); + fwdSpeedSecondStage = Integer.parseInt(preferences.getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.FFW_SPEED_SECOND_STAGE)); + } catch (NumberFormatException e) { + throw new DemoRecorderException("Make sure that you specified valid numbers for the settings " + + Preferences.FFW_SPEED_FIRST_STAGE + " and " + Preferences.FFW_SPEED_SECOND_STAGE, e); + } + + try { + cutter.cutDemo( + this.demoFile, + cutDemo, + this.startSecond, + this.endSecond, + injectAtStart, + injectBeforeCap, + injectAfterCap, + fwdSpeedFirstStage, + fwdSpeedSecondStage + ); + } catch (DemoCutterException e) { + throw new DemoRecorderException("Error occurred while trying to cut the demo: " + e.getMessage(), e); + } + + } + + private void removeOldAutocaps() { + for (String videoExtension : VIDEO_FILE_ENDINGS) { + String fileString = this.dpVideoPath.getAbsolutePath() + File.separator + CUT_DEMO_CAPVIDEO_NAMEFORMAT_OVERRIDE + + CUT_DEMO_CAPVIDEO_NUMBER_OVERRIDE + "." + videoExtension; + File videoFile = new File(fileString); + cleanUpFiles.add(videoFile); + if (videoFile.exists()) { + if (!videoFile.delete()) { + throw new DemoRecorderException("Could not delete old obsolete video file " + fileString); + } + } + } + } + + private void recordClip(File cutDemo) { + Process nexProc; + String demoFileName = DemoRecorderUtils.getJustFileNameOfPath(cutDemo); + String execPath = this.enginePath.getAbsolutePath() + " " + this.engineParameters + " -demo " + + this.relativeDemoPath + "/" + demoFileName; + File engineDir = this.enginePath.getParentFile(); + try { + nexProc = Runtime.getRuntime().exec(execPath, null, engineDir); + nexProc.getErrorStream(); + nexProc.getOutputStream(); + InputStream is = nexProc.getInputStream(); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + while (br.readLine() != null) { + //System.out.println(line); + } + } catch (IOException e) { + throw new DemoRecorderException("I/O Exception occurred when trying to execute the Nexuiz binary", e); + } + } + + public void run() { + this.execute(); + } + + public void setAppLayer(DemoRecorderApplication appLayer) { + this.appLayer = appLayer; + } + + public int getJobIndex() { + return jobIndex; + } + + public File getEnginePath() { + return enginePath; + } + + public void setEnginePath(File enginePath) { + this.checkForProcessingState(); + if (enginePath == null || !enginePath.exists()) { + throw new DemoRecorderException("Could not locate engine binary!"); + } + if (!enginePath.canExecute()) { + throw new DemoRecorderException("The file you specified is not executable!"); + } + this.enginePath = enginePath.getAbsoluteFile(); + } + + public String getEngineParameters() { + return engineParameters; + } + + public void setEngineParameters(String engineParameters) { + this.checkForProcessingState(); + if (engineParameters == null) { + engineParameters = ""; + } + this.engineParameters = engineParameters.trim(); + } + + public File getDemoFile() { + return demoFile; + } + + public void setDemoFile(File demoFile) { + this.checkForProcessingState(); + if (demoFile == null || !demoFile.exists()) { + throw new DemoRecorderException("Could not locate demo file!"); + } + if (!doReadWriteTest(demoFile.getParentFile())) { + throw new DemoRecorderException("The directory you specified for the demo to be recorded is not writable!"); + } + if (!demoFile.getAbsolutePath().endsWith(".dem")) { + throw new DemoRecorderException("The demo file you specified must have the ending .dem"); + } + + this.demoFile = demoFile.getAbsoluteFile(); + } + + public String getRelativeDemoPath() { + return relativeDemoPath; + } + + public void setRelativeDemoPath(String relativeDemoPath) { + this.checkForProcessingState(); + if (relativeDemoPath == null) { + relativeDemoPath = ""; + } + + //get rid of possible slashes + while (relativeDemoPath.startsWith("/") || relativeDemoPath.startsWith("\\")) { + relativeDemoPath = relativeDemoPath.substring(1, relativeDemoPath.length()); + } + while (relativeDemoPath.endsWith("/") || relativeDemoPath.endsWith("\\")) { + relativeDemoPath = relativeDemoPath.substring(0, relativeDemoPath.length() - 1); + } + + this.relativeDemoPath = relativeDemoPath.trim(); + } + + public File getDpVideoPath() { + return dpVideoPath; + } + + public void setDpVideoPath(File dpVideoPath) { + this.checkForProcessingState(); + if (dpVideoPath == null || !dpVideoPath.isDirectory()) { + throw new DemoRecorderException("Could not locate the specified DPVideo directory!"); + } + + if (!this.doReadWriteTest(dpVideoPath)) { + throw new DemoRecorderException("The DPVideo directory is not writable! It needs to be writable so that the file can be moved to its new location"); + } + this.dpVideoPath = dpVideoPath.getAbsoluteFile(); + } + + public File getVideoDestination() { + return videoDestination; + } + + public void setVideoDestination(File videoDestination) { + this.checkForProcessingState(); + //keep in mind, the parameter videoDestination points to the final avi/ogg file w/o extension! + if (videoDestination == null || !videoDestination.getParentFile().isDirectory()) { + throw new DemoRecorderException("Could not locate the specified video destination"); + } + + if (!this.doReadWriteTest(videoDestination.getParentFile())) { + throw new DemoRecorderException("The video destination directory is not writable! It needs to be writable so that the file can be moved to its new location"); + } + + this.videoDestination = videoDestination.getAbsoluteFile(); + } + + public String getExecuteBeforeCap() { + return executeBeforeCap; + } + + public void setExecuteBeforeCap(String executeBeforeCap) { + this.checkForProcessingState(); + if (executeBeforeCap == null) { + executeBeforeCap = ""; + } + executeBeforeCap = executeBeforeCap.trim(); + while (executeBeforeCap.endsWith(";")) { + executeBeforeCap = executeBeforeCap.substring(0, executeBeforeCap.length()-1); + } + this.executeBeforeCap = executeBeforeCap; + } + + public String getExecuteAfterCap() { + return executeAfterCap; + } + + public void setExecuteAfterCap(String executeAfterCap) { + this.checkForProcessingState(); + if (executeAfterCap == null) { + executeAfterCap = ""; + } + executeAfterCap = executeAfterCap.trim(); + while (executeAfterCap.endsWith(";")) { + executeAfterCap = executeAfterCap.substring(0, executeAfterCap.length()-1); + } + if (executeAfterCap.contains("cl_capturevideo_number") || executeAfterCap.contains("cl_capturevideo_nameformat")) { + throw new DemoRecorderException("Execute after String cannot contain cl_capturevideo_number or _nameformat changes!"); + } + this.executeAfterCap = executeAfterCap; + } + + public float getStartSecond() { + return startSecond; + } + + public void setStartSecond(float startSecond) { + this.checkForProcessingState(); + if (startSecond < 0) { + throw new DemoRecorderException("Start second cannot be < 0"); + } + this.startSecond = startSecond; + } + + public float getEndSecond() { + return endSecond; + } + + public void setEndSecond(float endSecond) { + this.checkForProcessingState(); + if (endSecond < this.startSecond) { + throw new DemoRecorderException("End second cannot be < start second"); + } + this.endSecond = endSecond; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + this.appLayer.fireUserInterfaceUpdate(this); + } + + public String getJobName() { + if (this.jobName == null || this.jobName.equals("")) { + return "Job " + this.jobIndex; + } + return this.jobName; + } + + public void setJobName(String jobName) { + if (jobName == null || jobName.equals("")) { + this.jobIndex = appLayer.getNewJobIndex(); + this.jobName = "Job " + this.jobIndex; + } else { + this.jobName = jobName; + } + } + + public DemoRecorderException getLastException() { + return lastException; + } + + /** + * Tests whether the given directory is writable by creating a file in there and deleting + * it again. + * @param directory + * @return true if directory is writable + */ + protected boolean doReadWriteTest(File directory) { + boolean writable = false; + String fileName = "tmp." + Math.random()*10000 + ".dat"; + File tempFile = new File(directory, fileName); + try { + writable = tempFile.createNewFile(); + if (writable) { + tempFile.delete(); + } + } catch (IOException e) { + writable = false; + } + return writable; + } + + private void checkForProcessingState() { + if (this.state == State.PROCESSING) { + throw new DemoRecorderException("Cannot modify this job while it is processing!"); + } + } + + public Properties getEncoderPluginSettings(EncoderPlugin plugin) { + if (this.encoderPluginSettings.containsKey(plugin.getName())) { + return this.encoderPluginSettings.get(plugin.getName()); + } else { + return new Properties(); + } + } + + public void setEncoderPluginSetting(String pluginName, String pluginSettingKey, String value) { + Properties p = this.encoderPluginSettings.get(pluginName); + if (p == null) { + p = new Properties(); + this.encoderPluginSettings.put(pluginName, p); + } + + p.put(pluginSettingKey, value); + } + + public Map getEncoderPluginSettings() { + return encoderPluginSettings; + } + + public void setEncoderPluginSettings(Map encoderPluginSettings) { + this.encoderPluginSettings = encoderPluginSettings; + } + + public File getActualVideoDestination() { + return actualVideoDestination; + } + + public void setActualVideoDestination(File actualVideoDestination) { + this.actualVideoDestination = actualVideoDestination; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordsDoneJob.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordsDoneJob.java new file mode 100644 index 000000000..d1bcd67a1 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/jobs/RecordsDoneJob.java @@ -0,0 +1,17 @@ +package com.nexuiz.demorecorder.application.jobs; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; + +public class RecordsDoneJob implements Runnable { + + private DemoRecorderApplication appLayer; + + public RecordsDoneJob(DemoRecorderApplication appLayer) { + this.appLayer = appLayer; + } + + public void run() { + this.appLayer.notifyAllJobsDone(); + } + +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/plugins/EncoderPlugin.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/plugins/EncoderPlugin.java new file mode 100644 index 000000000..b3c7de127 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/plugins/EncoderPlugin.java @@ -0,0 +1,69 @@ +package com.nexuiz.demorecorder.application.plugins; + +import java.util.Properties; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; +import com.nexuiz.demorecorder.application.jobs.RecordJob; + +public interface EncoderPlugin { + + /** + * Makes the application layer known to the plug-in, which is required so that the plug-in + * can access the preferences of the application. Call this method first before using any + * of the others. + */ + public void setApplicationLayer(DemoRecorderApplication appLayer); + + /** + * Returns the name of the plug-in. Must not contain a "." + */ + public String getName(); + + /** + * Returns true if the plug-in is enabled (checked from the preferences of the app layer) + * @return true if the plug-in is enabled + */ + public boolean isEnabled(); + + /** + * Global preferences are preferences of a plug-in that are application-wide and not job- + * specific. They should be shown in a global preferences dialog. + * Use this method in order to tell the application layer and GUI which global settings your + * encoder plug-in offers, and set a reasonable default. Note that for the default-values being + * set you can either set to "true" or "false", any String (can be empty), or "filechooser" if + * you want the user to select a file. + * @return + */ + public Properties getGlobalPreferences(); + + /** + * In order to influence the order of settings being displayed to the user in a UI, return an array + * of all keys used in the Properties object returned in getGlobalPreferences(), with your desired + * order. + * @return + */ + public String[] getGlobalPreferencesOrder(); + + /** + * Here you can return a Properties object that contains keys for values that can be specific to each + * individual RecordJob. + * @return + */ + public Properties getJobSpecificPreferences(); + + /** + * In order to influence the order of job-specific settings being displayed to the user in a UI, + * return an array of all keys used in the Properties object returned in getJobSpecificPreferences(), with + * your desired order. + * @return + */ + public String[] getJobSpecificPreferencesOrder(); + + /** + * Will be called by the application layer when a job has been successfully recorded and moved to its + * final destination. This method has to perform the specific tasks your plug-in is supposed to do. + * @param job + * @throws EncoderPluginException + */ + public void executeEncoder(RecordJob job) throws EncoderPluginException; +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/plugins/EncoderPluginException.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/plugins/EncoderPluginException.java new file mode 100644 index 000000000..70a98b30f --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/application/plugins/EncoderPluginException.java @@ -0,0 +1,14 @@ +package com.nexuiz.demorecorder.application.plugins; + +public class EncoderPluginException extends Exception { + + private static final long serialVersionUID = 2200737027476726978L; + + public EncoderPluginException(String message) { + super(message); + } + + public EncoderPluginException(String message, Throwable t) { + super(message, t); + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/main/Driver.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/main/Driver.java new file mode 100644 index 000000000..d7188b65b --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/main/Driver.java @@ -0,0 +1,18 @@ +package com.nexuiz.demorecorder.main; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; +import com.nexuiz.demorecorder.ui.swinggui.SwingGUI; +import com.nexuiz.demorecorder.ui.swinggui.utils.ShowErrorDialogExceptionHandler; + +public class Driver { + + public static void main(String[] args) { + SwingGUI.setSystemLAF(); + Thread.setDefaultUncaughtExceptionHandler(new ShowErrorDialogExceptionHandler()); + DemoRecorderApplication appLayer = new DemoRecorderApplication(); + + SwingGUI gui = new SwingGUI(appLayer); + appLayer.addUserInterfaceListener(gui); + + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/DemoRecorderUI.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/DemoRecorderUI.java new file mode 100644 index 000000000..c8dc3555d --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/DemoRecorderUI.java @@ -0,0 +1,20 @@ +package com.nexuiz.demorecorder.ui; + +import com.nexuiz.demorecorder.application.jobs.RecordJob; + +public interface DemoRecorderUI { + + /** + * Called by the application layer to inform the GUI about the fact that + * one or more properties of the given job changed (most likely the status). + * The given job might also be new to the GUI. + * @param job the affected job + */ + public void RecordJobPropertiesChange(RecordJob job); + + /** + * Called by the application layer to inform the GUI that it finished + * recording all assigned jobs. + */ + public void recordingFinished(); +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/JobDialog.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/JobDialog.java new file mode 100644 index 000000000..adb0f99dc --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/JobDialog.java @@ -0,0 +1,737 @@ +package com.nexuiz.demorecorder.ui.swinggui; + +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.ScrollPaneConstants; +import javax.swing.border.EmptyBorder; +import javax.swing.filechooser.FileFilter; + +import net.miginfocom.swing.MigLayout; + +import org.jdesktop.swingx.JXTable; +import org.jdesktop.swingx.JXTitledSeparator; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; +import com.nexuiz.demorecorder.application.DemoRecorderUtils; +import com.nexuiz.demorecorder.application.NDRPreferences; +import com.nexuiz.demorecorder.application.jobs.RecordJob; +import com.nexuiz.demorecorder.application.plugins.EncoderPlugin; +import com.nexuiz.demorecorder.ui.swinggui.tablemodels.RecordJobTemplatesTableModel; +import com.nexuiz.demorecorder.ui.swinggui.utils.SwingGUIUtils; + +/** + * Shows the dialog that allows to create a new job, create one from a template + * or edit an existing job. + */ + +public class JobDialog extends JDialog implements ActionListener { + private static final long serialVersionUID = 6926246716804560522L; + public static final int CREATE_NEW_JOB = 0; + public static final int EDIT_JOB = 1; + public static final int CREATE_NEW_TEMPLATE = 2; + public static final int EDIT_TEMPLATE = 3; + public static final int CREATE_JOB_FROM_TEMPLATE = 4; + + private DemoRecorderApplication appLayer; + private RecordJobTemplatesTableModel tableModel; +// private JXTable templatesTable; + private Frame parentFrame; + private int dialogType; + private RecordJob job = null; + private JPanel inputPanel; + private JPanel buttonPanel; + + private JTextField templateNameField; + private JTextField templateSummaryField; + private JTextField enginePathField; + private JButton enginePathChooserButton; + private JTextField engineParameterField; + private JTextField dpVideoDirField; + private JButton dpVideoDirChooserButton; + private JTextField relativeDemoPathField; + private JTextField jobNameField; + private JTextField demoFileField; + private JButton demoFileChooserButton; + private JTextField startSecondField; + private JTextField endSecondField; + private JTextArea execBeforeField; + private JTextArea execAfterField; + private JTextField videoDestinationField; + private JButton videoDestinationChooserButton; + + private JButton createButton; + private JButton cancelButton; + + //file choosers + private JFileChooser enginePathFC; + private JFileChooser dpVideoDirFC; + private JFileChooser demoFileFC; + private JFileChooser videoDestinationFC; + + private FileFilter userDirFilter = new NexuizUserDirFilter(); + + private Map pluginDialogSettings = new HashMap(); + + /** + * Constructor to create a dialog when creating a new job. + * @param owner + * @param appLayer + */ + public JobDialog(Frame owner, DemoRecorderApplication appLayer) { + super(owner, true); + this.parentFrame = owner; + this.dialogType = CREATE_NEW_JOB; + this.appLayer = appLayer; + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + + setTitle("Create new job"); + + this.setupLayout(); + } + + /** + * Constructor to create a dialog when creating a new template. + * @param owner + * @param dialogType + * @param appLayer + */ + public JobDialog(Frame owner, RecordJobTemplatesTableModel tableModel, JXTable templatesTable, DemoRecorderApplication appLayer) { + super(owner, true); + this.parentFrame = owner; + this.dialogType = CREATE_NEW_TEMPLATE; + this.tableModel = tableModel; + this.appLayer = appLayer; +// this.templatesTable = templatesTable; seems we don't need it + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setTitle("Create new template"); + + this.setupLayout(); + } + + /** + * Constructor to use when creating a new job from a template, or when editing a template. + * @param owner + * @param template + * @param type either CREATE_JOB_FROM_TEMPLATE or EDIT_TEMPLATE + */ + public JobDialog(Frame owner, RecordJobTemplate template, DemoRecorderApplication appLayer, int type) { + super(owner, true); + this.parentFrame = owner; + + this.job = template; + this.appLayer = appLayer; + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + + if (type != CREATE_JOB_FROM_TEMPLATE && type != EDIT_TEMPLATE) { + throw new RuntimeException("Illegal paraameter \"type\""); + } + + this.dialogType = type; + if (type == CREATE_JOB_FROM_TEMPLATE) { + setTitle("Create job from template"); + } else { + setTitle("Edit template"); + } + + this.setupLayout(); + } + + /** + * Constructor to create a dialog to be used when editing an existing job. + * @param owner + * @param job + */ + public JobDialog(Frame owner, RecordJob job, DemoRecorderApplication appLayer) { + super(owner, true); + this.parentFrame = owner; + this.dialogType = EDIT_JOB; + this.appLayer = appLayer; + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + + setTitle("Edit job"); + this.job = job; + + this.setupLayout(); + } + + + + public void showDialog() { + this.pack(); + Toolkit t = Toolkit.getDefaultToolkit(); + Dimension screenSize = t.getScreenSize(); + if (getHeight() > screenSize.height) { + Dimension newPreferredSize = getPreferredSize(); + newPreferredSize.height = screenSize.height - 100; + setPreferredSize(newPreferredSize); + this.pack(); + } + this.setLocationRelativeTo(this.parentFrame); + this.setVisible(true); + } + + private void setupLayout() { +// setLayout(new MigLayout("wrap 1", "[grow,fill]", "[]20[]")); + setLayout(new MigLayout("wrap 1", "[grow,fill]", "[][]")); + this.setupInputMask(); + this.setupButtonPart(); + + } + + private void setupInputMask() { + inputPanel = new JPanel(new MigLayout("insets 0,wrap 3", "[][250::,grow,fill][30::]")); + JScrollPane inputScrollPane = new JScrollPane(inputPanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + inputScrollPane.setBorder(new EmptyBorder(0,0,0,0)); + + JXTitledSeparator environmentHeading = new JXTitledSeparator("Environment settings"); + inputPanel.add(environmentHeading, "span 3,grow"); + + this.setupTemplateNameAndSummary(); + this.setupEnginePath(); + this.setupEngineParameters(); + this.setupDPVideoDir(); + this.setupRelativeDemoPath(); + + JXTitledSeparator jobSettingsHeading = new JXTitledSeparator("Job settings"); + inputPanel.add(jobSettingsHeading, "span 3,grow"); + + this.setupJobName(); + this.setupDemoFile(); + this.setupStartSecond(); + this.setupEndSecond(); + this.setupExecBefore(); + this.setupExecAfter(); + this.setupVideoDestination(); + + this.setupPluginPreferences(); + + getContentPane().add(inputScrollPane); + } + + private void setupTemplateNameAndSummary() { + if (this.dialogType != CREATE_NEW_TEMPLATE && this.dialogType != EDIT_TEMPLATE) { + return; + } + + //layout stuff + inputPanel.add(new JLabel("Template name:")); + templateNameField = new JTextField(); + inputPanel.add(templateNameField, "wrap"); + + inputPanel.add(new JLabel("Summary:")); + templateSummaryField = new JTextField(); + inputPanel.add(templateSummaryField, "wrap"); + + //UI logic stuff + if (this.dialogType == EDIT_TEMPLATE) { + RecordJobTemplate template = (RecordJobTemplate) this.job; + templateNameField.setText(template.getName()); + templateSummaryField.setText(template.getSummary()); + } + } + + private void setupEnginePath() { + //layout stuff + inputPanel.add(new JLabel("Engine:")); + enginePathField = new JTextField(); + enginePathField.setEditable(false); + inputPanel.add(enginePathField); + enginePathChooserButton = new FileChooserButton(); + inputPanel.add(enginePathChooserButton); + + //UI logic stuff + this.enginePathFC = createConfiguredFileChooser(); + enginePathChooserButton.addActionListener(this); + if (this.dialogType == EDIT_JOB || this.dialogType == EDIT_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + this.enginePathFC.setSelectedFile(this.job.getEnginePath()); + this.enginePathField.setText(this.job.getEnginePath().getAbsolutePath()); + } + } + + private void setupEngineParameters() { + //layout stuff + inputPanel.add(new JLabel("Engine parameters:")); + engineParameterField = new JTextField(); + inputPanel.add(engineParameterField, "wrap"); + + //UI logic stuff + if (this.dialogType == EDIT_JOB || this.dialogType == EDIT_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + engineParameterField.setText(this.job.getEngineParameters()); + } + } + + private void setupDPVideoDir() { + //layout stuff + inputPanel.add(new JLabel("DPVideo directory:")); + dpVideoDirField = new JTextField(); + dpVideoDirField.setEditable(false); + inputPanel.add(dpVideoDirField); + dpVideoDirChooserButton = new FileChooserButton(); + inputPanel.add(dpVideoDirChooserButton); + + //UI logic stuff + dpVideoDirChooserButton.addActionListener(this); + this.dpVideoDirFC = createConfiguredFileChooser(); + this.dpVideoDirFC.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + if (this.dialogType == EDIT_JOB || this.dialogType == EDIT_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + this.dpVideoDirFC.setSelectedFile(this.job.getDpVideoPath()); + this.dpVideoDirField.setText(this.job.getDpVideoPath().getAbsolutePath()); + } + } + + private void setupRelativeDemoPath() { + //layout stuff + inputPanel.add(new JLabel("Relative demo path:")); + relativeDemoPathField = new JTextField(); + inputPanel.add(relativeDemoPathField, "wrap 20"); + + //UI logic stuff + if (this.dialogType == CREATE_NEW_JOB || this.dialogType == CREATE_NEW_TEMPLATE) { + relativeDemoPathField.setText("demos"); + } + if (this.dialogType == EDIT_JOB || this.dialogType == EDIT_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + relativeDemoPathField.setText(this.job.getRelativeDemoPath()); + } + } + + private void setupJobName() { + inputPanel.add(new JLabel("Job name:")); + + jobNameField = new JTextField(); + inputPanel.add(jobNameField, "wrap"); + + //UI logic stuff + if (this.dialogType != CREATE_NEW_TEMPLATE && this.dialogType != CREATE_NEW_JOB) { + jobNameField.setText(this.job.getJobName()); + } + } + + private void setupDemoFile() { + String label; + if (this.dialogType == CREATE_NEW_JOB || this.dialogType == EDIT_JOB || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + label = "Demo file:"; + } else { + label = "Demo directory:"; + } + + //layout stuff + inputPanel.add(new JLabel(label)); + demoFileField = new JTextField(); + demoFileField.setEditable(false); + inputPanel.add(demoFileField); + demoFileChooserButton = new FileChooserButton(); + inputPanel.add(demoFileChooserButton); + + //UI logic stuff + this.demoFileFC = createConfiguredFileChooser(); + demoFileChooserButton.addActionListener(this); + if (this.dialogType == EDIT_JOB || this.dialogType == EDIT_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + if (this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + this.demoFileFC.setCurrentDirectory(this.job.getDemoFile()); + } else { + this.demoFileFC.setSelectedFile(this.job.getDemoFile()); + } + + this.demoFileField.setText(this.job.getDemoFile().getAbsolutePath()); + } + + //only specify directories for templates + if (this.dialogType == CREATE_NEW_TEMPLATE || this.dialogType == EDIT_TEMPLATE) { + this.demoFileFC.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + } + } + + private void setupStartSecond() { + //only exists for jobs, not for templates + if (this.dialogType != CREATE_NEW_JOB && this.dialogType != EDIT_JOB && this.dialogType != CREATE_JOB_FROM_TEMPLATE) { + return; + } + + //layout stuff + inputPanel.add(new JLabel("Start second:")); + startSecondField = new JTextField(); + inputPanel.add(startSecondField, "wrap"); + + //UI logic stuff + if (this.dialogType == EDIT_JOB) { + startSecondField.setText(String.valueOf( this.job.getStartSecond() )); + } + } + + private void setupEndSecond() { + //only exists for jobs, not for templates + if (this.dialogType != CREATE_NEW_JOB && this.dialogType != EDIT_JOB && this.dialogType != CREATE_JOB_FROM_TEMPLATE) { + return; + } + + //layout stuff + inputPanel.add(new JLabel("End second:")); + endSecondField = new JTextField(); + inputPanel.add(endSecondField, "wrap"); + + //UI logic stuff + if (this.dialogType == EDIT_JOB) { + endSecondField.setText(String.valueOf( this.job.getEndSecond() )); + } + } + + private void setupExecBefore() { + //layout stuff + inputPanel.add(new JLabel("Exec before capture:")); + execBeforeField = new JTextArea(3, 1); + inputPanel.add(new JScrollPane(execBeforeField), "wrap"); + + //UI logic stuff + if (this.dialogType == EDIT_JOB || this.dialogType == EDIT_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + execBeforeField.setText(this.job.getExecuteBeforeCap()); + } + } + + private void setupExecAfter() { + //layout stuff + inputPanel.add(new JLabel("Exec after capture:")); + execAfterField = new JTextArea(3, 1); + inputPanel.add(new JScrollPane(execAfterField), "wrap"); + + //UI logic stuff + if (this.dialogType == EDIT_JOB || this.dialogType == EDIT_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + execAfterField.setText(this.job.getExecuteAfterCap()); + } + } + + private void setupVideoDestination() { + //layout stuff + inputPanel.add(new JLabel("Video destination:")); + videoDestinationField = new JTextField(); + videoDestinationField.setEditable(false); + inputPanel.add(videoDestinationField); + videoDestinationChooserButton = new FileChooserButton(); + inputPanel.add(videoDestinationChooserButton, "wrap 20"); + + //UI logic stuff + videoDestinationChooserButton.addActionListener(this); + this.videoDestinationFC = createConfiguredFileChooser(); + if (this.dialogType == EDIT_JOB || this.dialogType == EDIT_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + if (this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + this.videoDestinationFC.setCurrentDirectory(this.job.getVideoDestination()); + } else { + this.videoDestinationFC.setSelectedFile(this.job.getVideoDestination()); + } + + this.videoDestinationField.setText(this.job.getVideoDestination().getAbsolutePath()); + } + if (this.dialogType == CREATE_NEW_TEMPLATE || this.dialogType == EDIT_TEMPLATE) { + this.videoDestinationFC.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + } + } + + private void setupPluginPreferences() { + for (EncoderPlugin plugin : this.appLayer.getEncoderPlugins()) { + String pluginName = plugin.getName(); + //only display settings if the plugin actually has any... + Properties jobSpecificDefaultPluginPreferences = plugin.getJobSpecificPreferences(); + Properties jobPluginPreferences = null; + if (this.job != null) { + jobPluginPreferences = this.job.getEncoderPluginSettings(plugin); + } + if (jobSpecificDefaultPluginPreferences.size() > 0 && plugin.isEnabled()) { + //add heading + JXTitledSeparator pluginHeading = new JXTitledSeparator(pluginName + " plugin settings"); + inputPanel.add(pluginHeading, "span 3,grow"); + + for (String pluginPreferenceKey : plugin.getJobSpecificPreferencesOrder()) { + String value = jobSpecificDefaultPluginPreferences.getProperty(pluginPreferenceKey); + if (this.job != null) { + if (jobPluginPreferences.containsKey(pluginPreferenceKey)) { + value = jobPluginPreferences.getProperty(pluginPreferenceKey); + } + } + + this.setupSinglePluginSetting(plugin, pluginPreferenceKey, value); + } + } + } + } + + private void setupSinglePluginSetting(EncoderPlugin plugin, String key, String value) { + inputPanel.add(new JLabel(key + ":")); + + if (SwingGUIUtils.isBooleanValue(value)) { + JCheckBox checkbox = new JCheckBox(); + checkbox.setSelected(Boolean.valueOf(value)); + inputPanel.add(checkbox, "wrap"); + this.pluginDialogSettings.put(NDRPreferences.getConcatenatedKey(plugin.getName(), key), checkbox); + } else if (SwingGUIUtils.isFileChooser(value)) { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + JButton fcButton = new JButton("..."); + final JTextField filePathField = new JTextField(); + filePathField.setEditable(false); + inputPanel.add(filePathField); + fcButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int returnValue = fc.showOpenDialog(JobDialog.this); + if (returnValue == JFileChooser.APPROVE_OPTION) { + File selectedFile = fc.getSelectedFile(); + filePathField.setText(selectedFile.getAbsolutePath()); + } + } + }); + + try { + File selectedFile = new File(value); + if (selectedFile.exists()) { + fc.setSelectedFile(selectedFile); + filePathField.setText(selectedFile.getAbsolutePath()); + } + } catch (Throwable e) {} + this.pluginDialogSettings.put(NDRPreferences.getConcatenatedKey(plugin.getName(), key), fc); + inputPanel.add(fcButton); + } else { + //textfield + JTextField textField = new JTextField(); + textField.setText(value); + this.pluginDialogSettings.put(NDRPreferences.getConcatenatedKey(plugin.getName(), key), textField); + inputPanel.add(textField, "wrap"); + } + } + + private void setupButtonPart() { + String createButtonText; + if (this.dialogType == CREATE_NEW_JOB || this.dialogType == CREATE_NEW_TEMPLATE || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + createButtonText = "Create"; + } else { + createButtonText = "Save"; + } + buttonPanel = new JPanel(new MigLayout("insets 0")); + createButton = new JButton(createButtonText); + createButton.addActionListener(this); + cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(this); + + buttonPanel.add(createButton); + buttonPanel.add(cancelButton); + + getContentPane().add(buttonPanel); + } + + + public void actionPerformed(ActionEvent e) { + if (e.getSource() == enginePathChooserButton) { + int returnValue = this.enginePathFC.showOpenDialog(this); + if (returnValue == JFileChooser.APPROVE_OPTION) { + File selectedFile = this.enginePathFC.getSelectedFile(); + this.enginePathField.setText(selectedFile.getAbsolutePath()); + } + } else if (e.getSource() == dpVideoDirChooserButton) { + int returnValue = this.dpVideoDirFC.showOpenDialog(this); + if (returnValue == JFileChooser.APPROVE_OPTION) { + File selectedFile = this.dpVideoDirFC.getSelectedFile(); + this.dpVideoDirField.setText(selectedFile.getAbsolutePath()); + } + } else if (e.getSource() == demoFileChooserButton) { + int returnValue = this.demoFileFC.showOpenDialog(this); + if (returnValue == JFileChooser.APPROVE_OPTION) { + File selectedFile = this.demoFileFC.getSelectedFile(); + if (this.dialogType == CREATE_NEW_JOB || this.dialogType == EDIT_JOB || this.dialogType == CREATE_JOB_FROM_TEMPLATE) { + this.demoFileField.setText(DemoRecorderUtils.getJustFileNameOfPath(selectedFile)); + } else { + //template, show full path of directory + this.demoFileField.setText(selectedFile.getAbsolutePath()); + } + + } + } else if (e.getSource() == videoDestinationChooserButton) { + int returnValue = this.videoDestinationFC.showSaveDialog(this); + if (returnValue == JFileChooser.APPROVE_OPTION) { + File selectedFile = this.videoDestinationFC.getSelectedFile(); + this.videoDestinationField.setText(selectedFile.getAbsolutePath()); + } + } else if (e.getSource() == createButton) { + switch (this.dialogType) { + case CREATE_NEW_JOB: + case CREATE_JOB_FROM_TEMPLATE: + this.requestNewRecordJob(); break; + case CREATE_NEW_TEMPLATE: + this.createNewTemplate(); + break; + case EDIT_JOB: + this.editJob(); + break; + case EDIT_TEMPLATE: + this.editTemplate(); + break; + } + } else if (e.getSource() == cancelButton) { + dispose(); + } + } + + private void requestNewRecordJob() { + float startSecond, endSecond = -1; + try { + startSecond = Float.valueOf(this.startSecondField.getText()); + endSecond = Float.valueOf(this.endSecondField.getText()); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Make sure that start and end second are floating point numbers", e, true); + return; + } + + try { + RecordJob j = this.appLayer.createRecordJob( + this.jobNameField.getText(), + this.enginePathFC.getSelectedFile(), + this.engineParameterField.getText(), + this.demoFileFC.getSelectedFile(), + this.relativeDemoPathField.getText(), + this.dpVideoDirFC.getSelectedFile(), + this.videoDestinationFC.getSelectedFile(), + this.execBeforeField.getText(), + this.execAfterField.getText(), + startSecond, + endSecond + ); + this.saveEncoderPluginSettings(j); + dispose(); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog(e); + return; + } + + } + + private void editJob() { + float startSecond, endSecond = -1; + try { + startSecond = Float.valueOf(this.startSecondField.getText()); + endSecond = Float.valueOf(this.endSecondField.getText()); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Make sure that start and end second are floating point numbers", e, true); + return; + } + + try { + this.job.setJobName(this.jobNameField.getText()); + this.job.setEnginePath(this.enginePathFC.getSelectedFile()); + this.job.setEngineParameters(this.engineParameterField.getText()); + this.job.setDemoFile(this.demoFileFC.getSelectedFile()); + this.job.setRelativeDemoPath(this.relativeDemoPathField.getText()); + this.job.setDpVideoPath(this.dpVideoDirFC.getSelectedFile()); + this.job.setVideoDestination(this.videoDestinationFC.getSelectedFile()); + this.job.setExecuteBeforeCap(this.execBeforeField.getText()); + this.job.setExecuteAfterCap(this.execAfterField.getText()); + this.job.setStartSecond(startSecond); + this.job.setEndSecond(endSecond); + this.saveEncoderPluginSettings(this.job); + this.appLayer.fireUserInterfaceUpdate(this.job); + dispose(); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog(e); + return; + } + + } + + private void createNewTemplate() { + try { + RecordJobTemplate templ = new RecordJobTemplate( + this.templateNameField.getText(), + this.templateSummaryField.getText(), + this.jobNameField.getText(), + this.enginePathFC.getSelectedFile(), + this.engineParameterField.getText(), + this.demoFileFC.getSelectedFile(), + this.relativeDemoPathField.getText(), + this.dpVideoDirFC.getSelectedFile(), + this.videoDestinationFC.getSelectedFile(), + this.execBeforeField.getText(), + this.execAfterField.getText() + ); + this.saveEncoderPluginSettings(templ); + this.tableModel.addRecordJobTemplate(templ); + dispose(); + } catch (NullPointerException e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Make sure that you chose a file/directory in each case!", e, true); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog(e); + return; + } + } + + private void editTemplate() { + try { + RecordJobTemplate template = (RecordJobTemplate) this.job; + template.setName(this.templateNameField.getText()); + template.setSummary(this.templateSummaryField.getText()); + template.setJobName(this.jobNameField.getText()); + template.setEnginePath(this.enginePathFC.getSelectedFile()); + template.setEngineParameters(this.engineParameterField.getText()); + template.setDpVideoPath(this.dpVideoDirFC.getSelectedFile()); + template.setRelativeDemoPath(this.relativeDemoPathField.getText()); + template.setDemoFile(this.demoFileFC.getSelectedFile()); + template.setExecuteBeforeCap(this.execBeforeField.getText()); + template.setExecuteAfterCap(this.execAfterField.getText()); + template.setVideoDestination(this.videoDestinationFC.getSelectedFile()); + this.saveEncoderPluginSettings(template); + dispose(); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog(e); + return; + } + } + + private void saveEncoderPluginSettings(RecordJob job) { + Set keys = this.pluginDialogSettings.keySet(); + //remember, the keys are concatenated, containing both the category and actual key + for (String key : keys) { + JComponent component = this.pluginDialogSettings.get(key); + if (component instanceof JCheckBox) { + JCheckBox checkbox = (JCheckBox) component; + job.setEncoderPluginSetting(NDRPreferences.getCategory(key), NDRPreferences.getKey(key), String.valueOf(checkbox.isSelected())); + } else if (component instanceof JFileChooser) { + JFileChooser fileChooser = (JFileChooser) component; + if (fileChooser.getSelectedFile() != null) { + String path = fileChooser.getSelectedFile().getAbsolutePath(); + job.setEncoderPluginSetting(NDRPreferences.getCategory(key), NDRPreferences.getKey(key), path); + } + } else if (component instanceof JTextField) { + JTextField textField = (JTextField) component; + job.setEncoderPluginSetting(NDRPreferences.getCategory(key), NDRPreferences.getKey(key), textField.getText()); + } + } + } + + private static class FileChooserButton extends JButton { + private static final long serialVersionUID = 1335571540372856959L; + public FileChooserButton() { + super("..."); + } + } + + private JFileChooser createConfiguredFileChooser() { + JFileChooser fc = new JFileChooser(); + fc.setFileHidingEnabled(false); + fc.setFileFilter(userDirFilter); + return fc; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/NexuizUserDirFilter.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/NexuizUserDirFilter.java new file mode 100644 index 000000000..e4864b288 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/NexuizUserDirFilter.java @@ -0,0 +1,31 @@ +package com.nexuiz.demorecorder.ui.swinggui; + +import java.io.File; + +import javax.swing.filechooser.FileFilter; + +import com.nexuiz.demorecorder.application.DemoRecorderUtils; + +/** + * File filter that makes sure that the hidden .nexuiz directory is being shown in the + * file dialog, but other hidden directories are not. + */ +public class NexuizUserDirFilter extends FileFilter { + + @Override + public boolean accept(File f) { + if (f.isHidden()) { + if (f.isDirectory() && DemoRecorderUtils.getJustFileNameOfPath(f).equals(".nexuiz")) { + return true; + } + return false; //don't show other hidden directories/files + } + return true; + } + + @Override + public String getDescription() { + return null; + } + +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/PreferencesDialog.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/PreferencesDialog.java new file mode 100644 index 000000000..7af4a2b4d --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/PreferencesDialog.java @@ -0,0 +1,195 @@ +package com.nexuiz.demorecorder.ui.swinggui; + +import java.awt.Frame; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; + +import org.jdesktop.swingx.JXTitledSeparator; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; +import com.nexuiz.demorecorder.application.NDRPreferences; +import com.nexuiz.demorecorder.application.plugins.EncoderPlugin; +import com.nexuiz.demorecorder.ui.swinggui.utils.SwingGUIUtils; + +public class PreferencesDialog extends JDialog implements ActionListener { + + private static final long serialVersionUID = 7328399646538571333L; + private Frame parentFrame; + private DemoRecorderApplication appLayer; + private NDRPreferences preferences; + private Map dialogSettings; + + private JButton saveButton = new JButton("Save"); + private JButton cancelButton = new JButton("Cancel"); + + public PreferencesDialog(Frame owner, DemoRecorderApplication appLayer) { + super(owner, true); + this.parentFrame = owner; + this.appLayer = appLayer; + this.preferences = appLayer.getPreferences(); + this.dialogSettings = new HashMap(); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + + setTitle("Preferences"); + + this.setupLayout(); + } + + private void setupLayout() { + setLayout(new MigLayout("wrap 2", "[][::150,fill]")); + + //add heading + JXTitledSeparator applicationHeading = new JXTitledSeparator("Application settings"); + getContentPane().add(applicationHeading, "span 2,grow"); + + for (int i = 0; i < DemoRecorderApplication.Preferences.PREFERENCES_ORDER.length; i++) { + String currentSetting = DemoRecorderApplication.Preferences.PREFERENCES_ORDER[i]; + if (this.preferences.getProperty(NDRPreferences.MAIN_APPLICATION, currentSetting) != null) { + this.setupSingleSetting(NDRPreferences.MAIN_APPLICATION, currentSetting); + } + } + + //add plugin settings + for (EncoderPlugin plugin : this.appLayer.getEncoderPlugins()) { + String pluginName = plugin.getName(); + //only display settings if the plugin actually has any... + Properties pluginPreferences = plugin.getGlobalPreferences(); + if (pluginPreferences.size() > 0) { + //add heading + JXTitledSeparator pluginHeading = new JXTitledSeparator(pluginName + " plugin settings"); + getContentPane().add(pluginHeading, "span 2,grow"); + + for (String pluginKey : plugin.getGlobalPreferencesOrder()) { + if (this.preferences.getProperty(pluginName, pluginKey) != null) { + this.setupSingleSetting(pluginName, pluginKey); + } + } + } + } + + JPanel buttonPanel = new JPanel(); + buttonPanel.add(saveButton); + buttonPanel.add(cancelButton); + saveButton.addActionListener(this); + cancelButton.addActionListener(this); + getContentPane().add(buttonPanel, "span 2"); + } + + private void setupSingleSetting(String category, String setting) { + getContentPane().add(new JLabel(setting + ":")); + + String value = this.preferences.getProperty(category, setting); + if (SwingGUIUtils.isBooleanValue(value)) { + JCheckBox checkbox = new JCheckBox(); + this.dialogSettings.put(NDRPreferences.getConcatenatedKey(category, setting), checkbox); + getContentPane().add(checkbox); + } else if (SwingGUIUtils.isFileChooser(value)) { + final JFileChooser fc = new JFileChooser(); + fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + JButton fcButton = new JButton("..."); + fcButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + fc.showOpenDialog(PreferencesDialog.this); + } + }); + this.dialogSettings.put(NDRPreferences.getConcatenatedKey(category, setting), fc); + getContentPane().add(fcButton); + } else { + JTextField textField = new JTextField(); + this.dialogSettings.put(NDRPreferences.getConcatenatedKey(category, setting), textField); + getContentPane().add(textField); + } + } + + + + public void showDialog() { + this.loadSettings(); + this.pack(); + this.setLocationRelativeTo(this.parentFrame); + setResizable(false); + this.setVisible(true); + } + + /** + * Loads the settings from the application layer (and global plug-in settings) to the form. + */ + private void loadSettings() { + Set keys = this.preferences.keySet(); + for (Object keyObj : keys) { + String concatenatedKey = (String) keyObj; + String value; + JComponent component = null; + if ((value = this.preferences.getProperty(concatenatedKey)) != null) { + if (SwingGUIUtils.isBooleanValue(value)) { + component = this.dialogSettings.get(concatenatedKey); + if (component != null) { + ((JCheckBox) component).setSelected(Boolean.valueOf(value)); + } + } else if (SwingGUIUtils.isFileChooser(value)) { + component = this.dialogSettings.get(concatenatedKey); + try { + File selectedFile = new File(value); + if (selectedFile.exists() && component != null) { + ((JFileChooser) component).setSelectedFile(selectedFile); + } + } catch (Throwable e) {} + + } else { + component = this.dialogSettings.get(concatenatedKey); + if (component != null) { + ((JTextField) component).setText(value); + } + } + } + } + } + + @Override + public void actionPerformed(ActionEvent e) { + if (e.getSource() == cancelButton) { + this.setVisible(false); + } else if (e.getSource() == saveButton) { + this.saveSettings(); + } + } + + private void saveSettings() { + Set keys = this.dialogSettings.keySet(); + //remember, the keys are concatenated, containing both the category and actual key + for (String key : keys) { + JComponent component = this.dialogSettings.get(key); + if (component instanceof JCheckBox) { + JCheckBox checkbox = (JCheckBox) component; + this.appLayer.setPreference(NDRPreferences.getCategory(key), NDRPreferences.getKey(key), checkbox.isSelected()); + } else if (component instanceof JFileChooser) { + JFileChooser fileChooser = (JFileChooser) component; + if (fileChooser.getSelectedFile() != null) { + String path = fileChooser.getSelectedFile().getAbsolutePath(); + this.appLayer.setPreference(NDRPreferences.getCategory(key), NDRPreferences.getKey(key), path); + } + } else if (component instanceof JTextField) { + JTextField textField = (JTextField) component; + this.appLayer.setPreference(NDRPreferences.getCategory(key), NDRPreferences.getKey(key), textField.getText()); + } + } + this.setVisible(false); + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/RecordJobTemplate.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/RecordJobTemplate.java new file mode 100644 index 000000000..89ab61cee --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/RecordJobTemplate.java @@ -0,0 +1,113 @@ +package com.nexuiz.demorecorder.ui.swinggui; + +import java.io.File; + +import com.nexuiz.demorecorder.application.DemoRecorderException; +import com.nexuiz.demorecorder.application.jobs.RecordJob; + +public class RecordJobTemplate extends RecordJob { + + private static final long serialVersionUID = 8311386509410161395L; + private String templateName; + private String summary; + + public RecordJobTemplate( + String templateName, + String summary, + String jobName, + File enginePath, + String engineParameters, + File demoFile, + String relativeDemoPath, + File dpVideoPath, + File videoDestination, + String executeBeforeCap, + String executeAfterCap + ) { + super(); + + /* + * Differences to jobs: + * - name and summary exist + * - "Demo file:" -> "Demo directory:" + * - no start/end second + */ + + if (templateName == null || summary == null || jobName == null || enginePath == null || engineParameters == null || + demoFile == null || relativeDemoPath == null || dpVideoPath == null || videoDestination == null + || executeBeforeCap == null || executeAfterCap == null) { + throw new DemoRecorderException("Error: Make sure that you filled the necessary fields! (file choosers!)"); + } + + this.templateName = templateName; + this.summary = summary; + this.jobName = jobName; + this.enginePath = enginePath; + this.engineParameters = engineParameters; + this.demoFile = demoFile; + this.relativeDemoPath = relativeDemoPath; + this.dpVideoPath = dpVideoPath; + this.videoDestination = videoDestination; + this.executeBeforeCap = executeBeforeCap; + this.executeAfterCap = executeAfterCap; + } + + public String getName() { + return templateName; + } + + public String getSummary() { + return summary; + } + + public void setName(String name) { + this.templateName = name; + } + + public void setSummary(String summary) { + this.summary = summary; + } + + /* + * (non-Javadoc) + * Overwrite this method because here we want to do the read/write test for the path directly + * (as this one already is the directory), and not its parent directory. + * @see com.nexuiz.demorecorder.application.jobs.RecordJob#setDemoFile(java.io.File) + */ + public void setDemoFile(File demoFile) { + if (demoFile == null || !demoFile.exists()) { + throw new DemoRecorderException("Could not locate demo file!"); + } + if (!doReadWriteTest(demoFile)) { + throw new DemoRecorderException("The directory you specified for the demo to be recorded is not writable!"); + } + this.demoFile = demoFile.getAbsoluteFile(); + } + + /* + * (non-Javadoc) + * Overwrite this method because here we want to do the read/write test for the path directly + * (as this one already is the directory), and not its parent directory. + * @see com.nexuiz.demorecorder.application.jobs.RecordJob#setVideoDestination(java.io.File) + */ + public void setVideoDestination(File videoDestination) { + //keep in mind, here videoDestination points to the destination directory, not the destination file + if (videoDestination == null || !videoDestination.isDirectory()) { + throw new DemoRecorderException("Could not locate the specified video destination directory"); + } + + if (!this.doReadWriteTest(videoDestination)) { + throw new DemoRecorderException("The video destination directory is not writable! It needs to be writable so that the file can be moved to its new location"); + } + + this.videoDestination = videoDestination.getAbsoluteFile(); + } + + public String getJobName() { + return this.jobName; + } + + public void setJobName(String jobName) { + this.jobName = jobName; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/StatusBar.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/StatusBar.java new file mode 100644 index 000000000..ed33c9cea --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/StatusBar.java @@ -0,0 +1,86 @@ +package com.nexuiz.demorecorder.ui.swinggui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.SystemColor; + +import javax.swing.BorderFactory; +import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JPanel; + +public class StatusBar extends JPanel { + + private static final long serialVersionUID = -1471757496863555741L; + private JLabel currentActivity = null; + + private static final String STATE_IDLE = "Idle"; + private static final String STATE_WORKING = "Working"; + + public StatusBar() { + BorderLayout borderLayout = new BorderLayout(0, 0); + setLayout(borderLayout); + JPanel rightPanel = new JPanel(new BorderLayout()); + rightPanel.add(new JLabel(new AngledLinesWindowsCornerIcon()), BorderLayout.SOUTH); + rightPanel.setOpaque(false); + + add(rightPanel, BorderLayout.EAST); + + this.currentActivity = new JLabel("Idle"); + add(this.currentActivity, BorderLayout.WEST); + setBackground(SystemColor.control); + setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.black)); + } + + /** + * Sets the state/display of the status bar to "idle" (false) or "working" (true). + * @param state + */ + public void showState(boolean state) { + if (state) { + currentActivity.setText(STATE_WORKING); + } else { + currentActivity.setText(STATE_IDLE); + } + } + + private static class AngledLinesWindowsCornerIcon implements Icon { + private static final Color WHITE_LINE_COLOR = new Color(255, 255, 255); + + private static final Color GRAY_LINE_COLOR = new Color(172, 168, 153); + private static final int WIDTH = 13; + + private static final int HEIGHT = 13; + + public int getIconHeight() { + return HEIGHT; + } + + public int getIconWidth() { + return WIDTH; + } + + public void paintIcon(Component c, Graphics g, int x, int y) { + + g.setColor(WHITE_LINE_COLOR); + g.drawLine(0, 12, 12, 0); + g.drawLine(5, 12, 12, 5); + g.drawLine(10, 12, 12, 10); + + g.setColor(GRAY_LINE_COLOR); + g.drawLine(1, 12, 12, 1); + g.drawLine(2, 12, 12, 2); + g.drawLine(3, 12, 12, 3); + + g.drawLine(6, 12, 12, 6); + g.drawLine(7, 12, 12, 7); + g.drawLine(8, 12, 12, 8); + + g.drawLine(11, 12, 12, 11); + g.drawLine(12, 12, 12, 12); + + } + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/SwingGUI.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/SwingGUI.java new file mode 100644 index 000000000..3b5f5de5a --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/SwingGUI.java @@ -0,0 +1,1109 @@ +package com.nexuiz.demorecorder.ui.swinggui; + +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import javax.help.HelpBroker; +import javax.help.HelpSet; +import javax.swing.BorderFactory; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +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.UIManager; +import javax.swing.border.TitledBorder; + +import net.miginfocom.swing.MigLayout; + +import org.jdesktop.swingx.JXTable; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; +import com.nexuiz.demorecorder.application.DemoRecorderUtils; +import com.nexuiz.demorecorder.application.NDRPreferences; +import com.nexuiz.demorecorder.application.DemoRecorderApplication.Preferences; +import com.nexuiz.demorecorder.application.jobs.RecordJob; +import com.nexuiz.demorecorder.application.jobs.RecordJob.State; +import com.nexuiz.demorecorder.application.plugins.EncoderPlugin; +import com.nexuiz.demorecorder.ui.DemoRecorderUI; +import com.nexuiz.demorecorder.ui.swinggui.tablemodels.RecordJobTemplatesTableModel; +import com.nexuiz.demorecorder.ui.swinggui.tablemodels.RecordJobsTableModel; +import com.nexuiz.demorecorder.ui.swinggui.utils.ShowErrorDialogExceptionHandler; +import com.nexuiz.demorecorder.ui.swinggui.utils.XProperties; +import com.nexuiz.demorecorder.ui.swinggui.utils.XProperties.XTableState; + +public class SwingGUI extends JFrame implements WindowListener, DemoRecorderUI { + + private static final long serialVersionUID = -7287303462488231068L; + public static final String JOB_TABLE_PREFERENCES_FILENAME = "jobsTable.pref"; + public static final String TEMPLATE_TABLE_PREFERENCES_FILENAME = "templatesTable.pref"; + public static final String TEMPLATE_TABLE_CONTENT_FILENAME = "templates.dat"; + + private DemoRecorderApplication appLayer; + private PreferencesDialog preferencesDialog; + + private JXTable jobsTable = null; + private JPopupMenu jobsTablePopupMenu; + private ActionListener jobButtonActionListener = new JobButtonActionListener(); + private MouseListener jobsTableMouseListener = new JobsTableMouseListener(); + + private JXTable templatesTable = null; + private JPopupMenu templatesTablePopupMenu; + private ActionListener templateButtonActionListener = new TemplateButtonActionListener(); + private MouseListener templatesTableMouseListener = new TemplatesTableMouseListener(); + + private ActionListener recordButtonActionListener = new RecordButtonActionListener(); + + private static final String LABEL_JOB_CREATE = "Create"; + private static final String LABEL_JOB_CREATE_FROM_TEMPL = "Create from template"; + private static final String LABEL_JOB_DELETE = "Delete"; + private static final String LABEL_JOB_CLEAR = "Clear"; + private static final String LABEL_JOB_EDIT = "Edit job"; + private static final String LABEL_JOB_DUPLICATE = "Duplicate job"; + private static final String LABEL_JOB_START = "Start job"; + private static final String LABEL_JOB_SHOWERROR = "Show error message"; + private static final String LABEL_JOB_RESET_STATE_WAITING = "Reset job status to 'waiting'"; + private static final String LABEL_JOB_RESET_STATE_DONE = "Reset job status to 'done'"; + + private static final String LABEL_TEMPL_CREATE = "Create"; + private static final String LABEL_TEMPL_CREATE_FROM_JOB = "Create from job"; + private static final String LABEL_TEMPL_DELETE = "Delete"; + private static final String LABEL_TEMPL_CLEAR = "Clear"; + private static final String LABEL_TEMPL_EDIT = "Edit template"; + private static final String LABEL_TEMPL_DUPLICATE = "Duplicate template"; + + private ActionListener menuButtonActionListener = new MenuButtonActionListener(); + private JMenuItem fileLoadQueue = new JMenuItem("Load job queue", getIcon("fileopen.png")); + private JMenuItem fileSaveQueue = new JMenuItem("Save job queue", getIcon("filesave.png")); + private JMenuItem filePreferences = new JMenuItem("Preferences", getIcon("advanced.png")); + private JMenuItem fileExit = new JMenuItem("Exit", getIcon("exit.png")); + private JMenuItem helpHelp = new JMenuItem("Show help", getIcon("help.png")); + private JMenuItem helpAbout = new JMenuItem("About", getIcon("info.png")); + private JFileChooser jobQueueSaveAsFC = new JFileChooser(); + + private JButton jobs_create = new JButton(LABEL_JOB_CREATE, getIcon("edit_add.png")); + private JButton jobs_createFromTempl = new JButton(LABEL_JOB_CREATE_FROM_TEMPL, getIcon("view_right_p.png")); + private JButton jobs_delete = new JButton(LABEL_JOB_DELETE, getIcon("editdelete.png")); + private JButton jobs_clear = new JButton(LABEL_JOB_CLEAR, getIcon("editclear.png")); + private JMenuItem jobs_contextmenu_edit = new JMenuItem(LABEL_JOB_EDIT, getIcon("edit.png")); + private JMenuItem jobs_contextmenu_duplicate = new JMenuItem(LABEL_JOB_DUPLICATE, getIcon("editcopy.png")); + private JMenuItem jobs_contextmenu_delete = new JMenuItem(LABEL_JOB_DELETE, getIcon("editdelete.png")); + private JMenuItem jobs_contextmenu_start = new JMenuItem(LABEL_JOB_START, getIcon("player_play.png")); + private JMenuItem jobs_contextmenu_showerror = new JMenuItem(LABEL_JOB_SHOWERROR, getIcon("status_unknown.png")); + private JMenuItem jobs_contextmenu_resetstate_waiting = new JMenuItem(LABEL_JOB_RESET_STATE_WAITING, getIcon("quick_restart.png")); + private JMenuItem jobs_contextmenu_resetstate_done = new JMenuItem(LABEL_JOB_RESET_STATE_DONE, getIcon("quick_restart_blue.png")); + private List jobs_contextmenu_runPluginMenuItems = new ArrayList(); + + private JButton templ_create = new JButton(LABEL_TEMPL_CREATE, getIcon("edit_add.png")); + private JButton templ_createFromJob = new JButton(LABEL_TEMPL_CREATE_FROM_JOB, getIcon("view_right_p.png")); + private JButton templ_delete = new JButton(LABEL_TEMPL_DELETE, getIcon("editdelete.png")); + private JButton templ_clear = new JButton(LABEL_TEMPL_CLEAR, getIcon("editclear.png")); + private JMenuItem templ_contextmenu_edit = new JMenuItem(LABEL_TEMPL_EDIT, getIcon("edit.png")); + private JMenuItem templ_contextmenu_duplicate = new JMenuItem(LABEL_TEMPL_DUPLICATE, getIcon("editcopy.png")); + private JMenuItem templ_contextmenu_delete = new JMenuItem(LABEL_TEMPL_DELETE, getIcon("editdelete.png")); + + private static final String PROCESSING_START = "Start processing"; + private static final String PROCESSING_STOP_NOW = "Stop processing"; + private static final String PROCESSING_STOP_LATER = "Processing will stop after current job finished"; + private JButton processing_start = new JButton(PROCESSING_START, getIcon("player_play.png")); + private JButton processing_stop = new JButton(PROCESSING_STOP_NOW, getIcon("player_pause.png")); + + private StatusBar statusBar = new StatusBar(); + + private static HelpBroker mainHelpBroker = null; + private static final String mainHelpSetName = "help/DemoRecorderHelp.hs"; + + public SwingGUI(DemoRecorderApplication appLayer) { + super("Nexuiz Demo Recorder v0.2"); + addWindowListener(this); + + this.appLayer = appLayer; + + this.setupLayout(); + this.setupHelp(); + this.preferencesDialog = new PreferencesDialog(this, appLayer); + + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + // Display the window. + pack(); + setVisible(true); + //now that we have the GUI we can set the parent window for the error dialog + ShowErrorDialogExceptionHandler.setParentWindow(this); + } + + private void setupHelp() { + if (mainHelpBroker == null){ + HelpSet mainHelpSet = null; + + try { + URL hsURL = HelpSet.findHelpSet(null, mainHelpSetName); + mainHelpSet = new HelpSet(null, hsURL); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Could not properly create the help", e, true); + } + + if (mainHelpSet != null) + mainHelpBroker = mainHelpSet.createHelpBroker(); + } + } + + private void setupLayout() { + setLayout(new MigLayout("wrap 1,insets 10", "[400:700:,grow,fill]", + "[grow,fill][grow,fill][][]")); + Container contentPane = getContentPane(); + setJMenuBar(this.buildMenu()); + + this.setupTemplatePanel(); + this.setupJobPanel(); + this.setupRecordPanel(); + + contentPane.add(statusBar, "south,height 23::"); + } + + private void setupTemplatePanel() { + JPanel templatePanel = new JPanel(new MigLayout("", "[500:500:,grow,fill][170!,fill,grow]", "[grow,fill]")); + TitledBorder templatePanelTitle = BorderFactory.createTitledBorder("Templates"); + templatePanel.setBorder(templatePanelTitle); + getContentPane().add(templatePanel); + + this.setupTemplatesTable(); + this.loadTableStates(this.templatesTable); + JScrollPane templateScrollPane = new JScrollPane(templatesTable); + templatePanel.add(templateScrollPane); + + this.templ_create.addActionListener(this.templateButtonActionListener); + this.templ_createFromJob.addActionListener(this.templateButtonActionListener); + this.templ_delete.addActionListener(this.templateButtonActionListener); + this.templ_clear.addActionListener(this.templateButtonActionListener); + + this.templ_contextmenu_edit.addActionListener(this.templateButtonActionListener); + this.templ_contextmenu_duplicate.addActionListener(this.templateButtonActionListener); + this.templ_contextmenu_delete.addActionListener(this.templateButtonActionListener); + + this.configureTableButtons(); + + JPanel templateControlButtonPanel = new JPanel(new MigLayout("wrap 1", "fill,grow")); + templateControlButtonPanel.add(this.templ_create); + templateControlButtonPanel.add(this.templ_createFromJob); + templateControlButtonPanel.add(this.templ_delete); + templateControlButtonPanel.add(this.templ_clear); + templatePanel.add(templateControlButtonPanel); + } + + private void setupJobPanel() { + JPanel jobPanel = new JPanel(new MigLayout("", "[500:500:,grow,fill][170!,fill,grow]", "[grow,fill]")); + TitledBorder jobPanelTitle = BorderFactory.createTitledBorder("Jobs"); + jobPanel.setBorder(jobPanelTitle); + getContentPane().add(jobPanel); + + this.setupJobsTable(); + this.loadTableStates(this.jobsTable); + + JScrollPane jobScrollPane = new JScrollPane(jobsTable); + jobPanel.add(jobScrollPane); + + this.jobs_create.addActionListener(this.jobButtonActionListener); + this.jobs_createFromTempl.addActionListener(this.jobButtonActionListener); + this.jobs_delete.addActionListener(this.jobButtonActionListener); + this.jobs_clear.addActionListener(this.jobButtonActionListener); + + this.jobs_contextmenu_edit.addActionListener(this.jobButtonActionListener); + this.jobs_contextmenu_duplicate.addActionListener(this.jobButtonActionListener); + this.jobs_contextmenu_delete.addActionListener(this.jobButtonActionListener); + this.jobs_contextmenu_start.addActionListener(this.jobButtonActionListener); + this.jobs_contextmenu_showerror.addActionListener(this.jobButtonActionListener); + this.jobs_contextmenu_resetstate_waiting.addActionListener(this.jobButtonActionListener); + this.jobs_contextmenu_resetstate_done.addActionListener(this.jobButtonActionListener); + + //initialize button states + configureTableButtons(); + + JPanel jobControlButtonPanel = new JPanel(new MigLayout("wrap 1", "fill,grow")); + jobControlButtonPanel.add(this.jobs_create); + jobControlButtonPanel.add(this.jobs_createFromTempl); + jobControlButtonPanel.add(this.jobs_delete); + jobControlButtonPanel.add(this.jobs_clear); + jobPanel.add(jobControlButtonPanel); + } + + private void setupJobsTable() { + RecordJobsTableModel tableModel = new RecordJobsTableModel(this.appLayer); + jobsTable = new JXTable(tableModel); + jobsTable.setColumnControlVisible(true); + jobsTable.setPreferredScrollableViewportSize(new Dimension(400, 100)); + jobsTable.addMouseListener(this.jobsTableMouseListener); + } + + private void setupTemplatesTable() { + RecordJobTemplatesTableModel tableModel = new RecordJobTemplatesTableModel(); + templatesTable = new JXTable(tableModel); + templatesTable.setColumnControlVisible(true); + templatesTable.setPreferredScrollableViewportSize(new Dimension(400, 100)); + templatesTable.addMouseListener(this.templatesTableMouseListener); + } + + private void setupRecordPanel() { + JPanel recButtonPanel = new JPanel(new MigLayout()); + recButtonPanel.add(processing_start); + recButtonPanel.add(processing_stop); + processing_stop.setEnabled(false); + processing_start.addActionListener(recordButtonActionListener); + processing_stop.addActionListener(recordButtonActionListener); + getContentPane().add(recButtonPanel); + } + + public static void setSystemLAF() { + try { + // Set System L&F + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + } + } + + public void RecordJobPropertiesChange(RecordJob job) { + RecordJobsTableModel jobsTableModel = (RecordJobsTableModel) this.jobsTable.getModel(); + List recordJobs = jobsTableModel.getRecordJobs(); + int jobIndex = recordJobs.indexOf(job); + if (jobIndex == -1) { + //new job + recordJobs.add(job); + //add job at the end of the table: + int position = jobsTableModel.getRowCount() - 1; + jobsTableModel.fireTableRowsInserted(position, position); + } else { + //job already existed + jobIndex = this.jobsTable.convertRowIndexToView(jobIndex); //convert due to possible view sorting + jobsTableModel.fireTableRowsUpdated(jobIndex, jobIndex); + } + } + + public void recordingFinished() { + JOptionPane.showMessageDialog(SwingGUI.this, "Finished recording all jobs", "Recording done", JOptionPane.INFORMATION_MESSAGE); + statusBar.showState(false); + processing_start.setEnabled(true); + processing_stop.setEnabled(false); + processing_stop.setText(PROCESSING_STOP_NOW); + } + + private JMenuBar buildMenu() { + JMenuBar menuBar = new JMenuBar(); + + JMenu fileMenu = new JMenu("File"); + fileMenu.add(fileLoadQueue); + fileMenu.add(fileSaveQueue); + fileMenu.add(filePreferences); + fileMenu.add(fileExit); + menuBar.add(fileMenu); + + fileLoadQueue.addActionListener(menuButtonActionListener); + fileSaveQueue.addActionListener(menuButtonActionListener); + filePreferences.addActionListener(menuButtonActionListener); + fileExit.addActionListener(menuButtonActionListener); + + JMenu helpMenu = new JMenu("Help"); + helpMenu.add(helpHelp); + helpMenu.add(helpAbout); + menuBar.add(helpMenu); + + helpHelp.addActionListener(menuButtonActionListener); + helpAbout.addActionListener(menuButtonActionListener); + + this.setupEncoderPluginButtons(); + + this.jobsTablePopupMenu = new JPopupMenu(); + this.jobsTablePopupMenu.add(jobs_contextmenu_edit); + this.jobsTablePopupMenu.add(jobs_contextmenu_duplicate); + this.jobsTablePopupMenu.add(jobs_contextmenu_delete); + this.jobsTablePopupMenu.add(jobs_contextmenu_start); + //add JMenus for plugins + for (JMenuItem menuItem : jobs_contextmenu_runPluginMenuItems) { + this.jobsTablePopupMenu.add(menuItem); + } + this.jobsTablePopupMenu.add(jobs_contextmenu_showerror); + this.jobsTablePopupMenu.add(jobs_contextmenu_resetstate_waiting); + this.jobsTablePopupMenu.add(jobs_contextmenu_resetstate_done); + + + + + this.templatesTablePopupMenu = new JPopupMenu(); + this.templatesTablePopupMenu.add(templ_contextmenu_edit); + this.templatesTablePopupMenu.add(templ_contextmenu_duplicate); + this.templatesTablePopupMenu.add(templ_contextmenu_delete); + + return menuBar; + } + + private void setupEncoderPluginButtons() { + for (EncoderPlugin plugin : appLayer.getEncoderPlugins()) { + JMenuItem pluginMenuItem = new JMenuItem("Just run " + plugin.getName() + " plugin", getIcon("package.png")); + pluginMenuItem.addActionListener(jobButtonActionListener); + this.jobs_contextmenu_runPluginMenuItems.add(pluginMenuItem); + } + } + + private void saveTableStates(JXTable table) { + String fileName; + if (table == jobsTable) { + fileName = JOB_TABLE_PREFERENCES_FILENAME; + } else { + fileName = TEMPLATE_TABLE_PREFERENCES_FILENAME; + } + String exceptionMessage = "An error occurred while trying to save the table state file " + fileName; + + XProperties.XTableProperty t = new XProperties.XTableProperty(); + XTableState tableState; + try { + tableState = (XTableState) t.getSessionState(table); + } catch (Exception e) { //most likely ClassCastException + DemoRecorderUtils.showNonCriticalErrorDialog(exceptionMessage, e, true); + return; + } + + File tableStateFile = DemoRecorderUtils.computeLocalFile(DemoRecorderApplication.PREFERENCES_DIRNAME, fileName); + DemoRecorderUtils.attemptFileCreation(tableStateFile); + + try { + FileOutputStream fout = new FileOutputStream(tableStateFile); + ObjectOutputStream oos = new ObjectOutputStream(fout); + oos.writeObject(tableState); + oos.close(); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog(exceptionMessage, e, true); + } + } + + private void loadTableStates(JXTable table) { + String fileName; + if (table == jobsTable) { + fileName = JOB_TABLE_PREFERENCES_FILENAME; + } else { + fileName = TEMPLATE_TABLE_PREFERENCES_FILENAME; + } + + XProperties.XTableProperty t = new XProperties.XTableProperty(); + + File tableStateFile = DemoRecorderUtils.computeLocalFile(DemoRecorderApplication.PREFERENCES_DIRNAME, fileName); + + XTableState tableState; + + try { + FileInputStream fin = new FileInputStream(tableStateFile); + ObjectInputStream ois = new ObjectInputStream(fin); + tableState = (XTableState) ois.readObject(); + t.setSessionState(table, tableState); + } catch (Exception e) { + //manually hide columns + if (table == jobsTable) { + //re-create table to be sure + this.setupJobsTable(); + //manually hide some columns + jobsTable.getColumnExt(RecordJobsTableModel.EXECUTE_AFTER_CAP).setVisible(false); + jobsTable.getColumnExt(RecordJobsTableModel.EXECUTE_BEFORE_CAP).setVisible(false); + jobsTable.getColumnExt(RecordJobsTableModel.VIDEO_DESTINATION_PATH).setVisible(false); + jobsTable.getColumnExt(RecordJobsTableModel.DPVIDEO_PATH).setVisible(false); + jobsTable.getColumnExt(RecordJobsTableModel.RELATIVE_DEMO_PATH).setVisible(false); + jobsTable.getColumnExt(RecordJobsTableModel.ENGINE_PARAMETERS).setVisible(false); + jobsTable.getColumnExt(RecordJobsTableModel.ENGINE_PATH).setVisible(false); + } else { + //re-create table to be sure + this.setupTemplatesTable(); + //manually hide some columns + templatesTable.getColumnExt(RecordJobTemplatesTableModel.EXECUTE_AFTER_CAP).setVisible(false); + templatesTable.getColumnExt(RecordJobTemplatesTableModel.EXECUTE_BEFORE_CAP).setVisible(false); + templatesTable.getColumnExt(RecordJobTemplatesTableModel.VIDEO_DESTINATION_PATH).setVisible(false); + templatesTable.getColumnExt(RecordJobTemplatesTableModel.DPVIDEO_PATH).setVisible(false); + templatesTable.getColumnExt(RecordJobTemplatesTableModel.RELATIVE_DEMO_PATH).setVisible(false); + templatesTable.getColumnExt(RecordJobTemplatesTableModel.DEMO_FILE_PATH).setVisible(false); + templatesTable.getColumnExt(RecordJobTemplatesTableModel.ENGINE_PARAMETERS).setVisible(false); + templatesTable.getColumnExt(RecordJobTemplatesTableModel.ENGINE_PATH).setVisible(false); + templatesTable.getColumnExt(RecordJobTemplatesTableModel.JOB_NAME).setVisible(false); + } + } + } + + private class MenuButtonActionListener implements ActionListener { + + public void actionPerformed(ActionEvent e) { + if (e.getSource() == fileLoadQueue) { + int result = jobQueueSaveAsFC.showOpenDialog(SwingGUI.this); + if (result == JFileChooser.APPROVE_OPTION) { + File selectedFile = jobQueueSaveAsFC.getSelectedFile(); + if (selectedFile.isFile()) { + RecordJobsTableModel tableModel = (RecordJobsTableModel) jobsTable.getModel(); + tableModel.loadNewJobQueue(selectedFile); + configureTableButtons(); + } + } + + } else if (e.getSource() == fileSaveQueue) { + int result = jobQueueSaveAsFC.showSaveDialog(SwingGUI.this); + if (result == JFileChooser.APPROVE_OPTION) { + File selectedFile = jobQueueSaveAsFC.getSelectedFile(); + if (!DemoRecorderUtils.getFileExtension(selectedFile).equals("queue")) { + //if file is not a .queue file, make it one + selectedFile = new File(selectedFile.getAbsoluteFile() + ".queue"); + } + if (selectedFile.exists()) { + int confirm = JOptionPane.showConfirmDialog(SwingGUI.this, "File already exists. Are you sure you want to overwrite it?", "Confirm overwrite", JOptionPane.YES_NO_OPTION); + if (confirm == JOptionPane.NO_OPTION) { + return; + } + } + appLayer.saveJobQueue(selectedFile); + } + } else if (e.getSource() == filePreferences) { + preferencesDialog.showDialog(); + } else if (e.getSource() == fileExit) { + shutDown(); + } else if (e.getSource() == helpHelp) { + if (mainHelpBroker != null) { + mainHelpBroker.setDisplayed(true); + } + } else if (e.getSource() == helpAbout) { + showAboutBox(); + } + } + + } + + /** + * Listens to the clicks on buttons that are in the job panel (next to the jobs table) + * or its context menu. + */ + private class JobButtonActionListener implements ActionListener { + + public void actionPerformed(ActionEvent e) { + List selectedJobs = getSelectedRecordJobs(jobsTable); + List selectedTemplates = getSelectedRecordJobs(templatesTable); + if (e.getSource() == jobs_create) { + JobDialog jobDialog = new JobDialog(SwingGUI.this, appLayer); + jobDialog.showDialog(); + configureTableButtons(); + } + else if (e.getSource() == jobs_createFromTempl) { + if (selectedTemplates.size() != 1) { + return; + } + RecordJobTemplate template = (RecordJobTemplate) selectedTemplates.get(0); +// JobDialog jobDialog = new JobDialog(SwingGUI.this, template, appLayer); + JobDialog jobDialog = new JobDialog(SwingGUI.this, template, appLayer, JobDialog.CREATE_JOB_FROM_TEMPLATE); + jobDialog.showDialog(); + configureTableButtons(); + } + else if (e.getSource() == jobs_delete || e.getSource() == jobs_contextmenu_delete) { + int result = JOptionPane.showConfirmDialog(SwingGUI.this, "Are you sure you want to delete the selected job(s)?", "Confirm delete", JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.YES_OPTION) { + deleteSelectedJobs(false); + configureTableButtons(); + } + } + else if (e.getSource() == jobs_clear) { + int result = JOptionPane.showConfirmDialog(SwingGUI.this, "Are you sure you want to clear the job list?", "Confirm clear", JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.YES_OPTION) { + deleteSelectedJobs(true); + configureTableButtons(); + } + } else if (e.getSource() == jobs_contextmenu_edit) { + if (selectedJobs.size() == 1) { + RecordJob selectedJob = selectedJobs.get(0); + JobDialog jobDialog = new JobDialog(SwingGUI.this, selectedJob, appLayer); + jobDialog.showDialog(); + configureTableButtons(); + } + } else if (e.getSource() == jobs_contextmenu_showerror) { + if (selectedJobs.size() == 1) { + RecordJob selectedJob = selectedJobs.get(0); + DemoRecorderUtils.showNonCriticalErrorDialog(selectedJob.getLastException()); + } + } else if (e.getSource() == jobs_contextmenu_resetstate_waiting) { + for (RecordJob job : selectedJobs) { + job.setState(RecordJob.State.WAITING); + } + } else if (e.getSource() == jobs_contextmenu_resetstate_done) { + for (RecordJob job : selectedJobs) { + if (job.getState() == State.ERROR_PLUGIN) { + job.setState(RecordJob.State.DONE); + } + } + } else if (e.getSource() == jobs_contextmenu_start) { + appLayer.recordSelectedJobs(selectedJobs); + if (appLayer.getState() == DemoRecorderApplication.STATE_WORKING) { + processing_start.setEnabled(false); + processing_stop.setEnabled(true); + statusBar.showState(true); + } + } else if (e.getSource() == jobs_contextmenu_duplicate) { + if (selectedJobs.size() > 0) { + this.duplicateRecordJobs(selectedJobs); + //select all new duplicates in the table automatically + jobsTable.setRowSelectionInterval(jobsTable.getRowCount() - selectedJobs.size(), jobsTable.getRowCount() - 1); + configureTableButtons(); + } + } else if (jobs_contextmenu_runPluginMenuItems.contains(e.getSource())) { + int index = jobs_contextmenu_runPluginMenuItems.indexOf(e.getSource()); + EncoderPlugin selectedPlugin = appLayer.getEncoderPlugins().get(index); + + appLayer.executePluginForSelectedJobs(selectedPlugin, selectedJobs); + if (appLayer.getState() == DemoRecorderApplication.STATE_WORKING) { + processing_start.setEnabled(false); + processing_stop.setEnabled(true); + statusBar.showState(true); + } + } + } + + private void duplicateRecordJobs(List jobs) { + String nameSuffix = appLayer.getPreferences().getProperty(NDRPreferences.MAIN_APPLICATION, Preferences.JOB_NAME_APPEND_DUPLICATE); + for (RecordJob job : jobs) { + RecordJob newJob = appLayer.createRecordJob( + job.getJobName() + nameSuffix, + job.getEnginePath(), + job.getEngineParameters(), + job.getDemoFile(), + job.getRelativeDemoPath(), + job.getDpVideoPath(), + job.getVideoDestination(), + job.getExecuteBeforeCap(), + job.getExecuteAfterCap(), + job.getStartSecond(), + job.getEndSecond() + ); + newJob.setEncoderPluginSettings(job.getEncoderPluginSettings()); + } + } + + } + + private class TemplateButtonActionListener implements ActionListener { + public void actionPerformed(ActionEvent e) { + if (e.getSource() == templ_create) { + RecordJobTemplatesTableModel tableModel = (RecordJobTemplatesTableModel) templatesTable.getModel(); + JobDialog jobDialog = new JobDialog(SwingGUI.this, tableModel, templatesTable, appLayer); + jobDialog.showDialog(); + configureTableButtons(); + } + else if (e.getSource() == templ_createFromJob) { + this.createTemplateFromJob(); + configureTableButtons(); + } + else if (e.getSource() == templ_delete || e.getSource() == templ_contextmenu_delete) { + int result = JOptionPane.showConfirmDialog(SwingGUI.this, "Are you sure you want to delete the selected template(s)?", "Confirm delete", JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.YES_OPTION) { + deleteSelectedTemplates(false); + } + configureTableButtons(); + } + else if (e.getSource() == templ_clear) { + int result = JOptionPane.showConfirmDialog(SwingGUI.this, "Are you sure you want to clear the template list?", "Confirm clear", JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.YES_OPTION) { + deleteSelectedTemplates(true); + } + configureTableButtons(); + } + else if (e.getSource() == templ_contextmenu_edit) { + List selectedTemplates = getSelectedRecordJobs(templatesTable); + if (selectedTemplates.size() == 1) { + RecordJobTemplate selectedTemplate = (RecordJobTemplate) selectedTemplates.get(0); + JobDialog jobDialog = new JobDialog(SwingGUI.this, selectedTemplate, appLayer, JobDialog.EDIT_TEMPLATE); + jobDialog.showDialog(); + configureTableButtons(); + } + } + else if (e.getSource() == templ_contextmenu_duplicate) { + List selectedTemplates = getSelectedRecordJobs(templatesTable); + if (selectedTemplates.size() > 0) { + this.duplicateTemplates(selectedTemplates); + //select all new duplicates in the table automatically + templatesTable.setRowSelectionInterval(templatesTable.getRowCount() - selectedTemplates.size(), templatesTable.getRowCount() - 1); + configureTableButtons(); + } + } + } + + private void createTemplateFromJob() { + List selectedJobs = getSelectedRecordJobs(jobsTable); + if (selectedJobs.size() == 1) { + RecordJob job = selectedJobs.get(0); + RecordJobTemplate templ = new RecordJobTemplate( + "Generated from job", + "Generated from job", + job.getJobName(), + job.getEnginePath(), + job.getEngineParameters(), + job.getDemoFile().getParentFile(), + job.getRelativeDemoPath(), + job.getDpVideoPath(), + job.getVideoDestination().getParentFile(), + job.getExecuteBeforeCap(), + job.getExecuteAfterCap() + ); + templ.setEncoderPluginSettings(job.getEncoderPluginSettings()); + + RecordJobTemplatesTableModel tableModel = (RecordJobTemplatesTableModel) templatesTable.getModel(); + tableModel.addRecordJobTemplate(templ); + } + } + + private void duplicateTemplates(List selectedTemplates) { + for (RecordJob job : selectedTemplates) { + RecordJobTemplate template = (RecordJobTemplate) job; + RecordJobTemplate templ = new RecordJobTemplate( + template.getName(), + template.getSummary(), + template.getJobName(), + template.getEnginePath(), + template.getEngineParameters(), + template.getDemoFile(), + template.getRelativeDemoPath(), + template.getDpVideoPath(), + template.getVideoDestination(), + template.getExecuteBeforeCap(), + template.getExecuteAfterCap() + ); + templ.setEncoderPluginSettings(template.getEncoderPluginSettings()); + + RecordJobTemplatesTableModel tableModel = (RecordJobTemplatesTableModel) templatesTable.getModel(); + tableModel.addRecordJobTemplate(templ); + } + } + } + + private class RecordButtonActionListener implements ActionListener { + + public void actionPerformed(ActionEvent e) { + if (e.getSource() == processing_start) { + appLayer.startRecording(); + if (appLayer.getState() == DemoRecorderApplication.STATE_WORKING) { + processing_start.setEnabled(false); + processing_stop.setEnabled(true); + statusBar.showState(true); + } + } else if (e.getSource() == processing_stop) { + if (appLayer.getState() == DemoRecorderApplication.STATE_WORKING) { + appLayer.stopRecording(); + processing_stop.setEnabled(false); + processing_stop.setText(PROCESSING_STOP_LATER); + } + } + } + } + + private void deleteSelectedJobs(boolean deleteAllJobs) { + RecordJobsTableModel tableModel = (RecordJobsTableModel) jobsTable.getModel(); + if (deleteAllJobs) { + int rowCount = jobsTable.getRowCount(); + for (int i = rowCount - 1; i >= 0; i--) { + int modelRowIndex = jobsTable.convertRowIndexToModel(i); + tableModel.deleteRecordJob(modelRowIndex, i); + } + } else { + int[] selectedRows = jobsTable.getSelectedRows(); + for (int i = selectedRows.length - 1; i >= 0; i--) { + int modelRowIndex = jobsTable.convertRowIndexToModel(selectedRows[i]); + tableModel.deleteRecordJob(modelRowIndex, selectedRows[i]); + } + } + } + + private void deleteSelectedTemplates(boolean deleteAllTemplates) { + RecordJobTemplatesTableModel tableModel = (RecordJobTemplatesTableModel) templatesTable.getModel(); + if (deleteAllTemplates) { + int rowCount = templatesTable.getRowCount(); + for (int i = rowCount - 1; i >= 0; i--) { + int modelRowIndex = templatesTable.convertRowIndexToModel(i); + tableModel.deleteRecordJobTemplate(modelRowIndex, i); + } + } else { + int[] selectedRows = templatesTable.getSelectedRows(); + for (int i = selectedRows.length - 1; i >= 0; i--) { + int modelRowIndex = templatesTable.convertRowIndexToModel(selectedRows[i]); + tableModel.deleteRecordJobTemplate(modelRowIndex, selectedRows[i]); + } + } + //update the button state of buttons dealing with jobs + this.configureTableButtons(); + } + + /** + * Iterates through all RecordJob objects (or just the selected ones) and returns true + * if at least one of them has one or more has the given state(s). + * @param state + * @param justSelectedJobs + * @return + */ + private boolean checkJobStates(RecordJob.State[] state, boolean justSelectedJobs) { + boolean foundState = false; + List jobsToLookAt = null; + if (!justSelectedJobs) { + jobsToLookAt = this.appLayer.getRecordJobs(); + } else { + jobsToLookAt = getSelectedRecordJobs(jobsTable); + } + + for (RecordJob currentJob : jobsToLookAt) { + for (int i = 0; i < state.length; i++) { + if (currentJob.getState() == state[i]) { + foundState = true; + break; + } + } + } + return foundState; + } + + /** + * Returns the list of selected RecordJobs or RecordJobTemplates. + * @param table jobsTable or templatesTable + * @return list of selected RecordJobs or RecordJobTemplates + */ + private List getSelectedRecordJobs(JXTable table) { + List list = new ArrayList(); + if (table.getSelectedRowCount() > 0) { + int[] selectedRows = table.getSelectedRows(); + for (int i = 0; i < selectedRows.length; i++) { + int modelRowIndex = table.convertRowIndexToModel(selectedRows[i]); + if (table == jobsTable) { + RecordJobsTableModel tableModel = (RecordJobsTableModel) table.getModel(); + RecordJob job = tableModel.getRecordJob(modelRowIndex); + if (job != null) { + list.add(job); + } + } else { + RecordJobTemplatesTableModel tableModel = (RecordJobTemplatesTableModel) table.getModel(); + RecordJobTemplate template = tableModel.getRecordJobTemplate(modelRowIndex); + if (template != null) { + list.add(template); + } + } + } + } + + return list; + } + + private void configureTableButtons() { + if (jobsTable != null) { + if (jobsTable.getRowCount() == 0) { + jobs_clear.setEnabled(false); + jobs_delete.setEnabled(false); + } else { + jobs_clear.setEnabled(true); + jobs_delete.setEnabled(true); + if (jobsTable.getSelectedRowCount() == 0) { + jobs_delete.setEnabled(false); + } else { + //go through all elements and check for attributes PROCESSING + RecordJob.State[] lookForState = {RecordJob.State.PROCESSING}; + boolean foundState = checkJobStates(lookForState, false); + if (foundState) { + //we have to disable the clear and delete button + jobs_delete.setEnabled(false); + } + } + } + if (templatesTable.getSelectedRowCount() == 1) { + jobs_createFromTempl.setEnabled(true); + } else { + jobs_createFromTempl.setEnabled(false); + } + } + + if (templatesTable != null) { + templ_createFromJob.setEnabled(false); + templ_delete.setEnabled(false); + templ_clear.setEnabled(false); + + if (jobsTable != null && jobsTable.getSelectedRowCount() == 1) { + templ_createFromJob.setEnabled(true); + } + + if (templatesTable.getSelectedRowCount() > 0) { + templ_delete.setEnabled(true); + } + + if (templatesTable.getRowCount() > 0) { + templ_clear.setEnabled(true); + } + } + } + + private class JobsTableMouseListener implements MouseListener { + + public void mouseClicked(MouseEvent e) { + if (e != null && e.getClickCount() == 2) { + List selectedJobs = getSelectedRecordJobs(jobsTable); + if (selectedJobs.size() == 1) { + RecordJob selectedJob = selectedJobs.get(0); + if (selectedJob.getState() != RecordJob.State.PROCESSING) { + JobDialog jobDialog = new JobDialog(SwingGUI.this, selectedJob, appLayer); + jobDialog.showDialog(); + } + } + } else { + configureTableButtons(); + } + } + + public void mouseEntered(MouseEvent e) {} + + public void mouseExited(MouseEvent e) {} + + public void mousePressed(MouseEvent e) { + this.showPopupMenu(e); + } + + public void mouseReleased(MouseEvent e) { + this.showPopupMenu(e); + } + + private void showPopupMenu(MouseEvent e) { + if (e.isPopupTrigger()) { + JTable table = (JTable)(e.getSource()); + Point p = e.getPoint(); + int row = table.rowAtPoint(p); + int[] selectedRows = table.getSelectedRows(); + //figure out whether we have to reselect the current row under the pointer, + //which is only the case if the already selected rows don't include the one under + //the pointer yet + boolean reSelect = true; + for (int i = 0; i < selectedRows.length; i++) { + if (row == selectedRows[i]) { + reSelect = false; + break; + } + } + + if (row != -1 && reSelect) { + table.setRowSelectionInterval(row, row); + } + + this.configurePopupMenu(); + configureTableButtons(); + jobsTablePopupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + private void configurePopupMenu() { + //Disable all buttons first + jobs_contextmenu_edit.setEnabled(false); + jobs_contextmenu_duplicate.setEnabled(false); + jobs_contextmenu_delete.setEnabled(false); + jobs_contextmenu_resetstate_waiting.setEnabled(false); + jobs_contextmenu_resetstate_done.setEnabled(false); + jobs_contextmenu_showerror.setEnabled(false); + jobs_contextmenu_start.setEnabled(false); + for (JMenuItem pluginItem : jobs_contextmenu_runPluginMenuItems) { + pluginItem.setEnabled(false); + } + + //edit, duplicate, and show error buttons + if (jobsTable.getSelectedRowCount() == 1) { + jobs_contextmenu_edit.setEnabled(true); + + //Show error button + List selectedJobs = getSelectedRecordJobs(jobsTable); + RecordJob selectedJob = selectedJobs.get(0); + if (selectedJob.getState() == RecordJob.State.ERROR || selectedJob.getState() == RecordJob.State.ERROR_PLUGIN) { + jobs_contextmenu_showerror.setEnabled(true); + } + } + + if (jobsTable.getSelectedRowCount() > 0) { + jobs_contextmenu_duplicate.setEnabled(true); + //Delete button + RecordJob.State[] states = {RecordJob.State.PROCESSING}; + if (!checkJobStates(states, false)) { + //none of the jobs is processing + jobs_contextmenu_delete.setEnabled(true); + jobs_contextmenu_resetstate_waiting.setEnabled(true); + } else { + jobs_contextmenu_edit.setEnabled(false); + jobs_contextmenu_duplicate.setEnabled(false); + } + + //Start button + RecordJob.State[] states2 = {RecordJob.State.ERROR, RecordJob.State.DONE, RecordJob.State.PROCESSING, RecordJob.State.ERROR_PLUGIN}; + if (!checkJobStates(states2, true)) { + //only enable start if none of the selected jobs as any of the States above + //as the only job State that is not listed is "waiting", we only enable the button if all jobs are waiting + jobs_contextmenu_start.setEnabled(true); + } + + //reset to 'done' button + RecordJob.State[] states3 = {RecordJob.State.ERROR, RecordJob.State.WAITING, RecordJob.State.PROCESSING}; + if (!checkJobStates(states3, true)) { + //only enable the "reset to done" button when processes have the state DONE or ERROR_PLUGIN + jobs_contextmenu_resetstate_done.setEnabled(true); + } + + //plugin buttons, enable only when state of the job is DONE + RecordJob.State[] states4 = {RecordJob.State.ERROR, RecordJob.State.WAITING, RecordJob.State.PROCESSING, RecordJob.State.ERROR_PLUGIN}; + if (!checkJobStates(states4, true)) { + int counter = 0; + for (JMenuItem pluginItem : jobs_contextmenu_runPluginMenuItems) { + if (appLayer.getEncoderPlugins().get(counter).isEnabled()) { + pluginItem.setEnabled(true); + } + counter++; + } + } + } + } + + } + + private class TemplatesTableMouseListener implements MouseListener { + + public void mouseClicked(MouseEvent e) { + if (e != null && e.getClickCount() == 2) { + List selectedJobs = getSelectedRecordJobs(templatesTable); + if (selectedJobs.size() == 1) { + RecordJobTemplate selectedJob = (RecordJobTemplate) selectedJobs.get(0); + JobDialog jobDialog = new JobDialog(SwingGUI.this, selectedJob, appLayer, JobDialog.EDIT_TEMPLATE); + jobDialog.showDialog(); + configureTableButtons(); + } + } else { + configureTableButtons(); + } + } + + public void mouseEntered(MouseEvent e) {} + + public void mouseExited(MouseEvent e) {} + + public void mousePressed(MouseEvent e) { + this.showPopupMenu(e); + } + + public void mouseReleased(MouseEvent e) { + this.showPopupMenu(e); + } + + private void showPopupMenu(MouseEvent e) { + if (e.isPopupTrigger()) { + JTable table = (JTable)(e.getSource()); + Point p = e.getPoint(); + int row = table.rowAtPoint(p); + int[] selectedRows = table.getSelectedRows(); + //figure out whether we have to reselect the current row under the pointer, + //which is only the case if the already selected rows don't include the one under + //the pointer yet + boolean reSelect = true; + for (int i = 0; i < selectedRows.length; i++) { + if (row == selectedRows[i]) { + reSelect = false; + break; + } + } + + if (row != -1 && reSelect) { + table.setRowSelectionInterval(row, row); + } + + this.configurePopupMenu(); + configureTableButtons(); + templatesTablePopupMenu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + private void configurePopupMenu() { + //Various buttons + templ_contextmenu_edit.setEnabled(false); + templ_contextmenu_duplicate.setEnabled(false); + templ_contextmenu_delete.setEnabled(false); + + //Edit button + if (templatesTable.getSelectedRowCount() == 1) { + templ_contextmenu_edit.setEnabled(true); + } + + //Delete and duplicate button + if (templatesTable.getSelectedRowCount() > 0) { + templ_contextmenu_delete.setEnabled(true); + templ_contextmenu_duplicate.setEnabled(true); + } + } + } + + private void showAboutBox() { + try { + InputStream inStream = ClassLoader.getSystemResourceAsStream("about.html"); + StringBuffer out = new StringBuffer(); + byte[] b = new byte[4096]; + for (int n; (n = inStream.read(b)) != -1;) { + out.append(new String(b, 0, n)); + } + String htmlString = out.toString(); + htmlString = htmlString.replaceAll("[\\r\\n]", ""); + JOptionPane.showMessageDialog(this, htmlString, "About", JOptionPane.PLAIN_MESSAGE); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + public void windowActivated(WindowEvent e) {} + public void windowClosed(WindowEvent e) {} + public void windowDeactivated(WindowEvent e) {} + public void windowDeiconified(WindowEvent e) {} + public void windowIconified(WindowEvent e) {} + public void windowOpened(WindowEvent e) {} + + public void windowClosing(WindowEvent e) { + this.shutDown(); + } + + private void shutDown() { + if (this.appLayer.getState() == DemoRecorderApplication.STATE_WORKING) { + int result = JOptionPane.showConfirmDialog(this, "There are still jobs being recorded. Are you sure you want to exit?", "Confirm close", JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.NO_OPTION) { + return; + } + } + saveTableStates(jobsTable); + saveTableStates(templatesTable); + saveTemplateTableContent(); + this.appLayer.shutDown(); + this.dispose(); + System.exit(0); + } + + private void saveTemplateTableContent() { + File path = DemoRecorderUtils.computeLocalFile(DemoRecorderApplication.PREFERENCES_DIRNAME, TEMPLATE_TABLE_CONTENT_FILENAME); + RecordJobTemplatesTableModel tableModel = (RecordJobTemplatesTableModel) templatesTable.getModel(); + tableModel.saveTemplateListToFile(path); + } + + private Icon getIcon(String iconString) { + URL url = ClassLoader.getSystemResource("icons/" + iconString); + Icon i = new ImageIcon(url); + return i; + } + +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/tablemodels/RecordJobTemplatesTableModel.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/tablemodels/RecordJobTemplatesTableModel.java new file mode 100644 index 000000000..ddb52f558 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/tablemodels/RecordJobTemplatesTableModel.java @@ -0,0 +1,221 @@ +package com.nexuiz.demorecorder.ui.swinggui.tablemodels; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.table.AbstractTableModel; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; +import com.nexuiz.demorecorder.application.DemoRecorderException; +import com.nexuiz.demorecorder.application.DemoRecorderUtils; +import com.nexuiz.demorecorder.ui.swinggui.RecordJobTemplate; +import com.nexuiz.demorecorder.ui.swinggui.SwingGUI; + +/** + * Columns: + * - Job Name + * - Engine path + * - Engine parameters + * - Demo file + * - Relative demo path + * - dpvideo path + * - video destination + * - execute before cap + * - execute after cap + * - start second + * - end second + * - status + * @author Marius + * + */ +public class RecordJobTemplatesTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 6541517890817708306L; + + public static final int TEMPLATE_NAME = 0; + public static final int TEMPLATE_SUMMARY = 1; + public static final int JOB_NAME = 2; + public static final int ENGINE_PATH = 3; + public static final int ENGINE_PARAMETERS = 4; + public static final int DEMO_FILE_PATH = 5; + public static final int RELATIVE_DEMO_PATH = 6; + public static final int DPVIDEO_PATH = 7; + public static final int VIDEO_DESTINATION_PATH = 8; + public static final int EXECUTE_BEFORE_CAP = 9; + public static final int EXECUTE_AFTER_CAP = 10; + + private static final int columns[] = { + TEMPLATE_NAME, + TEMPLATE_SUMMARY, + JOB_NAME, + ENGINE_PATH, + ENGINE_PARAMETERS, + DEMO_FILE_PATH, + RELATIVE_DEMO_PATH, + DPVIDEO_PATH, + VIDEO_DESTINATION_PATH, + EXECUTE_BEFORE_CAP, + EXECUTE_AFTER_CAP + }; + + private List templates; + + public RecordJobTemplatesTableModel() { + templates = new ArrayList(); + + //load table content + File path = DemoRecorderUtils.computeLocalFile(DemoRecorderApplication.PREFERENCES_DIRNAME, SwingGUI.TEMPLATE_TABLE_CONTENT_FILENAME); + this.loadTemplateListFromFile(path); + } + + public void deleteRecordJobTemplate(int modelRowIndex, int viewRowIndex) { + try { + this.templates.remove(modelRowIndex); + fireTableRowsDeleted(viewRowIndex, viewRowIndex); + } catch (IndexOutOfBoundsException e) { + throw new DemoRecorderException("Couldn't find correspondig template for modelRowIndex " + modelRowIndex + + " and viewRowIndex " + viewRowIndex, e); + } + } + + public void addRecordJobTemplate(RecordJobTemplate template) { + this.templates.add(template); + int position = this.templates.size() - 1; + fireTableRowsInserted(position, position); + } + + public RecordJobTemplate getRecordJobTemplate(int modelRowIndex) { + return this.templates.get(modelRowIndex); + } + + public int getColumnCount() { + return columns.length; + } + + public int getRowCount() { + return this.templates.size(); + } + + public void saveTemplateListToFile(File path) { + DemoRecorderUtils.attemptFileCreation(path); + + String exceptionMessage = "Could not save the templates to file " + path.getAbsolutePath(); + + if (!path.exists()) { + DemoRecorderException ex = new DemoRecorderException(exceptionMessage); + DemoRecorderUtils.showNonCriticalErrorDialog(ex); + return; + } + + try { + FileOutputStream fout = new FileOutputStream(path); + ObjectOutputStream oos = new ObjectOutputStream(fout); + oos.writeObject(this.templates); + oos.close(); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog(exceptionMessage, e, true); + } + } + + @SuppressWarnings("unchecked") + public void loadTemplateListFromFile(File path) { + if (!path.exists()) { + return; + } + + List newTemplateList; + try { + FileInputStream fin = new FileInputStream(path); + ObjectInputStream ois = new ObjectInputStream(fin); + newTemplateList = (List) ois.readObject(); + } catch (Exception e) { + DemoRecorderUtils.showNonCriticalErrorDialog("Could not load the templates from file " + path.getAbsolutePath(), e, true); + return; + } + this.templates = newTemplateList; +// fireTableRowsInserted(0, this.templates.size()); + } + + public Object getValueAt(int rowIndex, int columnIndex) { + RecordJobTemplate template = this.templates.get(rowIndex); + if (template == null) { + return null; + } + + if (columnIndex < 0 || columnIndex >= columns.length) { + return null; + } + + String cellData = "UNDEF"; + switch (columnIndex) { + case TEMPLATE_NAME: + cellData = template.getName(); break; + case TEMPLATE_SUMMARY: + cellData = template.getSummary(); break; + case JOB_NAME: + cellData = template.getJobName(); break; + case ENGINE_PATH: + cellData = template.getEnginePath().getAbsolutePath(); break; + case ENGINE_PARAMETERS: + cellData = template.getEngineParameters(); break; + case DEMO_FILE_PATH: + cellData = DemoRecorderUtils.getJustFileNameOfPath(template.getDemoFile()); break; + case RELATIVE_DEMO_PATH: + cellData = template.getRelativeDemoPath(); break; + case DPVIDEO_PATH: + cellData = template.getDpVideoPath().getAbsolutePath(); break; + case VIDEO_DESTINATION_PATH: + cellData = template.getVideoDestination().getAbsolutePath(); break; + case EXECUTE_BEFORE_CAP: + cellData = template.getExecuteBeforeCap(); break; + case EXECUTE_AFTER_CAP: + cellData = template.getExecuteAfterCap(); break; + } + + return cellData; + } + + @Override + public String getColumnName(int column) { + if (column < 0 || column >= columns.length) { + return ""; + } + + String columnName = "UNDEFINED"; + switch (column) { + case TEMPLATE_NAME: + columnName = "Name"; break; + case TEMPLATE_SUMMARY: + columnName = "Summary"; break; + case JOB_NAME: + columnName = "Job name"; break; + case ENGINE_PATH: + columnName = "Engine path"; break; + case ENGINE_PARAMETERS: + columnName = "Engine parameters"; break; + case DEMO_FILE_PATH: + columnName = "Demo directory"; break; + case RELATIVE_DEMO_PATH: + columnName = "Relative demo path"; break; + case DPVIDEO_PATH: + columnName = "DPVideo path"; break; + case VIDEO_DESTINATION_PATH: + columnName = "Video destination"; break; + case EXECUTE_BEFORE_CAP: + columnName = "Exec before"; break; + case EXECUTE_AFTER_CAP: + columnName = "Exec after"; break; + } + + return columnName; + } + + public List getRecordJobTemplates() { + return this.templates; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/tablemodels/RecordJobsTableModel.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/tablemodels/RecordJobsTableModel.java new file mode 100644 index 000000000..2d2f5fcdf --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/tablemodels/RecordJobsTableModel.java @@ -0,0 +1,192 @@ +package com.nexuiz.demorecorder.ui.swinggui.tablemodels; + +import java.io.File; +import java.util.List; + +import javax.swing.table.AbstractTableModel; + +import com.nexuiz.demorecorder.application.DemoRecorderApplication; +import com.nexuiz.demorecorder.application.DemoRecorderException; +import com.nexuiz.demorecorder.application.DemoRecorderUtils; +import com.nexuiz.demorecorder.application.jobs.RecordJob; + +/** + * Columns: + * - Job Name + * - Engine path + * - Engine parameters + * - Demo file + * - Relative demo path + * - dpvideo path + * - video destination + * - execute before cap + * - execute after cap + * - start second + * - end second + * - status + * @author Marius + * + */ +public class RecordJobsTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 5024144640874313910L; + + public static final int JOB_NAME = 0; + public static final int ENGINE_PATH = 1; + public static final int ENGINE_PARAMETERS = 2; + public static final int DEMO_FILE_PATH = 3; + public static final int RELATIVE_DEMO_PATH = 4; + public static final int DPVIDEO_PATH = 5; + public static final int VIDEO_DESTINATION_PATH = 6; + public static final int EXECUTE_BEFORE_CAP = 7; + public static final int EXECUTE_AFTER_CAP = 8; + public static final int START_SECOND = 9; + public static final int END_SECOND = 10; + public static final int STATUS = 11; + + private static final int columns[] = { + JOB_NAME, + ENGINE_PATH, + ENGINE_PARAMETERS, + DEMO_FILE_PATH, + RELATIVE_DEMO_PATH, + DPVIDEO_PATH, + VIDEO_DESTINATION_PATH, + EXECUTE_BEFORE_CAP, + EXECUTE_AFTER_CAP, + START_SECOND, + END_SECOND, + STATUS + }; + + private DemoRecorderApplication appLayer; + private List jobList = null; + + public RecordJobsTableModel(DemoRecorderApplication appLayer) { + this.appLayer = appLayer; + this.jobList = this.appLayer.getRecordJobs(); + } + + public void deleteRecordJob(int modelRowIndex, int viewRowIndex) { + try { + RecordJob job = this.jobList.get(modelRowIndex); + if (this.appLayer.deleteRecordJob(job)) { + this.jobList.remove(job); + fireTableRowsDeleted(viewRowIndex, viewRowIndex); + } + } catch (IndexOutOfBoundsException e) { + throw new DemoRecorderException("Couldn't find correspondig job for modelRowIndex " + modelRowIndex + + " and viewRowIndex " + viewRowIndex, e); + } + } + + public void loadNewJobQueue(File path) { + this.appLayer.loadJobQueue(path); + this.jobList = this.appLayer.getRecordJobs(); + fireTableDataChanged(); + } + + public RecordJob getRecordJob(int modelRowIndex) { + return this.jobList.get(modelRowIndex); + } + + public int getColumnCount() { + return columns.length; + } + + public int getRowCount() { + return this.jobList.size(); + } + + public Object getValueAt(int rowIndex, int columnIndex) { + RecordJob job = this.jobList.get(rowIndex); + if (job == null) { + return null; + } + + if (columnIndex < 0 || columnIndex >= columns.length) { + return null; + } + + String cellData = "UNDEF"; + switch (columnIndex) { + case JOB_NAME: + cellData = job.getJobName(); break; + case ENGINE_PATH: + cellData = job.getEnginePath().getAbsolutePath(); break; + case ENGINE_PARAMETERS: + cellData = job.getEngineParameters(); break; + case DEMO_FILE_PATH: + cellData = DemoRecorderUtils.getJustFileNameOfPath(job.getDemoFile()); break; + case RELATIVE_DEMO_PATH: + cellData = job.getRelativeDemoPath(); break; + case DPVIDEO_PATH: + cellData = job.getDpVideoPath().getAbsolutePath(); break; + case VIDEO_DESTINATION_PATH: + cellData = job.getVideoDestination().getAbsolutePath(); break; + case EXECUTE_BEFORE_CAP: + cellData = job.getExecuteBeforeCap(); break; + case EXECUTE_AFTER_CAP: + cellData = job.getExecuteAfterCap(); break; + case START_SECOND: + cellData = String.valueOf(job.getStartSecond()); break; + case END_SECOND: + cellData = String.valueOf(job.getEndSecond()); break; + case STATUS: + if (job.getState() == RecordJob.State.DONE) { + cellData = "done"; + } else if (job.getState() == RecordJob.State.ERROR) { + cellData = "error"; + } else if (job.getState() == RecordJob.State.ERROR_PLUGIN) { + cellData = "plug-in error"; + } else if (job.getState() == RecordJob.State.PROCESSING) { + cellData = "processing"; + } else if (job.getState() == RecordJob.State.WAITING) { + cellData = "waiting"; + } + } + + return cellData; + } + + @Override + public String getColumnName(int column) { + if (column < 0 || column >= columns.length) { + return ""; + } + + String columnName = "UNDEFINED"; + switch (column) { + case JOB_NAME: + columnName = "Name"; break; + case ENGINE_PATH: + columnName = "Engine path"; break; + case ENGINE_PARAMETERS: + columnName = "Engine parameters"; break; + case DEMO_FILE_PATH: + columnName = "Demo name"; break; + case RELATIVE_DEMO_PATH: + columnName = "Relative demo path"; break; + case DPVIDEO_PATH: + columnName = "DPVideo path"; break; + case VIDEO_DESTINATION_PATH: + columnName = "Video destination"; break; + case EXECUTE_BEFORE_CAP: + columnName = "Exec before"; break; + case EXECUTE_AFTER_CAP: + columnName = "Exec after"; break; + case START_SECOND: + columnName = "Start"; break; + case END_SECOND: + columnName = "End"; break; + case STATUS: + columnName = "Status"; break; + } + + return columnName; + } + + public List getRecordJobs() { + return this.jobList; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/ShowErrorDialogExceptionHandler.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/ShowErrorDialogExceptionHandler.java new file mode 100644 index 000000000..41e9b7823 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/ShowErrorDialogExceptionHandler.java @@ -0,0 +1,21 @@ +package com.nexuiz.demorecorder.ui.swinggui.utils; + +import java.awt.Component; +import java.lang.Thread.UncaughtExceptionHandler; + +import org.jdesktop.swingx.JXErrorPane; +import org.jdesktop.swingx.error.ErrorInfo; + +public class ShowErrorDialogExceptionHandler implements UncaughtExceptionHandler { + + private static Component parentWindow = null; + + public void uncaughtException(Thread t, Throwable e) { + ErrorInfo info = new ErrorInfo("Error occurred", e.getMessage(), null, null, e, null, null); + JXErrorPane.showDialog(parentWindow, info); + } + + public static void setParentWindow(Component c) { + parentWindow = c; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/SwingGUIUtils.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/SwingGUIUtils.java new file mode 100644 index 000000000..65462e742 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/SwingGUIUtils.java @@ -0,0 +1,26 @@ +package com.nexuiz.demorecorder.ui.swinggui.utils; + +import java.io.File; + +public class SwingGUIUtils { + public static boolean isBooleanValue(String value) { + if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + return true; + } + return false; + } + + public static boolean isFileChooser(String value) { + if (value.equalsIgnoreCase("filechooser")) { + return true; + } + try { + File file = new File(value); + if (file.exists()) { + return true; + } + } catch (Throwable e) { + } + return false; + } +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/XProperties.java b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/XProperties.java new file mode 100644 index 000000000..004b62622 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/java/com/nexuiz/demorecorder/ui/swinggui/utils/XProperties.java @@ -0,0 +1,432 @@ +/* + * Created on 08.02.2007 + * + */ +package com.nexuiz.demorecorder.ui.swinggui.utils; + +import java.awt.Component; +import java.beans.DefaultPersistenceDelegate; +import java.beans.XMLEncoder; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import javax.swing.SortOrder; +import javax.swing.RowSorter.SortKey; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import org.jdesktop.swingx.JXTable; +import org.jdesktop.swingx.JXTaskPane; +import org.jdesktop.swingx.sort.SortUtils; +import org.jdesktop.swingx.table.TableColumnExt; + +/** + * Container class for SwingX specific SessionStorage Properties. Is Factory for + * custom PersistanceDelegates + */ +public class XProperties { + + /** + * + * Registers all custom PersistenceDelegates needed by contained Property + * classes. + *

+ * + * PersistenceDelegates are effectively static properties shared by all + * encoders. In other words: Register once on an arbitrary encoder makes + * them available for all. Example usage: + * + *

+	 * 
+	 * new XProperties.registerPersistenceDelegates();
+	 * 
+	 * 
+ * + * PENDING JW: cleanup for 1.6 sorting/filtering incomplete. Missing storage + * - multiple sort keys + * + * PENDING JW: except for comparators: didn't before and is not state that's + * configurable by users ... so probably won't, not sure, need to revisit - + * comparator (?) - filters (?) - renderers/stringvalues (?) - enhanced + * sort-related table state (?) + */ + public void registerPersistenceDelegates() { + XMLEncoder encoder = new XMLEncoder(System.out); + encoder.setPersistenceDelegate(SortKeyState.class, new DefaultPersistenceDelegate( + new String[] { "ascending", "modelIndex" })); + encoder.setPersistenceDelegate(ColumnState.class, new DefaultPersistenceDelegate( + new String[] { "width", "preferredWidth", "modelIndex", "visible", "viewIndex" })); + encoder.setPersistenceDelegate(XTableState.class, new DefaultPersistenceDelegate( + new String[] { "columnStates", "sortKeyState", "horizontalScrollEnabled" })); + } + + /** + * Session storage support for JXTaskPane. + */ + public static class XTaskPaneProperty implements Serializable { + + private static final long serialVersionUID = -4069436038178318216L; + + public Object getSessionState(Component c) { + checkComponent(c); + return new XTaskPaneState(((JXTaskPane) c).isCollapsed()); + } + + public void setSessionState(Component c, Object state) { + checkComponent(c); + if ((state != null) && !(state instanceof XTaskPaneState)) { + throw new IllegalArgumentException("invalid state"); + } + ((JXTaskPane) c).setCollapsed(((XTaskPaneState) state).isCollapsed()); + } + + private void checkComponent(Component component) { + if (component == null) { + throw new IllegalArgumentException("null component"); + } + if (!(component instanceof JXTaskPane)) { + throw new IllegalArgumentException("invalid component"); + } + } + + } + + public static class XTaskPaneState implements Serializable { + private static final long serialVersionUID = 3363688961112031969L; + private boolean collapsed; + + public XTaskPaneState() { + this(false); + } + + /** + * @param b + */ + public XTaskPaneState(boolean collapsed) { + this.setCollapsed(collapsed); + } + + /** + * @param collapsed + * the collapsed to set + */ + public void setCollapsed(boolean collapsed) { + this.collapsed = collapsed; + } + + /** + * @return the collapsed + */ + public boolean isCollapsed() { + return collapsed; + } + + } + + /** + * Session storage support for JXTable. + */ + public static class XTableProperty implements Serializable { + + private static final long serialVersionUID = -5064142292091374301L; + + public Object getSessionState(Component c) { + checkComponent(c); + JXTable table = (JXTable) c; + List columnStates = new ArrayList(); + List columns = table.getColumns(true); + List visibleColumns = table.getColumns(); + for (TableColumn column : columns) { + columnStates.add(new ColumnState((TableColumnExt) column, visibleColumns + .indexOf(column))); + } + XTableState tableState = new XTableState(columnStates + .toArray(new ColumnState[columnStates.size()])); + tableState.setHorizontalScrollEnabled(table.isHorizontalScrollEnabled()); + List sortKeys = null; + if (table.getRowSorter() != null) { + sortKeys = table.getRowSorter().getSortKeys(); + } + // PENDING: store all! + if ((sortKeys != null) && (sortKeys.size() > 0)) { + tableState.setSortKey(sortKeys.get(0)); + } + return tableState; + } + + public void setSessionState(Component c, Object state) { + checkComponent(c); + JXTable table = (JXTable) c; + XTableState tableState = ((XTableState) state); + ColumnState[] columnState = tableState.getColumnStates(); + List columns = table.getColumns(true); + if (canRestore(columnState, columns)) { + for (int i = 0; i < columnState.length; i++) { + columnState[i].configureColumn((TableColumnExt) columns.get(i)); + } + restoreVisibleSequence(columnState, table.getColumnModel()); + } + table.setHorizontalScrollEnabled(tableState.getHorizontalScrollEnabled()); + if (tableState.getSortKey() != null) { + table.getRowSorter() + .setSortKeys(Collections.singletonList(tableState.getSortKey())); + } + } + + private void restoreVisibleSequence(ColumnState[] columnStates, TableColumnModel model) { + List visibleStates = getSortedVisibleColumnStates(columnStates); + for (int i = 0; i < visibleStates.size(); i++) { + TableColumn column = model.getColumn(i); + int modelIndex = visibleStates.get(i).getModelIndex(); + if (modelIndex != column.getModelIndex()) { + int currentIndex = -1; + for (int j = i + 1; j < model.getColumnCount(); j++) { + TableColumn current = model.getColumn(j); + if (current.getModelIndex() == modelIndex) { + currentIndex = j; + break; + } + } + model.moveColumn(currentIndex, i); + } + } + + } + + private List getSortedVisibleColumnStates(ColumnState[] columnStates) { + List visibleStates = new ArrayList(); + for (ColumnState columnState : columnStates) { + if (columnState.getVisible()) { + visibleStates.add(columnState); + } + } + Collections.sort(visibleStates, new VisibleColumnIndexComparator()); + return visibleStates; + } + + /** + * Returns a boolean to indicate if it's reasonably safe to restore the + * properties of columns in the list from the columnStates. Here: + * returns true if the length of both are the same and the modelIndex of + * the items at the same position are the same, otherwise returns false. + * + * @param columnState + * @param columns + * @return + */ + private boolean canRestore(ColumnState[] columnState, List columns) { + if ((columnState == null) || (columnState.length != columns.size())) + return false; + for (int i = 0; i < columnState.length; i++) { + if (columnState[i].getModelIndex() != columns.get(i).getModelIndex()) { + return false; + } + } + return true; + } + + private void checkComponent(Component component) { + if (component == null) { + throw new IllegalArgumentException("null component"); + } + if (!(component instanceof JXTable)) { + throw new IllegalArgumentException("invalid component - expected JXTable"); + } + } + + } + + public static class XTableState implements Serializable { + private static final long serialVersionUID = -3566913244872587438L; + ColumnState[] columnStates = new ColumnState[0]; + boolean horizontalScrollEnabled; + SortKeyState sortKeyState; + + public XTableState(ColumnState[] columnStates, SortKeyState sortKeyState, + boolean horizontalScrollEnabled) { + this.columnStates = copyColumnStates(columnStates); + this.sortKeyState = sortKeyState; + setHorizontalScrollEnabled(horizontalScrollEnabled); + + } + + public void setSortKey(SortKey sortKey) { + this.sortKeyState = new SortKeyState(sortKey); + + } + + private SortKey getSortKey() { + if (sortKeyState != null) { + return sortKeyState.getSortKey(); + } + return null; + } + + public XTableState(ColumnState[] columnStates) { + this.columnStates = copyColumnStates(columnStates); + } + + public ColumnState[] getColumnStates() { + return copyColumnStates(this.columnStates); + } + + public boolean getHorizontalScrollEnabled() { + return horizontalScrollEnabled; + } + + public void setHorizontalScrollEnabled(boolean horizontalScrollEnabled) { + this.horizontalScrollEnabled = horizontalScrollEnabled; + } + + private ColumnState[] copyColumnStates(ColumnState[] states) { + if (states == null) { + throw new IllegalArgumentException("invalid columnWidths"); + } + ColumnState[] copy = new ColumnState[states.length]; + System.arraycopy(states, 0, copy, 0, states.length); + return copy; + } + + public SortKeyState getSortKeyState() { + return sortKeyState; + } + } + + /** + * Quick hack to make SortKey encodable. How to write a PersistenceDelegate + * for a SortKey? Boils down to how to write a delegate for the + * uninstantiable class (SwingX) SortOrder which does enum-mimickry (defines + * privately intantiated constants) + * + */ + public static class SortKeyState implements Serializable { + private static final long serialVersionUID = 5819342622261460894L; + + int modelIndex; + + boolean ascending; + + /** + * Constructor used by the custom PersistenceDelegate. + * + * @param ascending + * @param modelIndex + * @param comparator + */ + public SortKeyState(boolean ascending, int modelIndex) { + this.ascending = ascending; + this.modelIndex = modelIndex; + } + + /** + * Constructor used by property. + * + * @param sortKey + */ + public SortKeyState(SortKey sortKey) { + this(SortUtils.isAscending(sortKey.getSortOrder()), sortKey.getColumn()); + } + + protected SortKey getSortKey() { + SortOrder sortOrder = getAscending() ? SortOrder.ASCENDING : SortOrder.DESCENDING; + return new SortKey(getModelIndex(), sortOrder); + } + + public boolean getAscending() { + return ascending; + } + + public int getModelIndex() { + return modelIndex; + } + } + + public static class ColumnState implements Serializable { + private static final long serialVersionUID = 6037947151025126049L; + private int width; + private int preferredWidth; + private int modelIndex; + private boolean visible; + private int viewIndex; + + /** + * Constructor used by the custom PersistenceDelegate. + * + * @param width + * @param preferredWidth + * @param modelColumn + * @param visible + * @param viewIndex + */ + public ColumnState(int width, int preferredWidth, int modelColumn, boolean visible, + int viewIndex) { + this.width = width; + this.preferredWidth = preferredWidth; + this.modelIndex = modelColumn; + this.visible = visible; + this.viewIndex = viewIndex; + } + + /** + * Constructor used by the Property. + * + * @param columnExt + * @param viewIndex + */ + public ColumnState(TableColumnExt columnExt, int viewIndex) { + this(columnExt.getWidth(), columnExt.getPreferredWidth(), columnExt.getModelIndex(), + columnExt.isVisible(), viewIndex); + } + + /** + * Restores column properties if the model index is the same as the + * column's model index. Does nothing otherwise. + *

+ * + * Here the properties are: width, preferredWidth, visible. + * + * @param columnExt + * the column to configure + */ + public void configureColumn(TableColumnExt columnExt) { + if (modelIndex != columnExt.getModelIndex()) + return; + columnExt.setPreferredWidth(preferredWidth); + columnExt.setWidth(width); + columnExt.setVisible(visible); + } + + public int getModelIndex() { + return modelIndex; + } + + public int getViewIndex() { + return viewIndex; + } + + public boolean getVisible() { + return visible; + } + + public int getWidth() { + return width; + } + + public int getPreferredWidth() { + return preferredWidth; + } + + } + + public static class VisibleColumnIndexComparator implements Comparator { + + public int compare(Object o1, Object o2) { + return ((ColumnState) o1).getViewIndex() - ((ColumnState) o2).getViewIndex(); + } + + } + +} diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/about.html b/misc/tools/NexuizDemoRecorder/src/main/resources/about.html new file mode 100644 index 000000000..16729344f --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/about.html @@ -0,0 +1,21 @@ + +

Nexuiz Demo Recorder v0.2

+ Written by GreEn`mArine
+

Credits

+ + +

Usage

+ Open the help in order to find out how to use this software. + +

License

+ GPL v2 + \ No newline at end of file diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelp.hs b/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelp.hs new file mode 100644 index 000000000..2bd2efdd2 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelp.hs @@ -0,0 +1,42 @@ + + + + + Demo Recorder Help + + +top + + + + + + +TOC + + + +javax.help.TOCView + + +DemoRecorderHelpTOC.xml + + + + + +Search + + + +javax.help.SearchView + + +JavaHelpSearch + + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelpIndex.xml b/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelpIndex.xml new file mode 100644 index 000000000..e2b16916f --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelpIndex.xml @@ -0,0 +1,4 @@ + + + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelpTOC.xml b/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelpTOC.xml new file mode 100644 index 000000000..2e12b8dd1 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/DemoRecorderHelpTOC.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/JHelpDev Project.xml b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JHelpDev Project.xml new file mode 100644 index 000000000..e8611a212 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JHelpDev Project.xml @@ -0,0 +1,32 @@ + + + + + + +DemoRecorderHelp + + +D:\Daten\Eclipse Projects\NexuizDemoRecorder\src\main\resources\help + + +html/introduction.html + + + + + + + +Demo Recorder Help + + + + + + + + + + + \ No newline at end of file diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/DOCS b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/DOCS new file mode 100644 index 0000000000000000000000000000000000000000..4a370ac5a09954a48ffcec1b7ee4be72203d4849 GIT binary patch literal 2905 zcmd^9Uu;uV81MbQ^THb}F)4pch~ZBls00`BPfYHEi9s@G$XJj$VZ;)cn~90?LYkbX zONyNqb9I}nr6FUsA`io^6WF?pbhwT#6jDZMH%)LMFt!rSK;_BrJGZwjp{R+E=G>n1 zo!|HSe&6q$+ZFyG&^JJ_>;`miLPz`z82}z2VUOnjZ}(w!0;?H|25?jU~w* z2>$GeQH>LiWHmn+TN=|~ENW`gh4F$^80Co6HkrZp=5;!8IvNYP5Oc?VlB~{5@OdX^ zDcT?**xy)>zT6VaU?=I_SV{U3K1kr|P6yfZHm`ychtJ$0cLObS=1$e$=RN3gP=z|3v1s1KioTPS ztdHn)fq6QlyB4~Cu^U6M?{z9CR{M*mlzi9AAwGMsZ$n*J_1U0n!7F=-!f^N170edK zDhVCV)!%BtO>5J9t&8r_VJtIBJNEsSg<=y!cshu~cgiP(K>qXSb;t zZ=R9pyPQxg??<)N%DQTPzf@@K62^1X_*xXcJk~Hvk;0*U{5o9*6FQ+|a&2P^Tk+0H z2q@S}ba9*(pRbWx8aq0z5aMUoWMDA4Uc%tWvo7o&B17ln8;Jlvj6Cncud>>Yg5Kcy zAt^pP$)zU(CU!U1YH5gfu6EIKVVMrSUiyxt;aqZZ2FBpV?$M8748Pe$@fZ&4aEAvD zb<^$N!MF(z`*!Cf6t>f*m?V5y7r|Z$^V##=bDM z!+UYSNys^OEyn4asYDA$!>VtgAr^xBP5lnI|6Uk}@@yei6D3(nKkVB~@0~PY?o+1! z?&(*Cw8IU178F^PKS<$lQYVM}#LZ$rG07>@%AM_)bp9Aw?&3b|YOr`jwnkfW`e0UF z2)MTmti%LfIj!UPU_=+a^mBLbJRc&=LX+c>E*;ej?xSIx&|%-M<7dK(K!%u?0Cyez zz%OO-(hd_7+Dq%Rm|!V}SeidXQ&93I2|{ZMGc=}job2R>H#~B8DcMpWwS3Nm*1BAX zVQsS?Gtk;_-N6-8CQb!d!pcw9nDF7wG0X=^Y)Oy$+`Rj>uJnjLgZE}=Dnvz$zPa-{ z&gM*?VAKkAqd%|UBRC&rzh#0pd$y-=zOKa!{rCRK>-SO5c zgH2l-T}4;QJ)eL*DtRimwMrNnq#pKDx}j_0n#j0lqJamOn<~oOmLhXNitC(EQ<-E@ z1ygcW#`Izav29(?6UlN)rL8Torg~XwYX6gh(~Gffy~%PsmxfhP-j}1wjM67S6?1~s zmPSh|s>)Wj*HU&WT?%K?;#S2_+Zl4ppuOG_b7?X6zN7^|S!${s0j1|8XT)69r9^rx zsFYE<6>5(W{{#NGZmDC1%Zl)Cnig44V^mzrKQKw53Zn{C<(4a13ob#=Z^cm&+R)S1 Ls?MIBoh|Se literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/DOCS.TAB b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/DOCS.TAB new file mode 100644 index 000000000..b61279852 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/DOCS.TAB @@ -0,0 +1,14 @@ +eÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÝÿÿÿÿÿ÷_÷_ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÝÿÿÿÿÿÿÿÝÿÿÿÿÿÿÿuÿÿuÿÿÿÿÿÿÿÿ÷_ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿuÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÝÿÿÿÿý×ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ÷_ÿÿÿuÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿý×ÿÿÿÿÿÿÿÿ÷_ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ@7û*ÌebøÖBìå(⍦Ob³J6,Ù© +³vRꯪŒ/ŽX³r(͋1‹3,ϋª8³J4³Š6/4³<£J8¨Ò¨â¯3þ*0³K¬É‹£J³Š4º¾Œ+Œÿ‹4³h£"(Ì£J4®º+9 ªÌê/Œ.»Ì/Œ‹Œ*7¢Œ.9"*6Ë0¿üÈ£(£Jî£ +Ì*4£;*1¤,⮣K0£ +0³"£ºþ¨ÞʸÒó 6ªÊ»³ 2(ªªªªªºªª®0¸Â£ +£"º®3âŒ*͸ª£ +4®4£Kªº0ª¨Â¨Ê(ȳ +ºªª®þÌ.¯®0£ +0º2¢ºªìª³ +Ì*0¾0³:.ŒŠ6(Ê(È£"Œ+þ꺌*2.몣 +ªªº0ªªº2.£ +0ªª³ +0ª£ +ªª£ +«Œ¢Ì*ªŒ*0ª«ªê«Œ*0ªÌ,ÂÿÌ,ÂÌ,ÂÿÿÌ/0¿ÿÿÿÿÿÿÌ/ó 0³"Ì/ÿüÂÿÿÌ,È¿ÿüÂÿÌ/ÿ0¿Ì/þ«ªªªªªêªªªªªªªªªªªªªªªªªªªªªªªªªªªªªªª¦ \ No newline at end of file diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/OFFSETS b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/OFFSETS new file mode 100644 index 0000000000000000000000000000000000000000..bdbbc97d58d23d6b0ffcf8d23f4e6fe0e7e220f4 GIT binary patch literal 68 zcmV-K0K5MOfof6Mjk-!W%51M0t~oFIf20Y3J}H(IvE1qh z!Yr@)7olD++~rFJmSQQrv(uyr<6)OB!jWS29ck>lHz{g7dJ-c(Akq2?~8)cN25RTe;7 zbYhZ@!GAW>I`I;zw|p(NGJo}#w~!7ycUrbvA(nw{D2P8oyy_8M2%u$M)5*lp8J%q7 z%bR+=O1@fk0v|bS_&BJY3$%cQJi<`I0BFx}zd#YOTsWoc*!<;sX4k@7v)gtod+r{{;|Dl{`X+ve3}6 zdv7qd*tHt#$9f@Rb6)y>9uPkflVjeyo=3>JKN?mfL=2J472!7SsUV6jS2=fCLOg1N@^Adem$x+f$O&X$vN%3pNMyx1$4P_sUU!fd4y$E>&?( z3a5YIa(X%#QgJie3p^49=d$}{z@Rw@J&|KrMkaB zVbTh{4~J0vQ2k2Il#UoLX1mq6P&m+?GtBq0(QH0{aUX+B7eF>wnLnjOrLN)79}QIm zFGf}dWn> z2WC;TFWZyVSc)J0 zX4SoR3QpiET88 zmbikhtsQ}hbHe`VpA={NNiCiS0GH1KRGD{wmEc@EZn~#^w(@iOD@GbKlRl*idU+YM zf(>lwynwg!xsjd};HRJ*dT_}6YtUoYG**E3TB(+JkGvL zQL~nkkk>!U)AnzjJog`0~KDc(4W2EgsyU>UIKO67(x;h(YPx z>Ic#qE#KBb$^A|=5WAl$%qc>5OxU&VapSnV0H7h`$<8r*gMC{Vj5Dr{vt=!?Z8Rez zF>(6T_j0?}ik=8~2bCKdwSkBqR>T8DgEOhHpE0^?Z#*-v?g5h=XLKdLi}D8!6{$@TYlC4GPwDL z12t;$&q_*d(I9muzj>5Bkd2URJZkKpI6$Pu>)~;C!Lf&)PrF$d|S1?ANReQaWcdN3K@KZWd%xk0I%L7sErL zfihLo*?NyMiP%I}e|Ya$Z}hW&;6Yj{1k%0s6O6Wkj#qGV_`R&kqAL&`GnN&$ z8D6k8Y|E8daXL9TdadWloE-?x&q5+E4i(PUW)?^&MRc4nB;>EAm~{Bg#=gpkxPl7 z*H@B-MfOwy3;&zhLKDZPeP?+_k80%SSU39*TjD2I8zTL=xcRo@)5DOHiOeW4MQHF) z7VkE2XyYr3xtl{Z8?mL3{2_Ch>3R5mSVJXGUh{6+P&16221z$j7RvDc9vrV2U2b0NRMVL0LyZl7l5-r~8fPB8sb};Ai|in%?BY{8UxZ~3 z*-B7}S&0q7YYgEt#_OcN2GTU>oig|N*8};Oi-m!G>qCAKGb`|xzzKct^_$2 zv?x?y9k4XJdm-`l-5ef^st9N|kO1#zG(wc69MyeQkpPtL>#BXwTQ)OOP#c)VopZNM zYr@~Lb~mrajGBBfL6rdj^VquQ-3ctO?KO?DTjLh5aQKu~s5)AFz5ZQWygMdmeZZmk zYdt~Dy(&q?Cq^>WNM5ywvY)RF-_8}^+}A5QtnDY2{emg853P3RGcTx)WLQeSQJ>^i}4ea3^w__AesV)QAGxK z9mT@aupgNij0CI)&2EBojwA&BWpMZ(7h!^h1OaT@0{y<5!ZR)zkv+N&E3C`EP1DQ1 zX+aa@LCWS&qm_b-T#lo_7`Ra*X!EWTpLPkT#j4=#T~~_ytU@esxHh}O4WdP$*iVhP zTfz-!<3iKqnP=)c7jM8!M#0G07i7IC2DdEJ?qJpj8kg&iJDhh16)NQmxtkWtfYyp9 zhzg;swIjZ7A|KlQ?+bs;2nCv|)Dmwpga_I+O&sG5efr5o;XKQtfehXo7;T5X{ z-xGF&4-PZPZww!c&rKT0$cMxjQFdS}Zw>Vs(ob&49PumVJsYF8%z8vzNdwvWkv}Ju zNx_{;YK_q7;6iC0HS37e)1%sk5Q&7hQ20+V>hxkGSB>F zQsbqfmR;rCE^3puI`P>1`-N+q>MMX&R6se}o^khE$|*7^aHMVZe!USC>RxOni9A;a z#CMocsqRz;7eNe;I+hBs)a(uQ(J$#WT?cWR=m`ayGtL%jyu`#vUdIA=*C$^<$>H$0 z5RW!~lPOYl4~1pde6fZrA`0o;W|lF{AT_c%M7t(k(M-#;86nU$8to!GRR}@pM z^#boNJO1$pFsnIsLdUTPRxiY)xhq7K&dZb260#bC-pYU_>8)qFyP7ysRY z+s0vei}b#|0=J`QVc{l-Cs#(S;}@tn<42(NYKfe7(}x8%z-=XjWFl3Zz6m^{JfGs= zy{ep;X%ZUE)VfeLx@TP3iljUL?Ax(5Jn3+uu4}T*`vdS+Cg>ci6=k<|uqxvqbQZS^JNp z=JUl!b&3NhPI_C2M=gH_%=By;^R3Y@l-53b)fF?n6-k9G)`^O-`xY9GUfr5J6I-P(n&@3< z{D_AFrWmL1F)XC1o2;V1ALR;l6&6i!jXx@{4=M$!v15OmLd zFhN%U1fquKzR-;=M^~&1=_9o~#XGq$wM+WiC%U>>ZoXF>xF?PFdsbAiDL+@$# z%S6vNCX?$dMT(GH*lv*GsNAO7d&RYfC;-!;fG=8MP^NSxUpC%;tXTa0F+(+jb}Qk7 zbit|nWk*_U13)X!mLD^m#HIc4>@VvnP1z1Ju;O(T@Hn1rg-*G1u)sV35%!Hx~r_Mk-IX}E@oWm;dXbb0{e5 ziq4BIELkgH9V_~}TXpk~d3}$fan}<5Mv-s@=um&hn~9x_*atb2NVMROXNRv_FZb0P zxhZ;Axflkv8kPsgVR+hgedCR6L!R;Ja1|g12lDt#r1)K4oCQlb`NSXfnHh~a%WgqH zk;fJrybH-Pr-GX(F_tQ`D>;L6otq>5<7-?#7w!nYiy-A+&;z?860!PP!2pp`{5=5| z*~!Gx;l_nrj@s(+D$3YyLaa0^x)@8`WAG2=p+SfHDEu$|DleK$Sj>o6%jINUF^n@Z zTa@BH^riY77p-_r7tLi2{tQUD>;Z9-M&~OhTL8cbKrj1*)eU#9Zj+5JB${f(Jc4o* zI@DwEK5B?WD1-%`2R%YqLDAVTWv9sB+x8E?PgABO=*6DCbQ(%cE@X;W`(r9A(POC{ zt*aX>OY%XUP$FkpK-N{Zu|E8g3x8ZZkT%h4M>hGQ)LwYpDL8J3CEa%*)O%iHh1t=9 zc}pvh-~xEVuT8PheRnsDG10#jQcW@!Ejz z-)NgXXc^h~%A{NDYtg_cp0S%#CuSNy($CnGR@B~?=ztgKtfvZBJX#=Vfoy=wTeX#2^XqC5E`LX4O)rFT`&#g-fP7B$a~Ac6do5$LnGe(0}f zle^^+t+k3hLG2t(DN9Cm>;9s3-OmHJDWzZA=aJ(mkOThC&7YJQcpHCXfHsA*vQBWQ zo*TsogzZFDFxU*)ZJ61`db`(8|HR``ih-<1rJ^dOYH44z0-kfi7=N5SNQ!5!R#9y@sn%~xO}FEUP{6$yH&W>;tJW^gp*hBE|tbz{%0hGk_!Ap*g4vxDb$ z*R@0X4o_wgF|!*3+InVpc>FFh{YhUjg+Gz2Hq}C9E8>XXi}7m9`Y*4-4S;^JH+gIJ z9#byf25|vR$2JE zKm0T5<_7(XPCFeourVjyswL;YfApK;&h1QKV;L!SR<_THPtzFt?)od4oXq$VnOCTD z@+6*Z8w^Rk)LqLaOe_*-VsN7$=gA&`-mwtx5d>hph= z5>)y8Q?>LnANP6m#CLbql0@@svUZXE5QRN% zUC(*}Fw11NEo~MalE}XS?h2WDmY0z7wwmU-`B?+(fv2R>HmlN^@>LJ-KT4N08a7^@ zW#%@hJYwn>4dS~N%=FqO<-Nf_Db&*G|D8V+($Am3v~8#sHA?A+uqfwT{WL5n2+1~dX!x5S*xS+STAbIm(be%_3SttrxQ9-_}Q@15M?O0r?3$>&N zL`^|$@k2(7!}Q(H=(GFa&QPa2QBaf+Y|r%AD)ftP=moO^+dD?b`iZZvFGvj=eC9lf z-R+%yyRG1^L7}wGJMa|<86thgCzJgI7**e#&lAG2aT^PQ2#UpMpbUwod zd6-p`$1*hpE|}kH#rl=MUgo|ZgEX$ps}n`l2{3}WJh+FJcZE&fPvbe|c8ncx>-ri9 zjqelC6N~;ej!u>gyP&qI8sX9%W0ho~(cbq@hX!lMQqAg|K4oV zCZoI@2gb~=c$*uAUV*GOt!GLp<(R>UZsIS2_@{P!OkHUaS>1k{JK?IxJ+%IX@%mGq zx^(*O{2V{b-I-26qYjrU^QA@#&AX_Vx43mRY&%4ath3CHD)YOm6vB?>Pp1VmsOr@_ z&P0j`7z)Kj#z&Z3YUoNiI(Xygd4r}dsq5L}{K!kE$>VPH2TxQ;MhP<1ED zu$dRRH~Q3d`e4#h4x6_J8#tTH)YiVq!aNB(N+x6!4EbXn+vb;WYkmE^Yf%M0xf>Ql z)|Hb;8BepP^0ZY=@T=X|G;h&9rOUvtzv?)2E3V?Lo|2?OgGjIv8JljAU(ztbN_I*T z%Ya=K$q@(1GL6b;Rxv`UJ3%ktg&D<_NzA$sPdWx;;NqIlt#5dcPE z6hygUzo6NE}~F{V^fM%K11-u3-F(WOGp)Oo#QD){HF>P zD-GQT8!FM6y(*&*M`!-V&1Ky`X5}Rkuv;h{1KN(-^a*Axo%sx6XUoag;QI*3h&$Qb z@{EJWu*y)o%DhJQU>p}p5gz9A5ZRe{3Lb~%Zl6(CS+X;lxX|yDA+3LuPczOvlxK1? zm*{G1i72`Kcq3)@Vs3}DZG)h&tcB0+t-ECv!-cQmYQe)#dqWTvgLjIx;Li;hDV=4{ z#^OtVLUCDB#K$yDIViS!)sbs>t5=o++Vgyd#n6K7 zL}Xk5Dc9+HgA4E((hr>SzXEl%ZZ5|^G%Z56O$16c^kDx)m$_;wSSLzk>3R9p@sKVm zv1YOLYIH;)KzKZXTvuK_mMNSR>K+}+x~zZ4pcT3Ne2HR9iNhKBdGQb}Op ztgi`O{G7kd*xs*b-Jc8XgFF+rm-spWeT|F@I|uYE7-Q9Tw#>?k`lQNCSJE2?#<@$% zZA*gX%z0n2%I%9N>1yK6Yf4T_=J-Si>mz&EX}q6SS9_$_5mH*=bjE?KcBPa3#TBLs zmYf^A-vXjL_j=V<%GEX;9E0QJt|>?A*~v;05GPFd;S|#W_`Ykm{t)r}Q~JB02F{K@ zev$@7WjiU3%DJw6KJicuVr8KYY~^sVx27E75sJv6LzGQQe9&kDM?B-v^lzn9eC)M% z^y=vTX|=D+XXEp8*)WwRqqCMR2JsPoE4^Ltq_AGQqN3U%(=<=={?}c=C=_~^ywbCB zyni8bHU!3h_<6PHiB37CgwR=YG{1&!26FGkohAEN6jYPU{@t0<>4hlszdVkJbeFh1 zo|l`pB!4oW)h~x1{I14&)^CO(;>EUd=;XqT%ELS%o5!yjCo<}0a_zbQ3L;13IeYZZ zksE3~x)~$x2BSOKT`v!obtSCF@iTUM(q2`ZXue*@Hc3^}>=Njc2X~-G^)r#yLQ{gm5#hqH+*{8Ia;0CA;<(Kk-?tQEc=+fo656}hVA`>}TKH$a*;OOSsj z&5;okL5}y5o$pk+0M*Wm>8a$B6G&W5uQlppC7BO%+1Vy*3Yf7KLRO1er`8$0n)P6f z@+A}ng?EuSra#}c3KrIKi}_|=pLp_@=Yl&Ad_kAk=zmdpKokIPA93*#6RkGSlw1cI z)qbpk&x*d1Q>MA^gCBrvG64AGE1k4G+s)8a8*qf9USTWPCce$N>P}kxHvou+-MxSR bc>l-7{$peRv9bTy*ne#7KQ{LN+1UR8w`79^ literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/SCHEMA b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/SCHEMA new file mode 100644 index 000000000..ec57b7c29 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/SCHEMA @@ -0,0 +1,2 @@ +JavaSearch 1.0 +TMAP bs=2048 rt=1 fl=-1 id1=1038 id2=1 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/TMAP b/misc/tools/NexuizDemoRecorder/src/main/resources/help/JavaHelpSearch/TMAP new file mode 100644 index 0000000000000000000000000000000000000000..d004b6247ed2a68b2f4fc9e4abb5b64dbb5679df GIT binary patch literal 14336 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 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/Map.jhm b/misc/tools/NexuizDemoRecorder/src/main/resources/help/Map.jhm new file mode 100644 index 000000000..9d004f116 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/Map.jhm @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-how-it-works.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-how-it-works.html new file mode 100644 index 000000000..4597748ca --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-how-it-works.html @@ -0,0 +1,77 @@ + + + + + + + + + + +

How it works

+

The following is a description of how +the program processes a single job:

+
    +
  • The program takes your original + demo, e.g. “C:\Nexuiz\data\demos\test.dem” and creates a + copy of it, the cut demo. This file has the name + <original_demo_name>_autocut.dem. The cut demo is + different to the original demo in a way that console commands have + been injected, so basically the engine thinks that you had + entered them yourself (e.g. cl_capturevideo 1 to start recording).

    +
      +
    • Since it is possible to inject + any possible console command, the first command that is injected is + to disable rendering (r_render 0), save the value of your “volume” + setting and then setting it to 0 (as long as you have sound and + rendering disabled while fast-forwarding in the preferences of the + Demo Recorder). Then a slowmo 100 command (first stage value from + the Demo Recorder preferences) is injected in order to fast-forward + the demo.

      +
    • Then, when the game time in the + demo is about 1 minute less than your specified start time of the + job, slowmo is reduced to 10 (second stage value from the + preferences)

      +
    • Then, when the game time in the + demo is about 5 seconds less than your specified start time, slowmo + is set to 1, rendering and sound is enabled again, and + whatever your put into the exec before capture field is + being injected, too. Then, the values of cl_capturevideo_nameformat + and _number are being saved to a temporary variable and are + overwritten with defined values (autorec and 1234567), + so that the Nexuiz Demo Recorder will know the exact name of the + output file (which is necessary so that it can move that file to + your desired video destination/location)

      +
    • When the the start time is + reached, cl_capturevideo 1 is injected, once the end time is + reached, cl_capturevideo 0 is injected.

      +
    • Shortly after, whatever you put + into the exec after capture field is executed, and then the + original values of cl_capturevideo_nameformat and _number are being + restored.

      +
    • Then a disconnect command is + injected

      +
    +
  • Next, your specified Nexuiz + engine binary is launched. The parameters given to the binary are:

    +
      +
    • The content of the engine + parameters field of the job, and

      +
    • -demo + <relative-demo-path>/<demo-file-name> (this will + start Nexuiz, e.g. -demo demos/test.dem, launch the test.dem + demo, and the engine will play the complete demo until a disconnect + is issued (which we have injected above), and then Nexuiz will + close automatically. This -demo parameter exists since Nexuiz 1.0

      +
    +
  • Once the Nexuiz Demo Recorder + notices that your Nexuiz engine binary closed, it will look for the + recorded video file in <DPVideo-directory>/autorec1234567.<avi/ogv> + and move it (rename it) to your desired <video-destination>, + keeping the original extension of the file. In case the preferences + are setup to not overwrite an existing video file, a file with + ending _copy1 (2, 3, …) will be created.

    +
+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-prelim-stop.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-prelim-stop.html new file mode 100644 index 000000000..dd37b2fb6 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-prelim-stop.html @@ -0,0 +1,23 @@ + + + + + + + + + + +

Preliminary stop

+

If you want to preliminarily stop the +recording process, the only thing you can do is to switch to the +Nexuiz Demo Recorder window and click on stop processing. +However, if Nexuiz is still open and processing a demo, you have to +end it manually of wait for it to end. This means that clicking on +stop processing button will simply remove all jobs that +haven't been started yet from the internal queue of jobs that would +normally be processed (of course they won't be removed from +your jobs table).

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-table-settings.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-table-settings.html new file mode 100644 index 000000000..26e28a033 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-table-settings.html @@ -0,0 +1,26 @@ + + + + + + + + + + +

Table settings

+

Both the jobs and templates table can +be customized. You can define whether to sort the jobs/templates +according to a particular column, you can change the order of +columns, and you can add and remove (hide) additional columns by +clicking the small icon as shown here on this image:

+

+

These settings are being saved and +automatically restored whenever you close and open the program. In +case you messed the columns and want them to be reset to default, go +into the settings sub-folder of the Nexuiz Demo Recorder and delete +the jobsTable.pref or templatesTable.pref file (.pref for +“preferences”)

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-topics.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-topics.html new file mode 100644 index 000000000..11524c30e --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/advanced-topics.html @@ -0,0 +1,18 @@ + + + + + + + + + + +

Advanced user topics

+

Here you find additional information +that is not mandatory for you to know, but might help you +understanding what and how the tool does stuff, or what you can do +with it.

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/basic_tutorial.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/basic_tutorial.html new file mode 100644 index 000000000..4f96ca4ab --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/basic_tutorial.html @@ -0,0 +1,169 @@ + + + + + + + + + + +

Basic tutorial

+

The following images shows the main window:

+ +

You will notice that the main window is separated into +4 areas:

+
    +
  • The menu bar (File, Help)

    +
  • The "Templates" area which has a table + and 4 buttons on the right

    +
  • The "Jobs" area which also has a table + and 4 buttons on the right

    +
  • The Start/stop processing buttons

    +
+

Step 1

+

Minimize this demo recorder application for a moment +and figure out which demo(s) you actually want to record. Once you +know the names of the demo files, you have to figure out the start +time and end time (when you want the recording process to start and +end). You have to be aware that the Nexuiz demo recorder needs a +value in seconds. The problem is that there are 2 different kinds of +values. +

+
    +
  • The first one is the one you see in your HUD in + the top right corner. This value is not useful for the Nexuiz demo + recorder, because the time is shown in the <minutes:seconds> + format. Also this value will be changed/reset to 0 in case the + ready-restart feature was using in the game, which resets the time.

    +
  • On the other hand there is the absolute time + value. It represents the amount of time that passed on the + server since the map was loaded. This value will always increase + each second, and won't be reset to 0 after a ready-restart call + either. This is the value you need for this tool. You can obtain it + the following ways

    +
      +
    • 1: There is a sub-folder "tools" in + this package which contains a .patch file that allows you to patch + the source-code of the Nexuiz 2.5.2 engine. What this patch does is + to add a new variable "showgametime". Once you patched + your source-code and compiled the engine, you can set this variable + to 1, and then in the lower right corner you will see the current + game time. Please note that I cannot help you with patching the + sources and compiling the engine. For Microsoft Windows users I put + the Nexuiz binary (darkplaces.exe) into the package as well, so you + can use this one (copy it into your Nexuiz directory)

      +
    • 2: If the demo was recorded with Nexuiz 2.5 or + newer you can obtain the time values by opening the console and + entering this: prvm_global client time

      +
    • 3: If the demo was recorded with a Nexuiz version + older than 2.5, just pause the demo at the desired moment, open the + scoreboard (by default: hold tabulator key). The scoreboard will + show you the current time in <minutes>:<seconds>. + Calculate the time using a calculator (time = minutes * 60 + + seconds)

      +
    +
+

Make sure that you know the demo file name, start and +end time, then open the Nexuiz demo recorder tool again

+

Step 2

+

In the "Jobs" panel, click on the +Create +button. A new dialog will open which asks you for all different kinds +of things:

+ +
    +
  • Engine: click on the ... button and specify the + Nexuiz engine that should be used for recording

    +
  • Engine parameters: Only fill this out if you already know what it + means. Examples include to execute a particular config (+exec + someConfig.cfg), or set a special directory (-userdir option) so + that your Engine will use a different set of configs

    +
  • DPVideo directory: here you have to specify the + path where your Nexuiz usually creates new .avi or .ogv files. This + is one of the items where you will see that it is required that you + already have recorded demos previously. Usually the path will be + Nexuiz/data/video (on Windows) or ~/.nexuiz/data/video

    +
  • Relative demo path: This is the path that is being + used within the virtual file-system of Nexuiz in order to find your + demo. Normally, when starting a demo by hand, you would enter + something like "playdemo demos/stormkeep_demo.dem", + because usually demos are stored in the directory "demos". + In case you changed that (e.g. If the demo is in a sub-directory of + the demos directory), make sure to put this into the field. But + usually you won't have to change the value (demos) to + anything else. Just leave it as it is.

    +
  • Job name: Here you can specify a name for this job. If + you don't specify any name, a name will be automatically chosen, using + the format "Job x", where x is a index number.

    +
  • Demo file: obviously you have to specify the demo + file here

    +
  • Start second and end second are self-explanatory

    +
  • Exec before capture: Here you have the chance to + enter console commands (that is, commands that you could otherwise + also enter into the console of the game). One example could be to + execute a config here that will make sure that the HUD is hidden. Or + you might want to change the FPS for the rendered video by setting + cl_capturevideo_fps to another value. If you want to put several + commands in here you can either separate them by a new-line, or by ; + (semi-colon)

    +
  • Exec after capture: works like the "exec + before capture" field. Can be used for anything, e.g. Restoring + settings that you altered in the exec before field.

    +
  • Video destination: Here you have to specify where + you want the recorded video file to be moved once the recording + process is done. Please note that you have to enter a name for the + file, however you should not enter the extension (.avi or .ogv). You + don't have to, because the Nexuiz demo recorder will automatically + figure out whether the Nexuiz engine generated an avi or ogv file, + and will keep its ending when moving the file to its destination.

    +
+

Once you filled out all fields, click on create. In +case you did something wrong with one of the fields, you will +hopefully get an error message immediately, which allows you to +correct the mistake. If nothing was filled out incorrectly the dialog +should close and you should now see a "Job 1" in the job +queue.

+

In order to edit a job, either double-click on it, or +right-click it and click on Edit

+

Step 3

+

Now that you have setup your first job, click on the +Start processing button. After a short moment you should see +Nexuiz opening. Don't be surprised if the "Loading" image +appears for a long time. What you don't see is that Nexuiz is +fast-forwarding your demo to the start time you specified, which can +take some time. After that, once Nexuiz started and finished +recording the stuff you wanted it to record, it should close +automatically. The demo recorder should show you a new message dialog +that it finished recording all jobs. The "Status" column of +your first job should now be showing "done".

+

Trouble-shooting:

+

In case something went wrong, the status column of your job will +show "error" (in the jobs table). In order to find out what the problem was, +right-click on the job and click on "Show error message". +The message dialog that pops up will hopefully help you to figure out +what went wrong. If it is something that you think you can fix, do so +(by editing the job settings), then right-click the job again and click on +"Reset job status to waiting". This will set the status of the job to +"waiting" again.

+

Job queue processing

+

You need to be aware that once you click on the Start +processing button only the jobs with status "waiting" will be +put into a queue and will be executed one after another. All other jobs +(with status "error" or "done" will not be started. +If an error occurs while processing one of the jobs this job will set its state to +"error" and the next job in queue will be executed. That is, the behavior +of the Nexuiz demo recorder is to continue working even if one or more individual +jobs failed.

+

In case you already have a job list with several +jobs but you want to just record one particular job (even though all +other jobs also have the state "waiting"), just right-click +on the particular job and click on "Start job". Note that this only +works if the Nexuiz demo recorder is currently not working on any other jobs.

+

Further reading

+

Congratulations, you managed your +first steps using this program. You should read the other help +chapters as well in order to be able to fully utilize the +functionality of the program.

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/changelog.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/changelog.html new file mode 100644 index 000000000..2c51ff167 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/changelog.html @@ -0,0 +1,37 @@ + + + + + + + + + + +

Changelog

+

v0.1 -> v0.2

+

Compatibility changes

+
    +
  • Table preferences, job queues and template lists are not compatible + between v0.1 and 0.2! Make sure you install v0.2 in a new direcory!

    +
  • NDR v0.2 requires Java SE 6 now (instead of Java SE 5 which was + required for v0.1)

    +
+

Functionality changes and fixes

+
    +
  • Plugin architecture that allows other Java developers to include + their own plug-ins that can encode the recorded job to + another format (e.g. H.264)

    +
  • Ships with encoder plug-in for VirtualDub (MS Windows, + www.virtualdub.org)

    +
  • Duplicates can now be created with more than one job selected, + duplicating all selected ones at once

    +
  • Templates can now be duplicated

    +
  • Fixed bug where deleting selected rows in the tables caused + a weird exception

    +
  • Jobs can be given individual names (don't have to be "Job 1", + "Job 2", ... anymore)

    +
+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/compat-limitations.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/compat-limitations.html new file mode 100644 index 000000000..8a27e16b0 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/compat-limitations.html @@ -0,0 +1,55 @@ + + + + + + + + + + +

Compatibility and limitations

+



+

+

Compatibility

+

Operating system

+

Since this tool is written in Java +you can use it on Windows, Linux and Mac OS. This means that it can +be used on all platforms supported by Nexuiz. You do need Java 6.

+

Nexuiz engine version

+

In order to use this tool you will +need to use a Nexuiz engine of version 2.5 or newer. The reason is +that the engine needs to have the variables +cl_capturevideo_nameformat and cl_capturevideo_number. If you use +older engines, such as the 2.3 engine, recording will work, but the +tool will be unable to move the avi clip rendered by the engine to +your desired location. However, you can manually move it and simply +ignore the fact that the job finished with an “error”.

+

Demo file version

+

There is not really any limitation +about the Nexuiz version that was used when recording the demo. That +means that you should be able to use the Nexuiz demo recorder with +demos from previous Nexuiz versions (they can be older than 2.5). You +just have to make sure that the engine binary you use for recording +them is v2.5 and newer. Keep in mind that using a newer-generation +engine such as the 2.5 one for older demos should usually work +without problems. You might have to move the dataXXXXXX.pk3 files +from the Nexuiz version that corresponds to the demo version into the +data directory.

+

Nexuiz Demo Recorder job queues

+

Table preferences, job queues and template lists are not compatible +between v0.1 and newer versions! Make sure you install this in a different +direcory than v0.1!

+

Limitations

+

This tool only works with 1-map demos +(such as the one being automatically recorded by Nexuiz when using +the cl_autodemo feature). If you manually recorded a demo that +contains several maps, you can only specify a start- and endtime for +the first map. However, you can try to use the demosplit.pl script +from svn (trunk/misc/tools/demosplit.pl) to split demos having +several maps into several 1-map demos. However, note that this tool +will probably only work properly for older demos (up to Nexuiz v2.4.2 +demos).

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/credits.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/credits.html new file mode 100644 index 000000000..4baabcd3a --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/credits.html @@ -0,0 +1,27 @@ + + + + + + + + + + +

Credits

+

Credits to the the +Crystal project (http://www.everaldo.com/crystal/) +for the icons. Thanks to the development teams of +JavaHelp +(https://javahelp.dev.java.net/, +SwingX (https://swingx.dev.java.net/) +and MigLayout (http://www.miglayout.com/). +Thanks to the JHelpDev team +(http://jhelpdev.sourceforge.net/) for developing the +tool that allowed me to create this documentation without much hassle. +Thanks to divverent (div0) for the demotc perl script. Thanks to merlijn for +some suggestions and hints. Thanks to Dave who offered some "useless" patches :P. +Thank you for using the Nexuiz Demo Recorder.

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/faq.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/faq.html new file mode 100644 index 000000000..7e8a6eda6 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/faq.html @@ -0,0 +1,100 @@ + + + + + + + + + + +

FAQ

+

Question: Is it possible to have a +job being recorded with one of the Nexuiz presets (Low, medium, +normal, high, ultra), without affecting my Nexuiz installation in +general (so that this configuration is just active during capturing +the current demo?

+

Yes it is. In Nexuiz there are config +files for this, which are called:

+
    +
  • effects-low.cfg

    +
  • effects-med.cfg

    +
  • effects-normal.cfg

    +
  • effects-high.cfg

    +
  • effects-ultra.cfg

    +
  • effects-ultimate.cfg

    +
  • effects-omg.cfg

    +
+

In order to use them, there are 2 +approaches

+

Approach 1: You should +consider to use a new directory for each new set of effect configs +you want to try out. This will simply help you to avoid loss of your +original config, or any unwanted modification to it. For example, +setup Nexuiz manually by starting it with some userdir, e.g. -userdir +effectTestUltimate, start Nexuiz, go into the menu and set the +effects to ultimate (exec effects-ultimate.cfg in the +console). Maybe fine-tune settings as you like. Then put the -userdir +effectTestUltimate directive into the engine parameters +field of the job.

+

Approach 2: For this approach it is +recommended that you do a backup of your config somewhere else, even +though everything should work fine and your original config should +not be modified at all after the demo was recorded.

+

Add these 4 lines to the exec before +capture field:

+

saveconfig backup.cfg

+

cvar_resettodefaults_all

+

exec effects-ultimate.cfg

+

r_restart

+

The first line will backup your +current config to “backup.cfg”. The second line resets +all variables (you can remove this if you want, but sometimes these +effect-xyz.cfg config files will not set every single variable to the +new value, but assume that some values have their default value, +which might not be the case in your config. So resetting everything +to defaults is recommended. Of course you can choose to use another +effects config than effects-ultimate.cfg. After executing that +effects config a restart of the renderer (r_restart) is +mandatory, because of some of the changes of the effects config not +being applied without a renderer-restart. Also, keep in mind that the +cvar_resettodefaults_all will have changed your Nexuiz screen +resolution (vid_height, vid_width). The r_restart command however +won't take these changes into account (it will still use your old resolution), +only vid_restart would. +This just means that you do not have to change these 2 variables +manually to “keep” your old screen resolution, as long as +you don't use vid_restart.

+

Also add this to your exec after +capture field:

+

cvar_resettodefaults_all

+

exec backup.cfg

+

Please note: in case you want to +setup other special things, e.g. whether to record in the AVI instead +of OGV format, the video FPS, etc, you'd have to put these settings +some place after the cvar_resettodefaults_all command +in the exec before capture field.

+



+

+

Question: Is it possible to record +a job in a particular screen resolution?

+

Yes it is. The height and width are +configured by setting the value vid_height and vid_width to your +desired value. For example to record a demo in the HD resolution, +which is 1280x720, put this into the exec before capture +field:

+

vid_width 1280

+

vid_height 720

+

vid_restart

+



+

+

Question: I have found a bug, or I +encountered some other problem, what do I do? +

+

Well, truth to be told, I cannot +guarantee for any support of this tool. You can put questions or +comments into the forum thread of the AlienTrap forums where you +downloaded the tool from.

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/create_job.gif b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/create_job.gif new file mode 100644 index 0000000000000000000000000000000000000000..e393d50dbfded70f78f7f3f29ba4efff497a7be9 GIT binary patch literal 9994 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 oY+A5AuSXr*7kLSxOI~m}__5c6? literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/customize_tables.gif b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/customize_tables.gif new file mode 100644 index 0000000000000000000000000000000000000000..f9e67b0980f64a763fafa2942878fe00a73f6981 GIT binary patch literal 18632 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 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/edit_job_vdub.gif b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/edit_job_vdub.gif new file mode 100644 index 0000000000000000000000000000000000000000..80fa7e98797d73a29ffb7a15658b352669b09401 GIT binary patch literal 3800 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 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/preferences_global_vdub.gif b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/images/preferences_global_vdub.gif new file mode 100644 index 0000000000000000000000000000000000000000..287676eeffba83f46494ec53d5b72713062d4f0a GIT binary patch literal 9368 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 SPy|g-1zk`EZO{Y_2mm{C + + + + + + + + + +

Introduction

+

Welcome to the Nexuiz demo recorder. This tool is made +for people who are already familiar with the manual demo +recording process and are looking for a way to automate this process. +In simple words, this tool allows you to create jobs that are being +put into a job queue, so that they can be executed one after another, +at a point in time when you don't need to access your computer +anymore. For each job you have to specify the demo you want to +record, the second when the record is supposed to start and end, as +well as the location where to save the final video file to. You can +also use one or more of the available plug-ins to encode the resulting +video file to another format.

+

To get started, take a look at the basic tutorial

+

Click here to learn about the new features of +this release.

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/license.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/license.html new file mode 100644 index 000000000..fbab77d12 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/license.html @@ -0,0 +1,15 @@ + + + + + + + + + + +

License

+

This tool is released under the GPL v2 (same license as Nexuiz).

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/open-save.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/open-save.html new file mode 100644 index 000000000..10e80a962 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/open-save.html @@ -0,0 +1,29 @@ + + + + + + + + + + +

Open/Save Jobs and Templates

+

First of all, all your templates and +jobs will automatically be saved when closing the program (and +automatically loaded when starting it again). You can find these +files in the sub-directory “settings”of the program, the +files are called templates.dat and jobs.dat. Please +note that the job queue will also be saved each time a job finished +its execution (so that you don't lose the progress of your jobs in +case your computer crashes while processing).

+

You have the possibility to manually +save and load other job queues (this might be handy if you are +working on different projects). For this simply use the File menu +and click on Load job queue or Save job queue +respectively. Please be aware that Load job queue will clear +your current job queue and it cannot be recovered, so you better save +your original job queue before loading a new one.

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/plugin-architecture.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/plugin-architecture.html new file mode 100644 index 000000000..35f950716 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/plugin-architecture.html @@ -0,0 +1,81 @@ + + + + + + + + + + +

Plug-in architecture

+

Introduction

+

While the Nexuiz Demo Recorder (NDR) +saves you a lot of time, what many video editors still need to do is +to transcode/convert/encode/you-name-it the resulting huge avi clip +to another (lossless) format. Or, if you are not an editor but just a +player who wants to distribute the recorded clip, unless you had +Nexuiz encode the clip in the Ogg Theora codec (cl_capturevideo_ogg +1), you might want to encode it in XviD, H.264 or some other lossy +codec. For encoding people use all different kinds of tools. Some +have a GUI, some don't. Some are available on just one platform, some +are cross-platform to some extent. However, since this release the +NDR is now able to also encode clips to whatever format you like by +the use of encoder plug-ins. For this the NDR has been +extended with a plug-in architecture that will look for plug-ins in +the “plugins” folder. NDR plug-ins are basically just a +bridge between the NDR application and an actual encoder. Examples +for actual encoders are VirtualDub for MS Windows, or mencoder +for Linux. What a plug-in will do is to assemble a command line that +will execute this encoder and hand it the required parameters.

+

Extension is easy

+

In this release the NDR ships with +just one plug-in: the VirtualDub plug-in. However, if you are a Java +developer it is really easy for you to write your own plug-ins that +will be able to handle whatever encoder you want to use. If you are +interested, have a look into the src folder at the Sample +Plugin. All you need to do is to implement an interface, edit +some meta-information files and build a jar file that is being put +into the plugins directory of the NDR.

+

Plug-in settings

+

Each plug-in allows the user to +interact with it, by offering settings (or “preferences”). +There are 2 kinds of settings:

+
    +
  • Global settings, which will be + shown in the File → Preferences dialog

    +
  • Job/template-specific settings, + which will be shown in the dialog you use when creating or editing + jobs or templates

    +
+

Use plug-ins later on

+

In order to invoke the functionality +of the plug-in you are not required to set up everything at the very +beginning (before the clip has actually been recorded). Of course, +you can do so, and in this case the job would be recorded and, right +after that, be encoded as well. But you can also choose to have jobs +recorded first, and then later when you see the need to encode them, +edit the job settings (add your plug-in settings there), and then +right-click the job and select “just run the xyz plug-in”. +Basically this menu item will work on any job with status = done.

+

Of course you may not move the +recorded clip from the destination that you set up, because otherwise +the plug-in would not find the file to encode it.

+

Error handling

+

In case something went wrong during +the execution of the plug-in itself, you will see a “plug-in +error” in the status column of the jobs table. Like for +normal “errors”, you can right-click the job and click on +“show error message” in order to see the error message of +the plug-in. In case you can repair whatever went wrong, do so, then +right-click the job and click on “reset job status to done”. +Then you can run the plug-in again.

+

If a plug-in appeared to do its job +just fine but you suspect that something went wrong, you might want +to have a look at the logs directory in the NDR folder. +Depending on whether the one who created the plug-in implemented it, +you might see log files which are basically text files containing the +output of the encoder itself.

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/plugin-virtualdub.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/plugin-virtualdub.html new file mode 100644 index 000000000..9824ee7ab --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/plugin-virtualdub.html @@ -0,0 +1,163 @@ + + + + + + + + + + +

VirtualDub plug-in

+

Introduction

+

Important: Make sure that +you read this documentation carefully in order to avoid mistakes!

+

The prerequisite for using this +plug-in is that you already familiar with the “manual” +process of encoding clips with VirtualDub (www.virtualdub.org). +The great thing about VirtualDub is that it offers both a graphical +user interface (GUI), accessible via virtualdub.exe, and a +command line interface (CLI), accessible via vdub.exe.

+

Step 1: create VDub configuration +files (VCF)

+

Let's say we had a clip that we +wanted to encode not only once, but twice – into different +formats. Let's say that the first goal is to encode it to a loss-less +Huffyuv clip, and the second goal is to encode it to a quality-based +1-pass XviD clip using MP3 sound.

+

What you need to do first is to open +VirtualDub, set up the settings for one of the goals (by going into +the video compression and audio compression dialogs) and then click +on File → Save processing settings (CTRL+S). You will end up +with saving a .vcf file somewhere on your hard drive … +remember where you put it! After that, you'd set up the settings for +your second goal and save these processing settings as well, as a new +.vcf file.

+

Step 2: Enable and set up the VDub +plug-in

+

First we need to set up the global +settings of the plug-in. Go into the preferences dialog and enable +the plug-in.

+

+

Use the “...” button to +specify the path to the vdub.exe file. The setting max number of +VCFs per job is important, because the number you put in there +will influence how many VCF files you will be able to select in the +job dialog (the dialog where you create or edit a +job/template). For our example we need to set it to 2, but it doesn't +matter if you choose a higher value. Since VDub will encode two +files for us, we will have to tell VDub under which filenames to save +them. You can select which approach you want to use by configuring +the setting output as suffix(0) or file(1). If you don't +check the checkbox this means that you will enter a small part of the +file name, the suffix, that will be put at the end of the +original file name. E.g. if your original filename was +“C:\render\myVideoFile.avi”, and the suffix is +“_huffyuv”, the resulting file would be +“C:\render\myVideoFile_huffyuv.avi”. However, if you +checked that checkbox, you would instead get a “...” +button that allows you to specify the filename for the resulting file +manually (this is handy if you wanted to save the file to another +folder for example).

+

The show extra options setting +will be explained at the end of this page.

+

Let's now have a look at the dialog +when editing a job

+

Step 3: Set up the job-specific +settings

+

Open the job dialog.

+

/P> +

Let's ignore the 2 checkboxes at the +top for a moment. What you will need to do is to specify the path to +the 2 .vcf files you created previously in VirtualDub in step 1. Then +you need to specify the suffix (or the file using the file-chooser) +so that VDub knows where to save the encoded videos.

+

Step 4: Run the plug-in

+
    +
  • If you had your job recorded + (without using the plug-in) then the status of the job will already + be “done” and you can right-click the job and run + the VirtualDub plug-in

    +
  • If the job has not been + recorded yet, just click on Start processing and wait for the result

    +
+

Additional information

+

Empty VCF paths

+

As you know you can specify any +number in the preferences dialog for the setting max number +of VCFs per job. In case of our example that consists of 2 jobs, +if you selected 3 (or more) as max. number, and then opened the job +dialog, you would see 3 fields each for the path to VCF file +and output file setting. This, however, is not a problem. The +plug-in will only execute those VCF jobs for which both the path to +the VCF and the output file fields are filled with proper +information.

+

VDub job control

+

You still don't know what the 2 +checkboxes in the job dialog at the top of the VDub settings +mean. What you need to know is that VirtualDub is having its own “job +queue”, which, however, is called “job control”.

+

The following scenarios can make +sense:

+
    +
  • Having both checkboxes + enabled: Each time a new NDR job is executed which has + VirtualDub jobs, the job control of VirtualDub will be + cleared. This means that if you had other, old things in there + (possibly when working with VirtualDub outside of the NDR) that + these will be deleted from VirtualDub's job control, before the new + jobs are added there. Checking the second checkbox means that after + adding one of the VirtualDub jobs from within the the NDR, + VirtualDub will actually encode them right away (which you will + want, at least under normal circumstances).

    +
  • Having both checkboxes + disabled: In this case the only thing the NDR would do would + be to add the two jobs to the job control of VirtualDub, but + VirtualDub won't be encoding them. They are just put into the queue. + This means that if you were to start the the graphical user + interface of VirtualDub after the NDR completed processing, and open + the Job Control (F4), you'd see these jobs “waiting”, + and you could render them by pressing the Start button in Virtual + Dub.

    +
+

The reason why these checkboxes exist +is flexibility. I often want the NDR to record stuff over night, +because during the recording process my PC is virtually unusable. +However, encoding clips in VirtualDub is a task that, depending on +the codec, can be done in the background and allows me to use the PC +for other things. In this case I'd disable both checkboxes, have the +NDR record all clips and add them to VirtualDub's job control, and +then on the next day, I decide when and which jobs to encode into +different formats from within the VirtualDub GUI.

+

And btw, just to be clear, the second +checkbox means that, when enabled, VirtualDub would actually +start encoding all clips that have already been in +VirtualDub's job control and that have not been encoded yet.

+

Extra options

+

If you selected the show extra +options checkbox in the preferences dialog, you will +notice that, in the job dialog, you got 2 additional +settings/checkboxes for each VCF file. Their names are pretty much +self-explanatory. I added these checkboxes because, after a scene was +rendered, I usually have the clip encoded into a loss-less format +(e.g. Huffyuv) right away. Since it is no problem to encode further +things (e.g. compressed XviD movies) based on the Huffyuv clip +(instead of always using the original avi file), enabling both these +checkboxes for VCF 1 would mean that the 2nd (3rd, +4th …) VCF jobs will be based on the +huffyuv-encoded file, and I got rid of original, big video as well, +saving hard disk space.

+

Trouble shooting

+

In case you suspect that the +expected, encoded file(s) is somehow incorrect (or even missing), +have a look at the logs directory in the NDR folder. You will +see log-files with the following format:

+

VirtualDub_NameOfTheJob_vcf1.log

+

Instead of “vcf1” you +might also find “vcf2”, “vcf3”... it +specifies for which VCF file of that job the log file stands for.

+



+

+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/preferences.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/preferences.html new file mode 100644 index 000000000..00a9c1228 --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/preferences.html @@ -0,0 +1,64 @@ + + + + + + + + + + +

Preferences dialog

+

The preferences dialog allows you to +change the behavior of the program in certain situaitons.

+

+
    +
  • Overwrite final video + destination file if it exists: This checkbox allows you to determine + whether the final destination video file (e.g. + C:\render\fragOfTheMonth.avi) should be overwritten by a job + if that file already existed from your harddrive. By default this + option is disabled. The program will, in this case, not overwrite + any existing files, but save the video file with a “copyX” + suffix, for example C:\render\fragOfTheMonth_copy1.avi

    +
  • Disable rendering while + fast-forwarding: As the Nexuiz demo recorder creates a cutted demo + that has slowmo-commands in it to fast-forward it to the desired + start time, the fast-forwarding process is often slowed down due to + the action taking place on your screen (and your graphic card + choking with the mere speed of the demo, e.g. during a “slowmo + 100” period). However, there is a variable called r_render in + the engine which can be set to 0, which will disable any render + updates. This will speed up the fast-forwarding process massively. + Of course, r_render is being set to 1 a few seconds before start + time is reached.

    +
  • Disable sound while + fast-forwarding: The value of the variable volume is saved and the + set to 0 right after the demo was loaded. The reason is that it can + be annoying to hear the in-game sounds while fast-forwarding.

    +
  • Fast-forward speed (first stage + or second stage): The demo is fast-forwarded using two different + fast-forward speeds. In the first stage, when the start time is + still about a minute away, the “first stage” value is + used. Then, until 5 seconds before the start time, the “second + stage” value is used. You will only need to manipulate these + values if you discover that the final recorded video is inaccurate + when it comes to the time when the recording starts (e.g. when then + video recording starts too late, reduce these 2 values for first and + second stage a bit – you will have to experiment)

    +
  • Do not delete cut demos: The + file name of the automatically created cut demo is like the original + name of your demo, but ends with “_autocut.dem”. + Normally this demo is created, being recorded from, and then deleted + again. If you enable this option the demo file is not deleted, and + you can inspect it with a Hex editor or whatever else you want to do + with it (maybe send it to a friend to have him record it?)

    +
  • Append this suffix to job-name when duplicating jobs: + As you now have customizable job names it would probably be a bad idea to + give duplicated jobs the same name as the original jobs. This is why you can + set up a suffix that will be appended to the original job name.

    + +
+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/templates.html b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/templates.html new file mode 100644 index 000000000..7385cc69c --- /dev/null +++ b/misc/tools/NexuizDemoRecorder/src/main/resources/help/html/templates.html @@ -0,0 +1,69 @@ + + + + + + + + + + +

Templates

+

The Nexuiz demo recorder offers you +templates that you can use to create new jobs (which are based on a +template) more quickly and efficiently. Imagine these "templates" +didn't exist ... you would always have to specify the complete path +to the engine, the video file, the demo file, and the final video +file – each single time when creating a job, from scratch. This +would take a long time and would be inefficient.

+

Instead you are encouraged to create +templates. Templates can be created either from scratch (click on the ++ Create button next to the template table) or from an +existing job (select the job, then click on the button Create from +job).

+

The latter will just add a new entry +in the templates table, without prompting you for any further +information. The template's name and summary will be "Generated +from job", and you can change this by double-clicking this +generated template, renaming it and giving it a meaningful summary, +then click save. You will notice that all other values have been +taken from the job and don't need to be filled out by you anymore.

+

Here you can see the template dialog +when creating a new template:

+

+

The dialog looks very much alike the +"create job" dialog presented to you in the Basic +tutorial. There are a few differences, however:

+
    +
  • The dialog shows you a template + name and description

    +
  • Instead of specifying a demo + file you are now specifying a demo directory

    +
  • Instead of specifying a video + destination file, you now just specify the directory

    +
  • You + don't specify a start time or end time for templates, + because these 2 input parameters are specific for each job and don't + make sense to be saved for a template

    +
+

Once you have a template you can +create new jobs easily by selecting a template in the templates table +and then click on the Create from template button in the jobs +panel. +

+

What could templates also be good +for?

+
    +
  • Using different configs for + recording (you can set a config to be used either in the “engine + parameters” field, e.g. +exec myconfig.cfg, or you + could put the exec myconfig.cfg into the “exec before” + field.

    +
  • Recording with different Nexuiz + engines to compare the visual quality

    +
  • Inspire me … what do you + use templates for? Post it to the forum!

    +
+ + + diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/advanced.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/advanced.png new file mode 100644 index 0000000000000000000000000000000000000000..34458516ba0c93ea9b5f183d2ecdd1c01b916726 GIT binary patch literal 838 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 N002ovPDHLkV1ldnC=>ty literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/edit_add.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/edit_add.png new file mode 100644 index 0000000000000000000000000000000000000000..5b051f647fecd43ea4ed665486734acb38eaa952 GIT binary patch literal 3329 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 LNkvXXu0mjf4`n&o literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/editclear.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/editclear.png new file mode 100644 index 0000000000000000000000000000000000000000..4cb04d0072d8034c8041735f642453e9538e2514 GIT binary patch literal 3054 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 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/editcopy.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/editcopy.png new file mode 100644 index 0000000000000000000000000000000000000000..a33296172963c445475f5c0fbde6fbe9b74ef213 GIT binary patch literal 3356 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 JPDHLkV1khOih2M5 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/filesave.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/filesave.png new file mode 100644 index 0000000000000000000000000000000000000000..9c020ad7aca7de392690e65db349ce45d0a2b4bb GIT binary patch literal 3319 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 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/info.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/info.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fad561901197400298c08aa18bdde65441a9c7 GIT binary patch literal 3632 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 Dz~!Oy literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/player_pause.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/player_pause.png new file mode 100644 index 0000000000000000000000000000000000000000..b61fb875916bec7f131c36ade4d258584deabb98 GIT binary patch literal 3661 zcmV-T4zlryP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}000AMNklUT5S{~Di9Ee$fL{qxx?&h*qPax z@lxY=|Gs=mH1-zNYKYO&LE-oir*x$Ck6pj2i^{MOjh{gXsaP(pR>GQ@g-1V3>WRim zJXMdK%YhImJfZ4%sCxA3bFaSk;qBK)ZVnFgsL6x?pjZl-pI`UC>PbI%cz^1%s_Xsq zpC_x@AYR2I+0=UL-A~^5@}2ju4b`Voa-k@&EU@z6xPq2UN*aHDptrU?^UB=R>V|z{ z{w2vWqNe6eLP;Hc`_qr^jg3FkXzoGLG6}1v=!r%^qGatN$tp&=nT9s_@%v@#i#vB; z#&;jdigZ>Vnz(UyXng$Q+LFW8?g=lPuX3eNV`)>uwjFL?323TS_<7!8XHO=TY0^uj zO2hv0$~`5SZobeoF!bW=)C#`iVHZt0ZuQfZNpbpc3)^<+?d+x@?{c)aOunL{Zze9peW?9dzuc7HWft|;x^Z>BCvxj}^0^QgZmyL}qCuIc!mq;otB8D-n z$cb3z&gvZby+a~KE0SvwggyX;LV?qY3kZDQ$1+biDk+3j2h|G@$#}PdOjyo7ps+HF zNDrfGH3a?y0G64<@jO5f1U~u04b0;bVR-|+HcC9!hf?*$pYy!r;k4 zZX4J200?{!dw&t{@EF`3l-dzOIWKVaiUQj08NWR=mJoqYoH`R z^AVa%y!}#GErv5v*|w%b)cXyCH=wE!$Z4h*_gGrq;=uAaEWqO5oBT1Cr5qR#Mo9Kt zMu|0iQ*lbai$wp&sI|$F_R;HKXMg#vHJ@E4-DZ$Z#WC#&A%gmthm|jIuw5XQuBUZ$ zBxkP7z2p^&ld|44h9CO7Qd0e=`Rv7Oidq{ln5QJ_GW7R7%TsNglwFOJe3fL!8Jb4U zn@;8EJ-1l=PKiYk$$>kdYb1Kw@l|mt-q7_yi_tjI*W4KGZmvU>C9($v7PhzDY;ONs z+uHwJaf3u-a`uUI%-{Y|z00000NkvXXu0mjfYY6X0 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/player_play.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/player_play.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ef5ef946d105e7d30d836b5856556a17df329c GIT binary patch literal 3638 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 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/quick_restart_blue.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/quick_restart_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..fc08cd7903954c67350c4a0f76b065e477fcb41b GIT binary patch literal 3509 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 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/status_unknown.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/status_unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..4d158de59d1c01bedaf57ff1f8dedcbd7ef2d1c2 GIT binary patch literal 3406 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 literal 0 HcmV?d00001 diff --git a/misc/tools/NexuizDemoRecorder/src/main/resources/icons/view_right_p.png b/misc/tools/NexuizDemoRecorder/src/main/resources/icons/view_right_p.png new file mode 100644 index 0000000000000000000000000000000000000000..1303df4fd135c185679bdbf658186c1ef6f0d6c3 GIT binary patch literal 3346 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 + +registry +javahome +jrepath +jdkpath +exepath +jview + +..\..\..\..\..\Nexuiz Demo Recorder\Build v0.2\NexuizDemoRecorder-0.2.jar +${EXECUTABLEPATH} +false +..\..\..\..\..\Nexuiz Demo Recorder\Build v0.2\NexuizDemoRecorder.exe +..\..\..\..\..\..\temp\128x128\filesystems\folder_video.png +-1 +com.nexuiz.demorecorder.main.Driver +-1 + +1.6 +Windowed Wrapper + +Message +Java has not been found on your computer. Do you want to download it? + + +URL +http://www.java.com + + +SingleProcess +1 + + +SingleInstance +1 + + +JniSmooth +0 + + +Debug +0 + + -- 2.39.2