Added a flexible mechanism for persistent boolean-, integer-, and
string-valued preferences built atop java.util.Properties. How it works: the jvm is asked first, and then the user's prefs file, if it exists, then the system-wide prefs file, and then the built-in preferences. Finally, for robustness, a default may be optionally hard-coded in the source. I made several things configurable, too: the default Tibetan keyboard the default font sizes and faces whether you want developer-only features enabled Savant's file extension (.savant) etc. The only known problems are the following: The default location for the user's preferences file is windows-specific, arbitrary, and not in the user documentation. Likewise for the location of the system-wide preferences file. You can change them using 'java -D', though. There is no "Save preferences" option yet, and closing the program does not save preferences either.
This commit is contained in:
parent
b914309dba
commit
08e4e2fc57
8 changed files with 591 additions and 101 deletions
46
build.xml
46
build.xml
|
@ -57,13 +57,12 @@ info on where to find these.
|
|||
<property name="privatejavadocs" location="${docs}/private-javadocs"/>
|
||||
<property name="j2ee.sdk.home" location="F:\Program Files\j2sdkee1.3.1\lib"/>
|
||||
|
||||
<!-- Set to false for less output when running the tools: -->
|
||||
<!-- Set to true for more output when running the tools and if you
|
||||
want the code to abort when it reaches poorly thought-out
|
||||
control flow paths (i.e., iffy code).
|
||||
-->
|
||||
<property name="thdl.debug" value="true"/>
|
||||
|
||||
<!-- Set to true if you want the code to abort when it reaches
|
||||
poorly thought-out control flow paths (i.e., iffy code). -->
|
||||
<property name="thdl.die.on.iffy" value="true"/>
|
||||
|
||||
<!-- Set to the name of the dictionary used by the Translation Tool: -->
|
||||
<property name="arch.dict" value="architecture_dictionary"/>
|
||||
|
||||
|
@ -160,7 +159,7 @@ info on where to find these.
|
|||
<antcall target="copy-ini-files-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${jskadbin}"/>
|
||||
</antcall>
|
||||
<antcall target="copy-license-to-bin-dir-for-jarring">
|
||||
<antcall target="copy-license-etc-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${jskadbin}"/>
|
||||
</antcall>
|
||||
<copy todir="${jskadbin}/org/thdl/tib/input">
|
||||
|
@ -191,7 +190,7 @@ info on where to find these.
|
|||
<antcall target="copy-ini-files-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${savantbin}"/>
|
||||
</antcall>
|
||||
<antcall target="copy-license-to-bin-dir-for-jarring">
|
||||
<antcall target="copy-license-etc-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${savantbin}"/>
|
||||
</antcall>
|
||||
</target>
|
||||
|
@ -213,7 +212,7 @@ info on where to find these.
|
|||
<antcall target="copy-ini-files-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${ttstandalonebin}"/>
|
||||
</antcall>
|
||||
<antcall target="copy-license-to-bin-dir-for-jarring">
|
||||
<antcall target="copy-license-etc-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${ttstandalonebin}"/>
|
||||
</antcall>
|
||||
</target>
|
||||
|
@ -230,7 +229,7 @@ info on where to find these.
|
|||
<param name="my.included.source.file"
|
||||
value="org/thdl/tib/scanner/WindowScannerFilter.java"/>
|
||||
</antcall>
|
||||
<antcall target="copy-license-to-bin-dir-for-jarring">
|
||||
<antcall target="copy-license-etc-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${tthandheldbin}"/>
|
||||
</antcall>
|
||||
</target>
|
||||
|
@ -252,7 +251,7 @@ info on where to find these.
|
|||
<antcall target="copy-ini-files-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${ttappletjwsbin}"/>
|
||||
</antcall>
|
||||
<antcall target="copy-license-to-bin-dir-for-jarring">
|
||||
<antcall target="copy-license-etc-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${ttappletjwsbin}"/>
|
||||
</antcall>
|
||||
</target>
|
||||
|
@ -265,7 +264,7 @@ info on where to find these.
|
|||
<param name="my.included.source.file"
|
||||
value="org/thdl/tib/scanner/ConsoleScannerFilter.java"/>
|
||||
</antcall>
|
||||
<antcall target="copy-license-to-bin-dir-for-jarring">
|
||||
<antcall target="copy-license-etc-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${ttbin}"/>
|
||||
</antcall>
|
||||
</target>
|
||||
|
@ -292,7 +291,7 @@ info on where to find these.
|
|||
<antcall target="copy-ini-files-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${qdbin}"/>
|
||||
</antcall>
|
||||
<antcall target="copy-license-to-bin-dir-for-jarring">
|
||||
<antcall target="copy-license-etc-to-bin-dir-for-jarring">
|
||||
<param name="mybin" value="${qdbin}"/>
|
||||
</antcall>
|
||||
</target>
|
||||
|
@ -538,8 +537,7 @@ info on where to find these.
|
|||
<pathelement location="${dist}/lib/QuillDriver-${my.jar.suffix}.jar"/>
|
||||
<path refid="entire.class.path"/>
|
||||
</classpath>
|
||||
<jvmarg value="-DTHDL_DEBUG=${thdl.debug}"/>
|
||||
<jvmarg value="-DTHDL_DIE_ON_IFFY_CODE=${thdl.die.on.iffy}"/>
|
||||
<jvmarg value="-Dthdl.debug=${thdl.debug}"/>
|
||||
</java>
|
||||
</target>
|
||||
|
||||
|
@ -549,8 +547,7 @@ info on where to find these.
|
|||
<pathelement location="${dist}/lib/Savant-${my.jar.suffix}.jar"/>
|
||||
<path refid="entire.class.path"/>
|
||||
</classpath>
|
||||
<jvmarg value="-DTHDL_DEBUG=${thdl.debug}"/>
|
||||
<jvmarg value="-DTHDL_DIE_ON_IFFY_CODE=${thdl.die.on.iffy}"/>
|
||||
<jvmarg value="-Dthdl.debug=${thdl.debug}"/>
|
||||
</java>
|
||||
</target>
|
||||
|
||||
|
@ -560,8 +557,7 @@ info on where to find these.
|
|||
<pathelement location="${dist}/lib/Jskad-${my.jar.suffix}.jar"/>
|
||||
<path refid="entire.class.path"/>
|
||||
</classpath>
|
||||
<jvmarg value="-DTHDL_DEBUG=${thdl.debug}"/>
|
||||
<jvmarg value="-DTHDL_DIE_ON_IFFY_CODE=${thdl.die.on.iffy}"/>
|
||||
<jvmarg value="-Dthdl.debug=${thdl.debug}"/>
|
||||
</java>
|
||||
</target>
|
||||
|
||||
|
@ -572,8 +568,7 @@ info on where to find these.
|
|||
location="${dist}/lib/DictionarySearchStandalone-${my.jar.suffix}.jar"/>
|
||||
<path refid="entire.class.path"/>
|
||||
</classpath>
|
||||
<jvmarg value="-DTHDL_DEBUG=${thdl.debug}"/>
|
||||
<jvmarg value="-DTHDL_DIE_ON_IFFY_CODE=${thdl.die.on.iffy}"/>
|
||||
<jvmarg value="-Dthdl.debug=${thdl.debug}"/>
|
||||
<!-- For non-swing version: <arg value="-simple"/> -->
|
||||
<arg value="${arch.dict}"/>
|
||||
</java>
|
||||
|
@ -710,12 +705,19 @@ info on where to find these.
|
|||
</target>
|
||||
|
||||
|
||||
<target name="copy-license-to-bin-dir-for-jarring"
|
||||
<target name="copy-license-etc-to-bin-dir-for-jarring"
|
||||
depends="init"
|
||||
description="INTERNAL TASK: Copies the license documents to the bin directory. Usually not called directly.">
|
||||
description="INTERNAL TASK: Copies the license documents and the default properties file (hence the et cetera) to the bin directory. Usually not called directly.">
|
||||
<copy todir="${mybin}">
|
||||
<fileset dir="${license}"/>
|
||||
</copy>
|
||||
|
||||
<!-- The default properties file, shared by all our tools: -->
|
||||
<copy todir="${mybin}">
|
||||
<fileset dir="${source}">
|
||||
<include name="options.txt"/>
|
||||
</fileset>
|
||||
</copy>
|
||||
</target>
|
||||
|
||||
|
||||
|
|
81
source/options.txt
Normal file
81
source/options.txt
Normal file
|
@ -0,0 +1,81 @@
|
|||
# This is the built-in preferences file for Jskad, Savant,
|
||||
# QuillDriver, and the Translation Tool.
|
||||
#
|
||||
# Please do not edit this file. Instead, create a system-wide or
|
||||
# user-specific properties file, defining just the options that you
|
||||
# want to differ from the defaults given here. See the documentation
|
||||
# for how to do that. Or, if you are a developer or if we haven't
|
||||
# written any documentation yet *smile*, look at
|
||||
# 'http://thdltools.sourceforge.net/api/org/thdl/util/ThdlOptions.html'.
|
||||
#
|
||||
#
|
||||
# This file must remain readable by the
|
||||
# java.util.Properties.load(InputStream) mechanism, so don't just edit
|
||||
# it willy-nilly -- instead, read the comments for each item.
|
||||
|
||||
|
||||
|
||||
############################################################################
|
||||
######################### User Preferences ###########################
|
||||
############################################################################
|
||||
|
||||
# Sets the Tibetan keyboard that Jskad uses on startup.
|
||||
# Here are the acceptable values:
|
||||
#
|
||||
# 0: Extended Wylie Keyboard
|
||||
# 1: TCC Keyboard #1
|
||||
# 2: TCC Keyboard #2
|
||||
# 3: Sambhota Keymap One
|
||||
thdl.default.tibetan.keyboard = 0
|
||||
|
||||
# Set this to the default font face for Roman input. No error is
|
||||
# given if this font is not found.
|
||||
thdl.default.roman.font.face = Serif
|
||||
|
||||
# Set this to the default font size for Roman input:
|
||||
thdl.default.roman.font.size = 14
|
||||
|
||||
# Set this to the default font size for Tibetan input:
|
||||
thdl.default.tibetan.font.size = 36
|
||||
|
||||
# The extension that Savant's input files, the titles, must have:
|
||||
thdl.savant.file.extension = .savant
|
||||
|
||||
# Set this to true only if you do not want Jskad to have a status bar.
|
||||
thdl.Jskad.disable.status.bar = false
|
||||
|
||||
# The message displayed in Jskad's status bar when you start it up
|
||||
# with thdl.Jskad.disable.status.bar set to false:
|
||||
thdl.Jskad.initial.status.message = Welcome to Jskad!
|
||||
|
||||
|
||||
############################################################################
|
||||
####################### Developer Preferences ########################
|
||||
############################################################################
|
||||
|
||||
# Set this to true to disable the creation of log files.
|
||||
thdl.disable.log.file = false
|
||||
|
||||
# Set this to true if you want the debugging output, the .log files,
|
||||
# to be placed in your system's temporary directory. You cannot set
|
||||
# this to true AND set thdl.log.directory.
|
||||
thdl.use.temp.file.directory.for.log = false
|
||||
|
||||
# Set this to the directory in which you want the debugging output,
|
||||
# the .log files, placed. You cannot set this to true AND set
|
||||
# thdl.use.temp.file.directory.for.log to true.
|
||||
thdl.log.directory =
|
||||
|
||||
# Set this to true only if you are a developer and want to see what
|
||||
# Jskad is "thinking" as you enter tibetan text. If
|
||||
# thdl.Jskad.disable.status.bar is true, then this will have no
|
||||
# effect.
|
||||
thdl.Jskad.enable.tibetan.mode.status = false
|
||||
|
||||
# Set this to true only if you are a developer and want exceptional
|
||||
# behavior to rear its ugly head as soon as possible:
|
||||
thdl.debug = false
|
||||
|
||||
# Set this to true if you want Savant to treat all files as valid
|
||||
# input. Otherwise, only .savant files are acceptable.
|
||||
thdl.treat.all.files.as.dot.savant.files.regardless.of.extension = false
|
|
@ -24,6 +24,7 @@ import javax.swing.*;
|
|||
import javax.swing.filechooser.*;
|
||||
|
||||
import org.thdl.util.ThdlDebug;
|
||||
import org.thdl.util.ThdlOptions;
|
||||
|
||||
/**
|
||||
* The SavantFileView "sees through" a <code>*.savant</code> file and
|
||||
|
@ -40,7 +41,10 @@ import org.thdl.util.ThdlDebug;
|
|||
public class SavantFileView extends FileView {
|
||||
/** When opening a file, this is the only extension Savant cares
|
||||
about. This is case-insensitive. */
|
||||
public final static String dotSavant = ".savant";
|
||||
public final static String getDotSavant() {
|
||||
return ThdlOptions.getStringOption("thdl.savant.file.extension",
|
||||
".savant");
|
||||
}
|
||||
|
||||
/** This loads <code>*.savant</code> files as properties files and
|
||||
returns an associated TITLE attribute. For any other type of
|
||||
|
@ -56,8 +60,8 @@ public class SavantFileView extends FileView {
|
|||
unresponsive. In addition, you'll cause the floppy drive
|
||||
to spin up every time you refresh or cd in the file
|
||||
chooser. */
|
||||
if (!Boolean.getBoolean("THDL_TREAT_ALL_FILES_AS_DOT_SAVANT_FILES_REGARDLESS_OF_EXTENSION")) { /* FIXME */
|
||||
if (!f.getName().toLowerCase().endsWith(dotSavant))
|
||||
if (!ThdlOptions.getBooleanOption("thdl.treat.all.files.as.dot.savant.files.regardless.of.extension")) { /* FIXME */
|
||||
if (!f.getName().toLowerCase().endsWith(getDotSavant()))
|
||||
return null;
|
||||
}
|
||||
Properties p = new Properties();
|
||||
|
|
|
@ -97,7 +97,7 @@ public class SavantShell extends JFrame
|
|||
if (f.isDirectory()) {
|
||||
return true;
|
||||
}
|
||||
return f.getName().toLowerCase().endsWith(SavantFileView.dotSavant);
|
||||
return f.getName().toLowerCase().endsWith(SavantFileView.getDotSavant());
|
||||
}
|
||||
|
||||
//the description of this filter
|
||||
|
|
|
@ -33,6 +33,7 @@ import java.io.*;
|
|||
import javax.swing.text.rtf.*;
|
||||
|
||||
import org.thdl.util.ThdlDebug;
|
||||
import org.thdl.util.ThdlOptions;
|
||||
import org.thdl.util.StatusBar;
|
||||
|
||||
/**
|
||||
|
@ -284,6 +285,20 @@ public RTFEditorKit rtfEd = null;
|
|||
}
|
||||
*/
|
||||
|
||||
private static int defaultTibFontSize() {
|
||||
// FIXME: at program exit, or when the user selects "Save
|
||||
// preferences", or somehow, save the value the users chooses:
|
||||
return ThdlOptions.getIntegerOption("thdl.default.tibetan.font.size",
|
||||
36);
|
||||
}
|
||||
|
||||
private static int defaultRomanFontSize() {
|
||||
// FIXME: at program exit, or when the user selects "Save
|
||||
// preferences", or somehow, save the value the users chooses:
|
||||
return ThdlOptions.getIntegerOption("thdl.default.roman.font.size",
|
||||
14);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method sets up the editor, assigns fonts and point sizes,
|
||||
* sets the document, the caret, and adds key and focus listeners.
|
||||
|
@ -298,15 +313,16 @@ public RTFEditorKit rtfEd = null;
|
|||
styleContext = new StyleContext();
|
||||
|
||||
doc = new TibetanDocument(styleContext);
|
||||
doc.setTibetanFontSize(36);
|
||||
doc.setTibetanFontSize(defaultTibFontSize());
|
||||
setDocument(doc);
|
||||
|
||||
Style defaultStyle = styleContext.getStyle(StyleContext.DEFAULT_STYLE);
|
||||
StyleConstants.setFontFamily(defaultStyle, "TibetanMachineWeb");
|
||||
StyleConstants.setFontSize(defaultStyle, 36);
|
||||
StyleConstants.setFontSize(defaultStyle, defaultTibFontSize()); // FIXME make pref
|
||||
|
||||
romanFontFamily = "Serif";
|
||||
romanFontSize = 14;
|
||||
romanFontFamily = ThdlOptions.getStringOption("thdl.default.roman.font.face",
|
||||
"Serif"); // FIXME write out this preference.
|
||||
romanFontSize = defaultRomanFontSize(); // FIXME make pref
|
||||
setRomanAttributeSet(romanFontFamily, romanFontSize);
|
||||
|
||||
// newDocument();
|
||||
|
@ -403,12 +419,12 @@ public RTFEditorKit rtfEd = null;
|
|||
styleContext = new StyleContext();
|
||||
|
||||
doc = new TibetanDocument(styleContext);
|
||||
doc.setTibetanFontSize(36);
|
||||
doc.setTibetanFontSize(defaultTibFontSize());
|
||||
setDocument(doc);
|
||||
|
||||
Style defaultStyle = styleContext.getStyle(StyleContext.DEFAULT_STYLE);
|
||||
StyleConstants.setFontFamily(defaultStyle, "TibetanMachineWeb");
|
||||
StyleConstants.setFontSize(defaultStyle, 36);
|
||||
StyleConstants.setFontSize(defaultStyle, defaultTibFontSize());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1006,9 +1022,9 @@ public void paste(int offset) {
|
|||
e.consume();
|
||||
initKeyboard();
|
||||
if (isTibetan)
|
||||
doc.appendDuff(caret.getDot(),"\n",TibetanMachineWeb.getAttributeSet(1));
|
||||
doc.appendDuff(caret.getDot(),"\n",TibetanMachineWeb.getAttributeSet(1)); // FIXME does this work on all platforms?
|
||||
else
|
||||
append("\n", romanAttributeSet);
|
||||
append("\n", romanAttributeSet); // FIXME does this work on all platforms?
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1455,10 +1471,10 @@ public void paste(int offset) {
|
|||
*/
|
||||
public void toTibetanMachineWeb(String wylie, int offset) {
|
||||
try {
|
||||
StringTokenizer sTok = new StringTokenizer(wylie, "\n\t", true);
|
||||
StringTokenizer sTok = new StringTokenizer(wylie, "\n\t", true); // FIXME does this work on all platforms?
|
||||
while (sTok.hasMoreTokens()) {
|
||||
String next = sTok.nextToken();
|
||||
if (next.equals("\n") || next.equals("\t")) {
|
||||
if (next.equals("\n") || next.equals("\t")) { // FIXME does this work on all platforms?
|
||||
try {
|
||||
doc.insertString(offset, next, null);
|
||||
offset++;
|
||||
|
|
|
@ -33,6 +33,7 @@ import javax.swing.text.rtf.*;
|
|||
|
||||
import org.thdl.tib.text.*;
|
||||
import org.thdl.util.ThdlDebug;
|
||||
import org.thdl.util.ThdlOptions;
|
||||
import org.thdl.util.StatusBar;
|
||||
import org.thdl.util.ThdlActionListener;
|
||||
import org.thdl.util.RTFPane;
|
||||
|
@ -80,7 +81,6 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
private final String keybd4HelpFile = "Sambhota_keymap_one.rtf";
|
||||
|
||||
|
||||
private String fontName = "";
|
||||
private JComboBox fontFamilies, fontSizes;
|
||||
private JFileChooser fileChooser;
|
||||
private javax.swing.filechooser.FileFilter rtfFilter;
|
||||
|
@ -90,7 +90,6 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
private static int numberOfTibsRTFOpen = 0;
|
||||
private static int x_size;
|
||||
private static int y_size;
|
||||
private TibetanKeyboard sambhota1Keyboard = null, duff1Keyboard = null, duff2Keyboard = null;
|
||||
|
||||
/**
|
||||
* The text editing window which this Jskad object embeds.
|
||||
|
@ -115,10 +114,12 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
* is a JApplet then the File menu is omitted from the menu bar.
|
||||
*/
|
||||
public Jskad(final Object parent) {
|
||||
if (Boolean.getBoolean("thdl.Jskad.disable.status.bar")) {
|
||||
if (ThdlOptions.getBooleanOption("thdl.Jskad.disable.status.bar")) {
|
||||
statusBar = null;
|
||||
} else {
|
||||
statusBar = new StatusBar("Welcome to Jskad!");
|
||||
statusBar
|
||||
= new StatusBar(ThdlOptions.getStringOption("thdl.Jskad.initial.status.message",
|
||||
"Welcome to Jskad!"));
|
||||
}
|
||||
parentObject = parent;
|
||||
numberOfTibsRTFOpen++;
|
||||
|
@ -378,49 +379,35 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
toolBar.add(new JLabel("Keyboard:"));
|
||||
toolBar.addSeparator();
|
||||
|
||||
/* Initialize dp before calling installKeyboard. */
|
||||
if (ThdlOptions.getBooleanOption(Jskad.enableKeypressStatusProp)) {
|
||||
dp = new DuffPane(statusBar);
|
||||
} else {
|
||||
dp = new DuffPane();
|
||||
}
|
||||
|
||||
|
||||
String[] keyboard_options = {keybd1Description,
|
||||
keybd2Description,
|
||||
keybd3Description,
|
||||
keybd4Description};
|
||||
final JComboBox keyboards = new JComboBox(keyboard_options);
|
||||
int initialKeyboard
|
||||
= ThdlOptions.getIntegerOption("thdl.default.tibetan.keyboard", 0);
|
||||
try {
|
||||
keyboards.setSelectedIndex(initialKeyboard);
|
||||
} catch (IllegalArgumentException e) {
|
||||
keyboards.setSelectedIndex(0); // good ol' Wylie
|
||||
}
|
||||
installKeyboard(initialKeyboard);
|
||||
keyboards.addActionListener(new ThdlActionListener() {
|
||||
public void theRealActionPerformed(ActionEvent e) {
|
||||
switch (keyboards.getSelectedIndex()) {
|
||||
case 0: //Extended Wylie
|
||||
installKeyboard("wylie");
|
||||
break;
|
||||
|
||||
case 1: //TCC 1
|
||||
if (duff1Keyboard == null)
|
||||
duff1Keyboard = installKeyboard("tcc1");
|
||||
else
|
||||
dp.registerKeyboard(duff1Keyboard);
|
||||
break;
|
||||
|
||||
case 2: //TCC 2
|
||||
if (duff2Keyboard == null)
|
||||
duff2Keyboard = installKeyboard("tcc2");
|
||||
else
|
||||
dp.registerKeyboard(duff2Keyboard);
|
||||
break;
|
||||
|
||||
case 3: //Sambhota
|
||||
if (sambhota1Keyboard == null)
|
||||
sambhota1Keyboard = installKeyboard("sambhota1");
|
||||
else
|
||||
dp.registerKeyboard(sambhota1Keyboard);
|
||||
break;
|
||||
}
|
||||
installKeyboard(keyboards.getSelectedIndex());
|
||||
}
|
||||
});
|
||||
toolBar.add(keyboards);
|
||||
toolBar.add(Box.createHorizontalGlue());
|
||||
|
||||
if (Boolean.getBoolean(Jskad.enableKeypressStatusProp)) {
|
||||
dp = new DuffPane(statusBar);
|
||||
} else {
|
||||
dp = new DuffPane();
|
||||
}
|
||||
JScrollPane sp = new JScrollPane(dp, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
dp.getDocument().addDocumentListener(this);
|
||||
|
||||
|
@ -786,7 +773,7 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
try {
|
||||
BufferedReader in = new BufferedReader(new FileReader(txt_fileChosen));
|
||||
DuffPane dp2;
|
||||
if (Boolean.getBoolean(Jskad.enableKeypressStatusProp)) {
|
||||
if (ThdlOptions.getBooleanOption(Jskad.enableKeypressStatusProp)) {
|
||||
dp2 = new DuffPane(statusBar);
|
||||
} else {
|
||||
dp2 = new DuffPane();
|
||||
|
@ -812,7 +799,41 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
}
|
||||
}
|
||||
|
||||
private TibetanKeyboard installKeyboard(String name) {
|
||||
private TibetanKeyboard sambhota1Keyboard = null, duff1Keyboard = null, duff2Keyboard = null;
|
||||
private void installKeyboard(int keyboardIndex) {
|
||||
switch (keyboardIndex) {
|
||||
case 1: //TCC 1
|
||||
if (duff1Keyboard == null)
|
||||
duff1Keyboard = installKeyboard("tcc1");
|
||||
else
|
||||
dp.registerKeyboard(duff1Keyboard);
|
||||
break;
|
||||
|
||||
case 2: //TCC 2
|
||||
if (duff2Keyboard == null)
|
||||
duff2Keyboard = installKeyboard("tcc2");
|
||||
else
|
||||
dp.registerKeyboard(duff2Keyboard);
|
||||
break;
|
||||
|
||||
case 3: //Sambhota
|
||||
if (sambhota1Keyboard == null)
|
||||
sambhota1Keyboard = installKeyboard("sambhota1");
|
||||
else
|
||||
dp.registerKeyboard(sambhota1Keyboard);
|
||||
break;
|
||||
default: //Extended Wylie
|
||||
if (0 != keyboardIndex
|
||||
&& ThdlOptions.getBooleanOption("thdl.debug")) {
|
||||
throw new Error("You set thdl.default.tibetan.keyboard to an illegal value: "
|
||||
+ keyboardIndex);
|
||||
}
|
||||
installKeyboard("wylie");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private TibetanKeyboard installKeyboard(String name) {
|
||||
TibetanKeyboard tk = null;
|
||||
|
||||
if (!name.equalsIgnoreCase("wylie")) {
|
||||
|
@ -833,6 +854,7 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
}
|
||||
catch (TibetanKeyboard.InvalidKeyboardException ike) {
|
||||
System.out.println("invalid keyboard file or file not found");
|
||||
ThdlDebug.noteIffyCode();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -840,6 +862,7 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
return null;
|
||||
}
|
||||
|
||||
ThdlDebug.verify("dp != null", dp != null);
|
||||
dp.registerKeyboard(tk);
|
||||
return tk;
|
||||
}
|
||||
|
@ -1074,22 +1097,27 @@ public class Jskad extends JPanel implements DocumentListener {
|
|||
* you discover a bug, please send us an email, making sure to include
|
||||
* the jskad.log file as an attachment. */
|
||||
public static void main(String[] args) {
|
||||
ThdlDebug.attemptToSetUpLogFile("jskad", ".log");
|
||||
try {
|
||||
ThdlDebug.attemptToSetUpLogFile("jskad", ".log");
|
||||
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
|
||||
JFrame f = new JFrame("Jskad");
|
||||
Dimension d = f.getToolkit().getScreenSize();
|
||||
x_size = d.width/4*3;
|
||||
y_size = d.height/4*3;
|
||||
f.setSize(x_size, y_size);
|
||||
f.setLocation(d.width/8, d.height/8);
|
||||
f.getContentPane().add(new Jskad(f));
|
||||
f.setVisible(true);
|
||||
JFrame f = new JFrame("Jskad");
|
||||
Dimension d = f.getToolkit().getScreenSize();
|
||||
x_size = d.width/4*3;
|
||||
y_size = d.height/4*3;
|
||||
f.setSize(x_size, y_size);
|
||||
f.setLocation(d.width/8, d.height/8);
|
||||
f.getContentPane().add(new Jskad(f));
|
||||
f.setVisible(true);
|
||||
} catch (ThdlLazyException e) {
|
||||
System.err.println("Jskad has a BUG:");
|
||||
e.getRealException().printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import java.io.FileOutputStream;
|
|||
import java.io.File;
|
||||
|
||||
import org.thdl.util.TeeStream;
|
||||
import org.thdl.util.ThdlOptions;
|
||||
|
||||
/**
|
||||
* This uninstantiable class provides assertions and the like in a
|
||||
|
@ -64,7 +65,7 @@ public class ThdlDebug {
|
|||
throw new ThdlLazyException(new Error(((msg == null)
|
||||
? "THDL Tools sanity check: "
|
||||
: msg)
|
||||
+ "An assertion failed. This means that there is a bug in this software."
|
||||
+ "\nAn assertion failed. This means that there is a bug in this software."
|
||||
+ contactMsg));
|
||||
}
|
||||
}
|
||||
|
@ -73,15 +74,15 @@ public class ThdlDebug {
|
|||
* out. For example, if you have to catch an IOException, but
|
||||
* you're fairly certain that it'll never be thrown, call this
|
||||
* function if it is indeed thrown. Developers can set the
|
||||
* THDL_DIE_EAGERLY property to true (using <code>'java
|
||||
* -DTHDL_DIE_ON_IFFY_CODE=true'</code>) in order to test the
|
||||
* code's robustness.
|
||||
* thdl.debug property to true (using <code>'java
|
||||
* -Dthdl.debug=true'</code> or the properties files) in order to
|
||||
* test the code's robustness.
|
||||
*
|
||||
* Throws a THDL-specific exception so that you can catch these
|
||||
* specially in case you want to ignore them.
|
||||
*
|
||||
* @throws ThdlLazyException if the THDL_DIE_ON_IFFY_CODE system
|
||||
* property is set to "true" */
|
||||
* @throws ThdlLazyException if the thdl.debug option is set to
|
||||
* "true" */
|
||||
public static void noteIffyCode()
|
||||
throws ThdlLazyException
|
||||
{
|
||||
|
@ -89,8 +90,8 @@ public class ThdlDebug {
|
|||
/* FIXME: find all calls to this function and rethink or shore
|
||||
up the calling code. */
|
||||
|
||||
if (Boolean.getBoolean("THDL_DIE_ON_IFFY_CODE"))
|
||||
throw new ThdlLazyException(new Error("You've reached some iffy code, some code that's not well thought-out. Because you invoked the Java runtime environment with the property THDL_DIE_ON_IFFY_CODE set to true (developers: use 'ant -Dthdl.die.on.iffy=false' to prevent this), the program is now aborting."));
|
||||
if (ThdlOptions.getBooleanOption("thdl.debug"))
|
||||
throw new ThdlLazyException(new Error("You've reached some iffy code, some code that's not well thought-out. Because you invoked the Java runtime environment with the property thdl.debug set to true (developers: use 'ant -Dthdl.debug=false' to prevent this), or because you set the thdl.debug preference to true in one of the preferences files, the program is now aborting."));
|
||||
}
|
||||
|
||||
/** Exits the program with a message that the CLASSPATH is not set
|
||||
|
@ -101,7 +102,7 @@ public class ThdlDebug {
|
|||
|
||||
/* FIXME */
|
||||
System.err.println("Note that Savant and QuillDriver CANNOT be invoked via the");
|
||||
System.err.println("'java -jar Savant-xyz.jar' option, because that silently ignores");
|
||||
System.err.println("'java -jar Savant-xyz.jar' option, because that silently ignores"); // FIXME yes you can when Java Web Start is up and running
|
||||
System.err.println("the CLASSPATH. This means that double-clicking them won't work");
|
||||
System.err.println("either, because we don't set the JARs' manifest files to contain");
|
||||
System.err.println("Class-path attributes. See installation instructions."); /* FIXME: we don't HAVE installation instructions, do we? */
|
||||
|
@ -109,7 +110,7 @@ public class ThdlDebug {
|
|||
System.err.println("Details: Missing class: "
|
||||
+ ((error == null)
|
||||
? "unknown!" : error.getMessage()));
|
||||
if (Boolean.getBoolean("THDL_DEBUG")) {
|
||||
if (ThdlOptions.getBooleanOption("thdl.debug")) {
|
||||
System.err.println("Details: Stack trace: "
|
||||
+ ((error == null)
|
||||
? "unknown!" : error.getMessage()));
|
||||
|
@ -124,18 +125,22 @@ public class ThdlDebug {
|
|||
* path, because we may put this file into an arbitrary
|
||||
* directory. */
|
||||
public static void attemptToSetUpLogFile(String prefix, String suffix) {
|
||||
if (ThdlOptions.getBooleanOption("thdl.disable.log.file"))
|
||||
return;
|
||||
// Else:
|
||||
final String tempDirProp = "thdl.use.temp.file.directory.for.log";
|
||||
final String logDirProp = "thdl.log.directory";
|
||||
File logFile = null;
|
||||
|
||||
if (Boolean.getBoolean(tempDirProp)) {
|
||||
if (ThdlOptions.getBooleanOption(tempDirProp)) {
|
||||
/* The log file won't be named 'jskad.log', it'll be named
|
||||
'jskad-SAKFJDS3134.log', and they'll all just pile up,
|
||||
because we don't deleteOnExit. */
|
||||
|
||||
/* First, ensure that the user hasn't set conflicting options. */
|
||||
try {
|
||||
if (null != System.getProperty("thdl.log.directory"))
|
||||
if (!("".equals(ThdlOptions.getStringOption("thdl.log.directory",
|
||||
""))))
|
||||
throw new Error("You cannot set the property "
|
||||
+ tempDirProp + " and the property "
|
||||
+ logDirProp
|
||||
|
@ -159,13 +164,14 @@ public class ThdlDebug {
|
|||
thdl.log.directory, respect their choice. */
|
||||
String logDir = null;
|
||||
try {
|
||||
logDir = System.getProperty(logDirProp);
|
||||
logDir = ThdlOptions.getStringOption(logDirProp, "");
|
||||
} catch (Exception e) {
|
||||
/* SecurityExceptions, e.g., will trigger this. We
|
||||
leave logDir null. */
|
||||
noteIffyCode();
|
||||
}
|
||||
if (null != logDir) {
|
||||
if (!("".equals(ThdlOptions.getStringOption("thdl.log.directory",
|
||||
"")))) {
|
||||
logFile = new File(logDir, prefix + suffix);
|
||||
} else {
|
||||
/* Create the log file in the current directory. For
|
||||
|
|
353
source/org/thdl/util/ThdlOptions.java
Normal file
353
source/org/thdl/util/ThdlOptions.java
Normal file
|
@ -0,0 +1,353 @@
|
|||
/*
|
||||
The contents of this file are subject to the THDL Open Community License
|
||||
Version 1.0 (the "License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License on the THDL web site
|
||||
(http://www.thdl.org/).
|
||||
|
||||
Software distributed under the License is distributed on an "AS IS" basis,
|
||||
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
|
||||
License for the specific terms governing rights and limitations under the
|
||||
License.
|
||||
|
||||
The Initial Developer of this software is the Tibetan and Himalayan Digital
|
||||
Library (THDL). Portions created by the THDL are Copyright 2001 THDL.
|
||||
All Rights Reserved.
|
||||
|
||||
Contributor(s): ______________________________________.
|
||||
*/
|
||||
|
||||
package org.thdl.util;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.thdl.util.ThdlLazyException;
|
||||
|
||||
/**
|
||||
* @author David Chandler
|
||||
*
|
||||
* Provides a clean interface to the multi-tiered system of user
|
||||
* preferences (also known as options).
|
||||
*
|
||||
* <p>The {@link java.util.Properties java.util.Properties} class
|
||||
* makes it easy for us to provide a hierarchical preferences (that
|
||||
* is, options) structure. One option a user might wish to set is the
|
||||
* font size for Tibetan, for example. The highest precedence goes to
|
||||
* a pref that the user set on the Java command line using something
|
||||
* like <code>java -Dpref=value -jar jskad_or_whatever.jar</code>. If
|
||||
* the user went to the trouble of doing this, we definitely want to
|
||||
* respect his or her wish. The next highest precedence goes to an
|
||||
* individual user's preferences file (a file readable and writable by
|
||||
* {@link java.util.Properties java.util.Properties}, but also
|
||||
* hand-editable), if one exists. Next is the system-wide preferences
|
||||
* file, if one exists. Finally, we fall back on the preferences file
|
||||
* shipped with the application inside the JAR.</p>
|
||||
*
|
||||
* <p>ThdlOptions is not instantiable. It contains only static
|
||||
* methods for answering questions about the user's preferences.</p>
|
||||
*
|
||||
* <p>There are three kinds of preferences: boolean-valued preferences
|
||||
* ("true" or "false"), integer-valued preferences, and string-valued
|
||||
* preferences. Boolean-valued preferences should, by convention, be
|
||||
* false by default. If you want to enable feature foo by default,
|
||||
* but give a preference for disabling it, then call the preference
|
||||
* "DisableFeatureFoo". If you want to disable feature foo by
|
||||
* default, call it "EnableFeatureFoo". This makes the users' lives
|
||||
* easier.</p>
|
||||
*
|
||||
* <p>ThdlOptions is a final class so that compilers can make this
|
||||
* code run efficiently.</p> */
|
||||
public final class ThdlOptions {
|
||||
/**
|
||||
* So that you're not tempted to instantiate this class, the
|
||||
* constructor is private: */
|
||||
private ThdlOptions() {
|
||||
// don't instantiate this class.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a boolean-valued option, or false if that
|
||||
* option is set nowhere in the hierarchy of properties files.
|
||||
*
|
||||
* @param optionName the option whose value you wish to retrieve
|
||||
* (note the naming conventions detailed in the class comment)
|
||||
*/
|
||||
public static boolean getBooleanOption(String optionName)
|
||||
{
|
||||
init();
|
||||
|
||||
// Look to the System first.
|
||||
String answer = getSystemValue(optionName);
|
||||
if (answer == null) {
|
||||
answer = userProperties.getProperty(optionName, "false");
|
||||
}
|
||||
return answer.equalsIgnoreCase("true");
|
||||
}
|
||||
|
||||
// FIXMEDOC
|
||||
private static String getSystemValue(String optionName) {
|
||||
// Look to the System first.
|
||||
String answer = null;
|
||||
try {
|
||||
answer = System.getProperty(optionName);
|
||||
} catch (SecurityException e) {
|
||||
if (!suppressErrs())
|
||||
throw e;
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a string-valued option, or null if that
|
||||
* option is set nowhere in the hierarchy of properties files.
|
||||
*
|
||||
* @param optionName the option whose value you wish to retrieve
|
||||
* (note the naming conventions detailed in the class comment) */
|
||||
public static String getStringOption(String optionName)
|
||||
{
|
||||
init();
|
||||
// Look to the System first.
|
||||
String answer = getSystemValue(optionName);
|
||||
if (answer == null) {
|
||||
answer = userProperties.getProperty(optionName, null);
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a string-valued option, or defaultValue if that
|
||||
* option is set nowhere in the hierarchy of properties files.
|
||||
*
|
||||
* @param optionName the option whose value you wish to retrieve
|
||||
* (note the naming conventions detailed in the class comment)
|
||||
* @param defaultValue the default value */
|
||||
public static String getStringOption(String optionName,
|
||||
String defaultValue)
|
||||
{
|
||||
String x = getStringOption(optionName);
|
||||
if (null == x)
|
||||
return defaultValue;
|
||||
else
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of an integer-valued option, or defaultValue
|
||||
* if that option is set nowhere in the hierarchy of properties
|
||||
* files or is set to something that cannot be decoded to an
|
||||
* integer.
|
||||
*
|
||||
* @param optionName the option whose value you wish to retrieve
|
||||
* (note the naming conventions detailed in the class comment)
|
||||
* @param defaultValue the default value */
|
||||
public static int getIntegerOption(String optionName, int defaultValue)
|
||||
{
|
||||
Integer x = getIntegerOption(optionName);
|
||||
if (null == x)
|
||||
return defaultValue;
|
||||
else
|
||||
return x.intValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of an integer-valued option, or null if that
|
||||
* option is set nowhere in the hierarchy of properties files or
|
||||
* is set to something that cannot be decoded to an integer.
|
||||
*
|
||||
* @param optionName the option whose value you wish to retrieve
|
||||
* (note the naming conventions detailed in the class comment) */
|
||||
public static Integer getIntegerOption(String optionName)
|
||||
{
|
||||
init();
|
||||
|
||||
// Look to the System first.
|
||||
String answer = getSystemValue(optionName);
|
||||
if (answer == null) {
|
||||
answer = userProperties.getProperty(optionName, null);
|
||||
}
|
||||
if (null == answer) {
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
return Integer.decode(answer);
|
||||
} catch (NumberFormatException e) {
|
||||
if (getBooleanOption("thdl.debug"))
|
||||
throw new ThdlLazyException(e);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean suppressErrs() {
|
||||
return false; /* FIXME--make THIS configurable. */
|
||||
}
|
||||
private static void init() {
|
||||
try {
|
||||
initialize(ThdlOptions.class, "/options.txt",
|
||||
suppressErrs());
|
||||
} catch (FileNotFoundException e) {
|
||||
if (!suppressErrs())
|
||||
throw new ThdlLazyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/** the properties object that is chained to the system-wide
|
||||
properties which is chained to the built-in properties which
|
||||
is chained to the Java System properties */
|
||||
private static Properties userProperties = null;
|
||||
|
||||
/** to avoid initializing twice */
|
||||
private static boolean isInitialized = false;
|
||||
|
||||
/** Sets userProperties so that it represents the entire, chained
|
||||
preferences hierarchy.
|
||||
|
||||
@param resourceHolder the class associated with the builtin
|
||||
defaults properties file resource
|
||||
@param resourceName the name of the builtin defaults
|
||||
properties file resource
|
||||
@param suppressErrors true if the show must go on, false if
|
||||
you want unchecked exceptions thrown when bad things happen
|
||||
|
||||
@throws FileNotFoundException if !suppressErrors and if the
|
||||
user gave the location of the system-wide or user preferences
|
||||
files, but gave it incorrectly. */
|
||||
private static void initialize(Class resourceHolder,
|
||||
String resourceName,
|
||||
boolean suppressErrors)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
if (isInitialized)
|
||||
return;
|
||||
isInitialized = true;
|
||||
try {
|
||||
|
||||
// Get the application's built-in, default properties.
|
||||
Properties defaultProperties
|
||||
= getPropertiesFromResource(resourceHolder,
|
||||
resourceName,
|
||||
suppressErrors,
|
||||
new Properties() /* empty */);
|
||||
|
||||
// Get the system's properties, if the system administrator
|
||||
// has created any:
|
||||
Properties systemWideProperties
|
||||
= tryToGetPropsFromFile("thdl.system.wide.options.file",
|
||||
// FIXME this default is
|
||||
// system-dependent:
|
||||
"C:\\thdl_opt.txt",
|
||||
defaultProperties,
|
||||
suppressErrors);
|
||||
|
||||
// Get the user's properties, if they've set any:
|
||||
userProperties
|
||||
= tryToGetPropsFromFile("thdl.user.options.file",
|
||||
// FIXME this default is
|
||||
// system-dependent:
|
||||
"C:\\thdl_uopt.txt",
|
||||
systemWideProperties,
|
||||
suppressErrors);
|
||||
} catch (SecurityException e) {
|
||||
if (suppressErrors) {
|
||||
if (userProperties == null) {
|
||||
userProperties = new Properties(); // empty
|
||||
} // else leave it as is.
|
||||
} else {
|
||||
throw new ThdlLazyException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a new, nonnull Properties object if (1) a preferences
|
||||
file is found at the location specified by the value of the
|
||||
System property pName, if it is set, or at defaultLoc, if
|
||||
pName is not set, and (2) if that file is successfully read
|
||||
in. Otherwise, this returns defaultProps.
|
||||
|
||||
@param pName the name of the System property that overrides
|
||||
this application's default idea of where to look for the file
|
||||
@param defaultLoc the default preferences file name
|
||||
@param suppressErrors true iff you want to proceed without
|
||||
throwing exceptions whenever possible
|
||||
|
||||
@throws FileNotFoundException if !suppressErrors and if the
|
||||
user gave the location of the system-wide or user preferences
|
||||
files, but gave it incorrectly. @throws SecurityException if
|
||||
playing with files or system properties is not OK */
|
||||
private static Properties tryToGetPropsFromFile(String pName,
|
||||
String defaultLoc,
|
||||
Properties defaultProps,
|
||||
boolean suppressErrors)
|
||||
throws FileNotFoundException, SecurityException
|
||||
{
|
||||
Properties props = defaultProps;
|
||||
String systemPropFileName = System.getProperty(pName, defaultLoc);
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
fis = new FileInputStream(systemPropFileName);
|
||||
} catch (FileNotFoundException e) {
|
||||
if (null != System.getProperty(pName)) {
|
||||
// the user explicitly set this, but not
|
||||
// correctly.
|
||||
if (!suppressErrors)
|
||||
throw e;
|
||||
} else {
|
||||
// definitely suppress this. On a Mac or
|
||||
// Unix/Linux box, this'll happen every time
|
||||
// at present. (FIXME)
|
||||
}
|
||||
}
|
||||
|
||||
if (fis != null) {
|
||||
props = getPropertiesFromStream(fis,
|
||||
suppressErrors,
|
||||
defaultProps);
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
|
||||
// FIXMEDOC
|
||||
private static Properties getPropertiesFromResource(Class resourceHolder,
|
||||
String resourceName,
|
||||
boolean suppressErrors,
|
||||
Properties defaults)
|
||||
{
|
||||
InputStream in = resourceHolder.getResourceAsStream(resourceName);
|
||||
return getPropertiesFromStream(in, suppressErrors, defaults);
|
||||
}
|
||||
|
||||
// FIXMEDOC
|
||||
private static Properties getPropertiesFromStream(InputStream in,
|
||||
boolean suppressErrors,
|
||||
Properties defaults)
|
||||
throws NullPointerException
|
||||
{
|
||||
Properties options;
|
||||
if (defaults == null)
|
||||
options = new Properties(); // empty properties list
|
||||
else
|
||||
options = new Properties(defaults);
|
||||
try {
|
||||
// Load props from the resource:
|
||||
options.load(in);
|
||||
return options;
|
||||
} catch (Exception e) {
|
||||
// e is an IOException or a SecurityException or, if the
|
||||
// resource was not found, a NullPointerException.
|
||||
if (suppressErrors) {
|
||||
return options;
|
||||
} else {
|
||||
throw new ThdlLazyException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in a new issue