diff --git a/.gitignore b/.gitignore index 66d2cf7357..0eb2a705a9 100644 --- a/.gitignore +++ b/.gitignore @@ -241,6 +241,9 @@ # VS Code .vscode +# IDE files +**/.idea/**/*.xml +**/.idea/**/*.iml .DS_Store diff --git a/android/KMAPro/build.gradle b/android/KMAPro/build.gradle index 47a5bb35f6..f7bea47432 100644 --- a/android/KMAPro/build.gradle +++ b/android/KMAPro/build.gradle @@ -20,5 +20,6 @@ allprojects { google() jcenter() mavenCentral() + maven { url "https://jitpack.io" } } } diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 3284df5642..ec03aeb697 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -127,6 +127,13 @@ dependencies { transitive = true } implementation 'androidx.preference:preference:1.1.0' + + // Add dependency for generating QR Codes + // (Even though it's embedded in KMEA, because we're manually copying keyman-engine.aar, + // we "lose" it in the dependency management) + implementation ('com.github.kenglxn.QRGen:android:2.6.0') { + transitive = true + } } /*def void dumpProperties(it){ diff --git a/android/KMEA/app/build.gradle b/android/KMEA/app/build.gradle index 96e21def85..c124d45721 100644 --- a/android/KMEA/app/build.gradle +++ b/android/KMEA/app/build.gradle @@ -82,6 +82,11 @@ dependencies { transitive = true } + // Generate QR Codes + implementation ('com.github.kenglxn.QRGen:android:2.6.0') { + transitive = true + } + // Assign the annotation processor for tests. testAnnotationProcessor 'com.google.auto.service:auto-service:1.0-rc4' } diff --git a/android/KMEA/app/src/main/assets/nrc.en.mtnt.model.kmp b/android/KMEA/app/src/main/assets/nrc.en.mtnt.model.kmp index 4a1ed46be6..f736a99c30 100644 Binary files a/android/KMEA/app/src/main/assets/nrc.en.mtnt.model.kmp and b/android/KMEA/app/src/main/assets/nrc.en.mtnt.model.kmp differ diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java index c2d7a1e9f9..c122381785 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java @@ -33,7 +33,7 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { public static final String ARG_MODEL_ID = "KMKeyboardActivity.modelID"; public static final String ARG_MODEL_NAME = "KMKeyboardActivity.modelName"; public static final String ARG_MODEL_URL = "KMKeyboardActivity.modelURL"; - public static final String ARG_MODEL_CUSTOM_HELP_LINK = "KMKeyboardActivity.customHelpLink"; + public static final String ARG_CUSTOM_HELP_LINK = "KMKeyboardActivity.customHelpLink"; // custom keyboard public static final String ARG_KEYBOARD = "KMKeyboardActivity.keyboard"; diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java index e44c36e564..ed8f21b69f 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java @@ -2224,12 +2224,15 @@ public final class KMManager { // Move the cursor back if there's a split surrogate pair if (Character.isHighSurrogate(sequence.charAt(sequence.length()-1))) { - String origChars = sequence.toString(); ic.commitText("", -1); sequence = ic.getTextBeforeCursor(length, 0); } - if (sequence != null && Character.isLowSurrogate(sequence.charAt(0))) { + if (sequence == null || sequence.length() <= 0) { + return ""; + } + + if (Character.isLowSurrogate(sequence.charAt(0))) { // Adjust if the first char is also a split surrogate pair // subSequence indices are start(inclusive) to end(exclusive) sequence = sequence.subSequence(1, sequence.length()); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java index 420d66810b..84eccb725f 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardInfoActivity.java @@ -4,7 +4,6 @@ package com.tavultesoft.kmea; -import java.io.File; import java.util.ArrayList; import java.util.HashMap; @@ -12,27 +11,32 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; -import androidx.core.content.FileProvider; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import android.view.Window; import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.Toast; -import com.tavultesoft.kmea.util.FileUtils; import com.tavultesoft.kmea.util.FileProviderUtils; +import com.tavultesoft.kmea.util.HelpFile; import com.tavultesoft.kmea.util.MapCompat; +import com.tavultesoft.kmea.util.QRCodeUtil; // Public access is necessary to avoid IllegalAccessException public final class KeyboardInfoActivity extends AppCompatActivity { + private static final String TAG = "KeyboardInfoActivity"; private static Toolbar toolbar = null; private static ListView listView = null; private static ArrayList> infoList = null; @@ -46,7 +50,6 @@ public final class KeyboardInfoActivity extends AppCompatActivity { super.onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_NO_TITLE); final Context context = this; - final String authority = FileProviderUtils.getAuthority(context); setContentView(R.layout.activity_list_layout); toolbar = (Toolbar) findViewById(R.id.list_toolbar); @@ -120,36 +123,38 @@ public final class KeyboardInfoActivity extends AppCompatActivity { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { if (position == 1) { - Intent i = new Intent(Intent.ACTION_VIEW); - if (customHelpLink != null) { - if (FileUtils.isWelcomeFile(customHelpLink) && ! KMManager.isTestMode()) { - File customHelp = new File(new File(customHelpLink).getAbsolutePath()); - i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // Starting with Android N, you can't pass file:// to intents, so we use FileProvider - try { - Uri contentUri = FileProvider.getUriForFile( - context, authority, customHelp); - i.setDataAndType(contentUri, "text/html"); - } catch (NullPointerException e) { - String message = "FileProvider undefined in app to load" + customHelp.toString(); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - Log.e("KeyboardInfoActivity", message); - } - } - else { - i.setData(Uri.parse(customHelpLink)); - } + // Display local welcome.htm help file, including associated assets + Intent i = HelpFile.toActionView(context, customHelpLink, packageID); + if (FileProviderUtils.exists(context)|| KMManager.isTestMode()) { startActivity(i); } } else { + Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(helpUrlStr)); startActivity(i); } } } }); + + // If QRGen library included, also display QR code for sharing keyboard + if (QRCodeUtil.libraryExists(context)) { + String url = String.format("%s%s", QRCodeUtil.QR_BASE, kbID); + + // Shorten listView so the QR code will show + ViewGroup.LayoutParams lp = listView.getLayoutParams(); + lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; + listView.setLayoutParams(lp); + + LinearLayout qrLayout = findViewById(R.id.qrLayout); + qrLayout.setVisibility(View.VISIBLE); + + Bitmap myBitmap = QRCodeUtil.toBitmap(url); + ImageView imageView = (ImageView) findViewById(R.id.qrCode); + imageView.setImageBitmap(myBitmap); + } } @Override diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardSettingsActivity.java index 89b1d07ebc..6921b86dcb 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardSettingsActivity.java @@ -13,14 +13,18 @@ import androidx.appcompat.widget.Toolbar; import android.app.DialogFragment; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import androidx.core.content.FileProvider; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import android.view.Window; import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.SimpleAdapter; @@ -29,7 +33,9 @@ import android.widget.Toast; import com.tavultesoft.kmea.util.FileUtils; import com.tavultesoft.kmea.util.FileProviderUtils; +import com.tavultesoft.kmea.util.HelpFile; import com.tavultesoft.kmea.util.MapCompat; +import com.tavultesoft.kmea.util.QRCodeUtil; import static com.tavultesoft.kmea.ConfirmDialogFragment.DialogType.DIALOG_TYPE_DELETE_KEYBOARD; @@ -135,33 +141,19 @@ public final class KeyboardSettingsActivity extends AppCompatActivity { // "Help" link clicked if (itemTitle.equals(getString(R.string.help_link))) { - Intent i = new Intent(Intent.ACTION_VIEW); - if (customHelpLink != null) { - if (FileUtils.isWelcomeFile(customHelpLink)) { - File customHelp = new File(new File(customHelpLink).getAbsolutePath()); - i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // Starting with Android N, you can't pass file:// to intents, so we use FileProvider - try { - Uri contentUri = FileProvider.getUriForFile( - context, authority, customHelp); - i.setDataAndType(contentUri, "text/html"); - } catch (NullPointerException e) { - String message = "FileProvider undefined in app to load" + customHelp.toString(); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - Log.e("TAG", message); - } - } - else { - i.setData(Uri.parse(customHelpLink)); - } - if (FileProviderUtils.exists(context)) { + // Display local welcome.htm help file, including associated assets + Intent i = HelpFile.toActionView(context, customHelpLink, packageID); + + if (FileProviderUtils.exists(context) || KMManager.isTestMode()) { startActivity(i); } } else { + Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(helpUrlStr)); startActivity(i); } + // "Uninstall Keyboard" clicked } else if (itemTitle.equals(getString(R.string.uninstall_keyboard))) { // Uninstall selected keyboard @@ -173,6 +165,23 @@ public final class KeyboardSettingsActivity extends AppCompatActivity { } } }); + + // If QRGen library included, also display QR code for sharing keyboard + if (QRCodeUtil.libraryExists(context)) { + String url = String.format("%s%s", QRCodeUtil.QR_BASE, kbID); + + // Shorten listView so the QR code will show + ViewGroup.LayoutParams lp = listView.getLayoutParams(); + lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; + listView.setLayoutParams(lp); + + LinearLayout qrLayout = findViewById(R.id.qrLayout); + qrLayout.setVisibility(View.VISIBLE); + + Bitmap myBitmap = QRCodeUtil.toBitmap(url); + ImageView imageView = (ImageView) findViewById(R.id.qrCode); + imageView.setImageBitmap(myBitmap); + } } @Override diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java index 8f78d3a359..b360dbda6f 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelInfoActivity.java @@ -28,6 +28,7 @@ import android.widget.Toast; import com.tavultesoft.kmea.util.FileUtils; import com.tavultesoft.kmea.util.FileProviderUtils; +import com.tavultesoft.kmea.util.HelpFile; import com.tavultesoft.kmea.util.MapCompat; import static com.tavultesoft.kmea.ConfirmDialogFragment.DialogType.DIALOG_TYPE_DELETE_MODEL; @@ -81,7 +82,7 @@ public final class ModelInfoActivity extends AppCompatActivity { // Currently, model help only available if custom link exists String icon = String.valueOf(R.drawable.ic_arrow_forward); // Don't show help link arrow if both custom help and File Provider don't exist - // TODO: Update this when model help available on help.keyman.com + // TODO: Update this when model help available on help.keyman.com if ( (!customHelpLink.equals("") && !FileProviderUtils.exists(context)) || customHelpLink.equals("") ){ icon = noIcon; @@ -128,32 +129,17 @@ public final class ModelInfoActivity extends AppCompatActivity { // "Help" link clicked if (itemTitle.equals(getString(R.string.help_link))) { - Intent i = new Intent(Intent.ACTION_VIEW); - if (!customHelpLink.equals("")) { - if (FileUtils.isWelcomeFile(customHelpLink)) { - File customHelp = new File(new File(customHelpLink).getAbsolutePath()); - i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - // Starting with Android N, you can't pass file:// to intents, so we use FileProvider - try { - Uri contentUri = FileProvider.getUriForFile( - context, authority, customHelp); - i.setDataAndType(contentUri, "text/html"); - } catch (NullPointerException e) { - String message = "FileProvider undefined in app to load" + customHelp.toString(); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - Log.e("ModelInfoActivity", message); - } - } - else { - i.setData(Uri.parse(customHelpLink)); - } - if (FileProviderUtils.exists(context)) { + // Display local welcome.htm help file, including associated assets + Intent i = HelpFile.toActionView(context, customHelpLink, packageID); + + if (FileProviderUtils.exists(context) || KMManager.isTestMode()) { startActivity(i); } } else { // We should always have a help file packaged with models. } + // "Uninstall Model" clicked } else if (itemTitle.equals(getString(R.string.uninstall_model))) { // Uninstall selected model diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtil.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtil.java index a6070b6fc2..0a83d73733 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtil.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudDataJsonUtil.java @@ -37,7 +37,7 @@ public class CloudDataJsonUtil { public static HashMap createKeyboardInfoMap(String aPackageId,String aLanguageId, String aLanguageName, String aKeyboardId, String aKeyboardName, String aKeyboardVersion, String anIsCustomKeyboard, - String aFont, String aOskFont) + String aFont, String aOskFont, String aCustomHelpLink) { HashMap keyboardInfo = new HashMap(); keyboardInfo.put(KMManager.KMKey_PackageID, aPackageId); @@ -48,8 +48,12 @@ public class CloudDataJsonUtil { keyboardInfo.put(KMManager.KMKey_KeyboardVersion, aKeyboardVersion); keyboardInfo.put(KMManager.KMKey_CustomKeyboard, anIsCustomKeyboard); keyboardInfo.put(KMManager.KMKey_Font, aFont); - if (aOskFont != null) + if (aOskFont != null) { keyboardInfo.put(KMManager.KMKey_OskFont, aOskFont); + } + if (aCustomHelpLink != null) { + keyboardInfo.put(KMManager.KMKey_CustomHelpLink, aCustomHelpLink); + } return keyboardInfo; } @@ -81,9 +85,10 @@ public class CloudDataJsonUtil { String kbName = keyboardJSON.getString(KMManager.KMKey_Name); String kbVersion = keyboardJSON.optString(KMManager.KMKey_KeyboardVersion, "1.0"); String kbFont = keyboardJSON.optString(KMManager.KMKey_Font, ""); + String customHelpLink = keyboardJSON.optString(KMManager.KMKey_CustomHelpLink, null); //String kbKey = String.format("%s_%s", langID, kbID); - HashMap hashMap = createKeyboardInfoMap(pkgID,langID,langName,kbID,kbName,kbVersion,isCustom,kbFont,null); + HashMap hashMap = createKeyboardInfoMap(pkgID,langID,langName,kbID,kbName,kbVersion,isCustom,kbFont,null, customHelpLink); // if (keyboardModifiedDates.get(kbID) == null) { diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudKeyboardMetaDataDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudKeyboardMetaDataDownloadCallback.java index 07c7fb5f9c..4938057056 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudKeyboardMetaDataDownloadCallback.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudKeyboardMetaDataDownloadCallback.java @@ -271,12 +271,13 @@ public class CloudKeyboardMetaDataDownloadCallback implements ICloudDownloadCall String _font = _keyboard.optString(KMManager.KMKey_Font); String _oskFont = _keyboard.optString(KMManager.KMKey_OskFont); + String _customHelpLink = _keyboard.optString(KMManager.KMKey_CustomHelpLink, null); theKbData.additionalDownloadid = CloudKeyboardDataDownloadCallback.createDownloadId(_key_id); theKbData.keyboardInfo = CloudDataJsonUtil .createKeyboardInfoMap( - _pkgID, _lang_id, _langName, _key_id, _kbName, _kbVersion, _kbIsCustom, _font, _oskFont); + _pkgID, _lang_id, _langName, _key_id, _kbName, _kbVersion, _kbIsCustom, _font, _oskFont, _customHelpLink); theKbData.additionalDownloads = urls; } catch(JSONException _e) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java index fa088416a8..bed03bbbb4 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java @@ -5,6 +5,7 @@ import android.os.Bundle; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.KeyboardPickerActivity; +import com.tavultesoft.kmea.util.MapCompat; import java.io.Serializable; import java.util.Map; @@ -50,6 +51,13 @@ public class Keyboard implements Serializable, LanguageResource { return this.map.get(KMManager.KMKey_KeyboardName); } + public String getCustomHelpLink() { + if (this.map.containsKey(KMManager.KMKey_CustomHelpLink)) { + return this.map.get(KMManager.KMKey_CustomHelpLink); + } + return null; + } + public String getVersion() { return this.map.get(KMManager.KMKey_KeyboardVersion); } @@ -60,6 +68,7 @@ public class Keyboard implements Serializable, LanguageResource { public Bundle buildDownloadBundle() { Bundle bundle = new Bundle(); + bundle.putString(KMKeyboardDownloaderActivity.ARG_PKG_ID, getPackage()); bundle.putString(KMKeyboardDownloaderActivity.ARG_KB_ID, getResourceId()); bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_ID, getLanguageCode()); @@ -70,9 +79,13 @@ public class Keyboard implements Serializable, LanguageResource { if(isCustom == null) { isCustom = "N"; } - bundle.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, isCustom.equals("Y")); + String customHelpLink = map.get(KMManager.KMKey_CustomHelpLink); + if (customHelpLink != null) { + bundle.putString(KMKeyboardDownloaderActivity.ARG_CUSTOM_HELP_LINK, getCustomHelpLink()); + } + return bundle; } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageResource.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageResource.java index 635c76580b..fb687637a9 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageResource.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LanguageResource.java @@ -9,6 +9,7 @@ public interface LanguageResource { String getLanguageName(); String getVersion(); String getPackage(); + String getCustomHelpLink(); Bundle buildDownloadBundle(); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java index 2dddca0ddb..876013eb56 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/LexicalModel.java @@ -4,6 +4,7 @@ import android.os.Bundle; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; +import com.tavultesoft.kmea.util.MapCompat; import java.io.Serializable; import java.util.Map; @@ -52,6 +53,13 @@ public class LexicalModel implements Serializable, LanguageResource { return this.map.get(KMManager.KMKey_PackageID); } + public String getCustomHelpLink() { + if (this.map.containsKey(KMManager.KMKey_CustomHelpLink)) { + return this.map.get(KMManager.KMKey_CustomHelpLink); + } + return null; + } + public Bundle buildDownloadBundle() { Bundle bundle = new Bundle(); @@ -64,8 +72,6 @@ public class LexicalModel implements Serializable, LanguageResource { return null; } - String customHelpLink = map.get(KMManager.KMKey_CustomHelpLink); - bundle.putString(KMKeyboardDownloaderActivity.ARG_PKG_ID, getPackage()); bundle.putString(KMKeyboardDownloaderActivity.ARG_MODEL_ID, getResourceId()); bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_ID, getLanguageCode()); @@ -73,7 +79,11 @@ public class LexicalModel implements Serializable, LanguageResource { bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_NAME, getLanguageName()); bundle.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, false); bundle.putString(KMKeyboardDownloaderActivity.ARG_MODEL_URL, modelURL); - bundle.putString(KMKeyboardDownloaderActivity.ARG_MODEL_CUSTOM_HELP_LINK, customHelpLink); + + String customHelpLink = map.get(KMManager.KMKey_CustomHelpLink); + if (customHelpLink != null) { + bundle.putString(KMKeyboardDownloaderActivity.ARG_CUSTOM_HELP_LINK, customHelpLink); + } return bundle; } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/logic/ResourcesUpdateTool.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/logic/ResourcesUpdateTool.java index 8ac3d66b7a..5de4b018e5 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/logic/ResourcesUpdateTool.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/logic/ResourcesUpdateTool.java @@ -1,6 +1,5 @@ package com.tavultesoft.kmea.logic; - import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; @@ -13,6 +12,7 @@ import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.util.Log; import android.widget.Toast; import androidx.appcompat.app.AlertDialog; @@ -21,7 +21,6 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Builder; import androidx.core.app.NotificationManagerCompat; - import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KeyboardPickerActivity; import com.tavultesoft.kmea.KMManager; @@ -39,8 +38,12 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import org.json.JSONException; +import org.json.JSONObject; + public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownloadEventListener, CloudRepository.UpdateHandler{ + private static final String TAG = "ResourceUpdateTool"; /** * Force resource update. @@ -63,6 +66,16 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl */ public static final String PREF_KEY_LAST_UPDATE_CHECK = "lastUpdateCheck"; + /** + * Preference key for ignored notifications + */ + public static final String PREF_KEY_IGNORE_NOTIFICATIONS = "ignoredNotifications"; + + /** + * Months to ignore update notification + */ + public static final int MONTHS_TO_IGNORE_NOTIFICATION = 3; + private static final class OngoingUpdate { Integer notificationid; @@ -269,6 +282,84 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl addOpenUpdate(createKeyboardId(langid, kbid), null, theResourceBundle); } } + + /** + * Check shared preference to see if an update notification should be ignored. + * The window is MONTHS_TO_IGNORE_NOTIFICATION from the last time the notification was ignored. + * @param id keyboard or lexical model ID + * @return true if the notification should be ignored + */ + private boolean shouldIgnoreNotification(String id) { + SharedPreferences prefs = currentContext.getSharedPreferences(currentContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + String ignoredNotificationsStr = prefs.getString(PREF_KEY_IGNORE_NOTIFICATIONS, null); + + /* + * Preference is a JSON Object (as a string) + * { PREF_KEY_IGNORE_NOTIFICATIONS : + * { id1 : time1 ignored, + * id2 : time2 ignored + * } + * } + */ + JSONObject ignoredNotificationsObj; + if (ignoredNotificationsStr != null) { + try { + ignoredNotificationsObj = new JSONObject(ignoredNotificationsStr); + + Long lastIgnoredTime = ignoredNotificationsObj.optLong(id, 0); + if (lastIgnoredTime > 0) { + Calendar now = Calendar.getInstance(); + Calendar ignoreUntilTime = Calendar.getInstance(); + ignoreUntilTime.setTime(new Date(lastIgnoredTime)); + ignoreUntilTime.add(Calendar.MONTH, MONTHS_TO_IGNORE_NOTIFICATION); + if (now.compareTo(ignoreUntilTime) < 0) { + return true; + } + } + } catch (JSONException e) { + Log.e(TAG, "JSON Exception parsing ignoreNotifications preference"); + } + } + + return false; + } + + /** + * Update preference to ignore notifications for keyboard / lexical model ID + * @param id : keyboard or lexical model ID to ignore for MONTHS_TO_IGNORE_NOTIFICATION months + */ + private void setPrefKeyIgnoreNotifications(String id) { + SharedPreferences prefs = currentContext.getSharedPreferences( + currentContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + String ignoredNotificationsStr = prefs.getString(PREF_KEY_IGNORE_NOTIFICATIONS, null); + + /* + * Preference is a JSON Object (stored as a string) + * { PREF_KEY_IGNORE_NOTIFICATIONS : + * { id1 : time1 ignored, + * id2 : time2 ignored + * } + * } + */ + JSONObject ignoredNotificationsObj; + try { + if (ignoredNotificationsStr == null) { + ignoredNotificationsObj = new JSONObject(); + } else { + ignoredNotificationsObj = new JSONObject(ignoredNotificationsStr); + } + + Calendar now = Calendar.getInstance(); + ignoredNotificationsObj.put(id, now.getTime().getTime()); + editor.putString(PREF_KEY_IGNORE_NOTIFICATIONS, ignoredNotificationsObj.toString()); + editor.commit(); + } catch (JSONException e) { + Log.e(TAG, "JSON Exception updating ignoreNotifications preference"); + } + } + /** * send update notification. * @param theResourceBundle the bundle @@ -290,13 +381,25 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl String modelid = theResourceBundle.getString(KMKeyboardDownloaderActivity.ARG_MODEL_ID); String modelName = theResourceBundle.getString(KMKeyboardDownloaderActivity.ARG_MODEL_NAME); message = currentContext.getString(R.string.dictionary_update_message, langName, modelName); - addOpenUpdate(createLexicalModelId(langid,modelid),notification_id, theResourceBundle); + if (!shouldIgnoreNotification(modelid)) { + addOpenUpdate(createLexicalModelId(langid, modelid), notification_id, theResourceBundle); + } else { + // Update notification should be ignored + notificationManager.cancel(notification_id); + return; + } } else { String kbid = theResourceBundle.getString(KMKeyboardDownloaderActivity.ARG_KB_ID); String kbName = theResourceBundle.getString(KMKeyboardDownloaderActivity.ARG_KB_NAME); message = currentContext.getString(R.string.keyboard_update_message, langName, kbName); - addOpenUpdate(createKeyboardId(langid,kbid),notification_id, theResourceBundle); + if (!shouldIgnoreNotification(kbid)) { + addOpenUpdate(createKeyboardId(langid, kbid), notification_id, theResourceBundle); + } else { + // Update notification should be ignored + notificationManager.cancel(notification_id); + return; + } } @@ -465,6 +568,7 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl public void cancelKeyboardUpdate(String aLangId, String aKbId) { + setPrefKeyIgnoreNotifications(aKbId); removeOpenUpdate(createKeyboardId(aLangId,aKbId)); if(openUpdates.isEmpty()) checkingUpdates = false; @@ -472,6 +576,7 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl public void cancelLexicalModelUpdate(String aLangId, String aModelId) { + setPrefKeyIgnoreNotifications(aModelId); removeOpenUpdate(createLexicalModelId(aLangId,aModelId)); if(openUpdates.isEmpty()) checkingUpdates = false; diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java new file mode 100644 index 0000000000..588a7546db --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java @@ -0,0 +1,89 @@ +package com.tavultesoft.kmea.util; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +import androidx.core.content.FileProvider; + +import com.tavultesoft.kmea.KMManager; +import com.tavultesoft.kmea.util.FileProviderUtils; +import com.tavultesoft.kmea.util.FileUtils; + +import java.io.File; +import java.io.FileFilter; + + +public final class HelpFile { + private static final String TAG = "HelpFile"; + private static final String[] ASSET_MIME_TYPES = { + ClipDescription.MIMETYPE_TEXT_HTML, + "text/css", + "image/gif", + "image/jpeg", + "image/png"}; + + /** + * Utility to pass a help file and all associated assets to an Intent for Intent.ACTION_VIEW + * @param context + * @param helpFile Full path string of the html file to view + * @param packageID String of the package ID + * @return Intent + */ + public static Intent toActionView(Context context, String helpFile, String packageID) { + Intent i = new Intent(Intent.ACTION_VIEW); + + if (FileUtils.isWelcomeFile(helpFile) && ! KMManager.isTestMode()) { + File customHelp = new File(new File(helpFile).getAbsolutePath()); + i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + // Starting with Android N, you can't pass file:// to intents, so we use FileProvider + try { + final String authority = FileProviderUtils.getAuthority(context); + Uri contentUri = FileProvider.getUriForFile( + context, authority, customHelp); + i.setDataAndType(contentUri, "text/html"); + + // Grant read permission to all the files in the package so embedded assets can be viewed + ClipData clipData = new ClipData(null, ASSET_MIME_TYPES, new ClipData.Item(contentUri)); + + // Exclude html help files and JS files. Treat rest of the files as assets + FileFilter _fileFilter = new FileFilter() { + @Override + public boolean accept(File pathname) { + String name = pathname.getName(); + if (pathname.isFile() && (FileUtils.isReadmeFile(name) || + FileUtils.isWelcomeFile(name) || FileUtils.hasJavaScriptExtension(name))) { + return false; + } + return true; + } + }; + + String base = helpFile.contains("packages") ? "packages" : "models"; + File packageDir = new File( + context.getDir("data", Context.MODE_PRIVATE), base + File.separator + packageID + File.separator); + File[] files = packageDir.listFiles(_fileFilter); + for(File assetFile : files) { + Uri assetUri = FileProvider.getUriForFile( + context, authority, assetFile); + clipData.addItem(new ClipData.Item(assetUri)); + } + + // Associate assets in clipData to the intent + i.setClipData(clipData); + } catch (NullPointerException e) { + String message = "FileProvider undefined in app to load" + customHelp.toString(); + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + Log.e(TAG, message); + } + } else { + i.setData(Uri.parse(helpFile)); + } + return i; + } + +} diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/QRCodeUtil.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/QRCodeUtil.java new file mode 100644 index 0000000000..0318a9fd15 --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/QRCodeUtil.java @@ -0,0 +1,35 @@ +package com.tavultesoft.kmea.util; + +import android.content.Context; +import android.graphics.Bitmap; + +import net.glxn.qrgen.android.QRCode; + +/** + * Utility to generate QR Code from a URL string + */ +public final class QRCodeUtil { + public static final int DEFAULT_HEIGHT = 800; + public static final int DEFAULT_WIDTH = 800; + public static final String QR_BASE = "https://keyman.com/go/keyboard/%s/share"; + + /** + * Generate QR Code as a Bitmap + * @param url String + * @return Bitmap of the QR Code + */ + public static Bitmap toBitmap(String url) { + Bitmap result = QRCode.from(url).withSize(DEFAULT_WIDTH, DEFAULT_HEIGHT).bitmap(); + return result; + } + + public static boolean libraryExists(Context context) { + boolean result = false; + try { + Class.forName("net.glxn.qrgen.android.QRCode"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} \ No newline at end of file diff --git a/android/KMEA/app/src/main/res/layout/activity_list_layout.xml b/android/KMEA/app/src/main/res/layout/activity_list_layout.xml index 51c506fc28..fea75a373f 100644 --- a/android/KMEA/app/src/main/res/layout/activity_list_layout.xml +++ b/android/KMEA/app/src/main/res/layout/activity_list_layout.xml @@ -10,4 +10,5 @@ + diff --git a/android/KMEA/app/src/main/res/layout/qr_layout.xml b/android/KMEA/app/src/main/res/layout/qr_layout.xml new file mode 100644 index 0000000000..7310cf6bc9 --- /dev/null +++ b/android/KMEA/app/src/main/res/layout/qr_layout.xml @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/android/KMEA/app/src/main/res/values/dimens.xml b/android/KMEA/app/src/main/res/values/dimens.xml index f05d33ba44..88a4bdd566 100644 --- a/android/KMEA/app/src/main/res/values/dimens.xml +++ b/android/KMEA/app/src/main/res/values/dimens.xml @@ -17,4 +17,5 @@ 12dp 10dp 70dp + 250dp diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index 11f639cd06..35ca6e8697 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -55,6 +55,7 @@ Help link Uninstall keyboard [new] + Scan this code to load this\nkeyboard on another device Getting dictionary catalog.\nThis may take a while… diff --git a/android/KMEA/app/src/main/res/values/styles.xml b/android/KMEA/app/src/main/res/values/styles.xml index 0f99d6dab3..8be0f5a7b3 100644 --- a/android/KMEA/app/src/main/res/values/styles.xml +++ b/android/KMEA/app/src/main/res/values/styles.xml @@ -23,6 +23,15 @@ @android:color/transparent + + + + " + "" "" "" "

%@

" "
" + "
" + "
" + "" + "
Scan this code to load this keyboard on another device or " + "share online
" + "
" "

Keyboard Layouts:


" "%@" "
" @@ -144,8 +153,10 @@ else { fontsStr = [NSMutableString stringWithString:@"


"]; } - + NSString *linkFormat = @"%@"; + NSString *packageId = [self.packagePath lastPathComponent]; + NSString *shareUrl = [NSString stringWithFormat:@"https://keyman.com/go/keyboard/%@/share", packageId]; NSString *name = [[self.infoDict objectForKey:kName] objectAtIndex:0]; if (name == nil) name = @"Unknown Keyboard Package"; @@ -184,7 +195,8 @@ else if (copyrightV2.length) copyright = [NSString stringWithFormat:linkFormat, copyrightV2, copyrightV2]; - NSString *htmlStr = [NSString stringWithFormat:htmlFormat, name, kbsStr, fontsStr, version, author, website, copyright]; + NSString *htmlStr = [NSString stringWithFormat:htmlFormat, name, shareUrl, shareUrl, + kbsStr, fontsStr, version, author, website, copyright]; return htmlStr; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 89ecff808b..fdc9964e6e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -233,6 +233,8 @@ id _lastServerWithOSKShowing = nil; if (_kvk != nil && (_alwaysShowOSK || _lastServerWithOSKShowing == newServer)) { [self showOSK]; } + + _lastServerWithOSKShowing = nil; } CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/qrcode.min.js b/mac/Keyman4MacIM/Keyman4MacIM/qrcode.min.js new file mode 100644 index 0000000000..993e88f396 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/mac/Keyman4MacIM/Podfile.lock b/mac/Keyman4MacIM/Podfile.lock index 818e41a69b..2e9b5d1895 100644 --- a/mac/Keyman4MacIM/Podfile.lock +++ b/mac/Keyman4MacIM/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - Crashlytics (3.13.4): + - Crashlytics (3.14.0): - Fabric (~> 1.10.2) - Fabric (1.10.2) @@ -8,14 +8,14 @@ DEPENDENCIES: - Fabric SPEC REPOS: - https://github.com/cocoapods/specs.git: + trunk: - Crashlytics - Fabric SPEC CHECKSUMS: - Crashlytics: 2dfd686bcb918dc10ee0e76f7f853fe42c7bd552 + Crashlytics: 540b7e5f5da5a042647227a5e3ac51d85eed06df Fabric: 706c8b8098fff96c33c0db69cbf81f9c551d0d74 PODFILE CHECKSUM: f97fe7081827fd61ed7a61b5a0820d4e0df01dee -COCOAPODS: 1.5.3 +COCOAPODS: 1.8.4 diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/KeyView.m b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/KeyView.m index 916118425a..c68d25715b 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/KeyView.m +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/OnScreenKeyboard/KeyView.m @@ -20,6 +20,7 @@ const NSTimeInterval repeatInterval = 0.05f; @property (nonatomic, assign) NSUInteger keyCode; @property (nonatomic, assign) BOOL hasKeyCaption; @property (nonatomic, assign) BOOL isModifierKey; +@property (nonatomic, assign) BOOL isSpecialKey; @property (nonatomic, strong) KeyLabel *label; @property (nonatomic, strong) NSTextField *caption; @property (nonatomic, strong) NSImageView *bitmapView; @@ -52,29 +53,29 @@ const NSTimeInterval repeatInterval = 0.05f; [_label setTextColor:[NSColor blackColor]]; [_label setStringValue:@""]; [self addSubview:_label]; - + bgColor1 = [self getOpaqueColorWithRed:209 green:211 blue:212]; bgColor2 = [self getOpaqueColorWithRed:166 green:169 blue:172]; } - + return self; } - (void)drawRect:(NSRect)rect { [[self getOpaqueColorWithRed:241 green:242 blue:242] setFill]; NSRectFillUsingOperation(rect, NSCompositeSourceOver); - + // Drawing code here. CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort]; CGContextSetLineJoin(context, kCGLineJoinRound); CGContextSetLineWidth(context, lw); CGContextSetStrokeColorWithColor(context, bgColor1.CGColor); - + CGFloat x = rect.origin.x + lw; CGFloat y = rect.origin.y + lw; CGFloat w = rect.size.width - 2*lw; CGFloat h = rect.size.height - 2*lw; - + CGContextBeginPath(context); CGContextMoveToPoint(context, w/2.0, y); CGContextAddArcToPoint(context, w, y, w, y+h, r); @@ -83,7 +84,7 @@ const NSTimeInterval repeatInterval = 0.05f; CGContextAddArcToPoint(context, x, y, w/2.0, y, r); CGContextClosePath(context); CGContextDrawPath(context, kCGPathStroke); - + CGContextBeginPath(context); CGContextMoveToPoint(context, w/2.0, y); CGContextAddArcToPoint(context, w, y, w, y+h, r); @@ -92,13 +93,12 @@ const NSTimeInterval repeatInterval = 0.05f; CGContextAddArcToPoint(context, x, y, w/2.0, y, r); CGContextClosePath(context); CGContextClip(context); - + NSColor *bgColor = bgColor1; - if (self.keyCode >= MVK_CAPS_LOCK && self.keyCode <= MVK_RIGHT_ALT) + + if ([self isSpecialKey]) bgColor = bgColor2; - else if (self.keyCode == MVK_ENTER || self.keyCode == MVK_TAB || self.keyCode == MVK_BACKSPACE) - bgColor = bgColor2; - + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGFloat gradientLocations[] = {0, 1}; NSArray *gradientColors = [NSArray arrayWithObjects:(id)bgColor.CGColor, bgColor.CGColor, nil]; @@ -118,7 +118,10 @@ const NSTimeInterval repeatInterval = 0.05f; _key = key; [self setKeyCode:key.keyCode]; [self setTag:key.keyCode|0x1000]; - [self setLabelText:key.caption]; + if ([self isSpecialKey]) + [self setLabelText:key.caption]; + else + [self setLabelText:@""]; if (key.scale == 1.0) [self setCaptionText:key.caption]; } @@ -131,6 +134,13 @@ const NSTimeInterval repeatInterval = 0.05f; _isModifierKey = YES; else if (keyCode == MVK_LEFT_CTRL || keyCode == MVK_RIGHT_CTRL) _isModifierKey = YES; + + if (keyCode >= MVK_CAPS_LOCK && keyCode <= MVK_RIGHT_ALT) + _isSpecialKey = YES; + else if (self.keyCode == MVK_ENTER || self.keyCode == MVK_TAB || self.keyCode == MVK_BACKSPACE) + _isSpecialKey = YES; + else + _isSpecialKey = NO; } - (void)setLabelText:(NSString *)text { @@ -156,7 +166,7 @@ const NSTimeInterval repeatInterval = 0.05f; - (void)setCaptionFont:(NSString *)fontName { if (_caption == nil) [self setHasKeyCaption:YES]; - + CGFloat fontSize = [_caption.font pointSize]; if (fontName != nil) { NSFont *font = [NSFont fontWithName:fontName size:fontSize]; @@ -270,7 +280,7 @@ const NSTimeInterval repeatInterval = 0.05f; @synchronized(self.target) { //NSLog(@"KeyView TIMER - Fired for key %lu", [self keyCode]); [self processKeyClick]; - + if ([timer timeInterval] == delayBeforeRepeating) { // Fired following a normal (non-modifier key press). As long as user continues to hold // down that key, it will now begin to repeat every 0.05 seconds. All we really want to diff --git a/mac/README.md b/mac/README.md index bbb7bcfa1d..96d001ef86 100644 --- a/mac/README.md +++ b/mac/README.md @@ -23,7 +23,7 @@ test builds. This requires additional configuration for your build environment. be either your 10-digit Team Identifier or a shortname that can be extracted with the following command: - `/Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u '' -p '' -account_type itunes_connect -v off` + `/Applications/Xcode.app/Contents/Developer/usr/bin/iTMSTransporter -m provider -u '' -p '' -account_type itunes_connect -v off` Note: Use your Apple ID for `` and the app-specific password you generated above for ``. You'll use these in the next step as well. diff --git a/mac/build.sh b/mac/build.sh index d0e2650829..3d46c1cb09 100755 --- a/mac/build.sh +++ b/mac/build.sh @@ -21,7 +21,7 @@ display_usage() { echo " form Major.Minor.BuildCounter (optional, but expected if deploy preprelease)" echo " -config NAME NAME is passed to xcodebuild as -configuration parameter. Defaults to Debug, unless" echo " the -deploy option is used, in which configuration will be Release (i.e., -config option" - echo" is ignored)." + echo " is ignored)." echo " -clean Removes all previously-existing build products for anything to be built before building." echo " -test Runs unit tests (not applicable to 'testapp' target)" echo " -no-codesign Disables code-signing for Keyman4MacIM, allowing it to be performed separately later" @@ -42,7 +42,7 @@ display_usage() { echo " already configured in the project." echo " * APPSTORECONNECT_PROVIDER: The shortname of the preferred provider in your Apple Developer account" echo " To find this, run:" - echo " /Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/itms/bin/iTMSTransporter \\" + echo " /Applications/Xcode.app/Contents/Developer/usr/bin/iTMSTransporter \\" echo " -m provider -u 'USERNAME' -p 'PASSWORD' -account_type itunes_connect -v off" echo " * APPSTORECONNECT_USERNAME: Your Apple ID login name." echo " * APPSTORECONNECT_PASSWORD: Your Apple ID password (may need to be a app-specific password)." diff --git a/mac/history.md b/mac/history.md index 3fac0ab694..8ac0cebbc5 100644 --- a/mac/history.md +++ b/mac/history.md @@ -2,6 +2,8 @@ ## 13.0 alpha * Start version 13.0 +* Fix: On Screen Keyboard should not show base letters for blank keycaps (#2414) +* Fix: On Screen Keyboard would sometimes show unexpectedly (#2413) ## 2019-08-12 12.0.7 beta * Fix compatibility issues with macOS 10.14.5 (#1889) diff --git a/web/history.md b/web/history.md index ececec1c82..64c8c505f0 100644 --- a/web/history.md +++ b/web/history.md @@ -3,6 +3,16 @@ ## 13.0 alpha * Start version 13.0 +## 2019-11-28 12.0.105 stable +* Bug Fix: Keyman was not working on old Android browsers (#2358) +* Bug Fix: Firefox on macOS was not being detected correctly (#2383) + +## 2019-11-26 12.0.104 stable +* Updates to build environment (#2364) + +## 2019-11-25 12-0.103 stable +* Updates to build environment only (#2372) + ## 2019-11-13 12.0.102 stable * Fixes issue with mnemonic keyboard handling of backspace and delete keys (#2288) * Fix for iOS Safari's "Request Desktop Website" option disabling touch interactivity (#2283) diff --git a/web/source/build.sh b/web/source/build.sh index 09166c6e3d..d5ddb1630e 100755 --- a/web/source/build.sh +++ b/web/source/build.sh @@ -83,7 +83,14 @@ minifier="$CLOSURECOMPILERPATH/compiler.jar" # `jsDocMissingType` prevents errors on type documentation Closure thinks is missing. TypeScript may not # have the same requirements, and we trust TypeScript over Closure. minifier_warnings="--jscomp_error=* --jscomp_off=lintChecks --jscomp_off=unusedLocalVariables --jscomp_off=globalThis --jscomp_off=checkTypes --jscomp_off=checkVars --jscomp_off=jsdocMissingType --jscomp_off=uselessCode" -minifycmd="$JAVA -jar $minifier --compilation_level WHITESPACE_ONLY $minifier_warnings --generate_exports" + +# We use these to prevent Closure from auto-inserting its own polyfills. Turns out, they can break in the +# WebView used by Android API 19, which our app still supports. +# +# Also, we currently apply all needed polyfills either manually or during TS compilation; we don't need the extra, +# excess code. +minifier_lang_specs="--language_in ECMASCRIPT5 --language_out ECMASCRIPT5" +minifycmd="$JAVA -jar $minifier --compilation_level WHITESPACE_ONLY $minifier_warnings --generate_exports $minifier_lang_specs" if ! [ -f $minifier ]; then diff --git a/web/source/kmwdevice.ts b/web/source/kmwdevice.ts index fce6196117..5588601aa4 100644 --- a/web/source/kmwdevice.ts +++ b/web/source/kmwdevice.ts @@ -1,5 +1,6 @@ // Includes version-related functionality /// +/// // The Device object definition ------------------------------------------------- @@ -13,6 +14,10 @@ namespace com.keyman { version: string; orientation: string|number; browser: string; + colorScheme: 'light' | 'dark'; + + private detected: boolean = false; + private _styles: utils.StyleConstants; // Generates a default Device value. constructor() { @@ -178,6 +183,9 @@ namespace com.keyman { } } } + + this.colorScheme = this.prefersDarkMode() ? 'dark' : 'light'; + this.detected = true; } static _GetIEVersion() { @@ -219,5 +227,27 @@ namespace com.keyman { return 999; } + + /** + * Checks is a user's browser is in dark mode, if the feature is supported. Returns false otherwise. + * + * Thanks to https://stackoverflow.com/a/57795518 for this code. + */ + private prefersDarkMode(): boolean { + // Ensure the detector exists (otherwise, returns false) + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + } + + public get styles(): utils.StyleConstants { + if(!this._styles) { + if(!this.detected) { + this.detect(); + } + + this._styles = new utils.StyleConstants(this); + } + + return this._styles; + } } } \ No newline at end of file diff --git a/web/source/kmwdom.ts b/web/source/kmwdom.ts index ad4c05f533..370e663a98 100644 --- a/web/source/kmwdom.ts +++ b/web/source/kmwdom.ts @@ -1631,6 +1631,10 @@ namespace com.keyman { // Display the OSK (again) if enabled, in order to set its position correctly after // adding the UI to the page this.keyman.osk._Show(); + } else if(this.keyman.isEmbedded) { + // UI modules aren't utilized in embedded mode. There's nothing to init, so we simply + // return instead of waiting for a UI module that will never come. + return; } else { window.setTimeout(this.initializeUI.bind(this),1000); } diff --git a/web/source/kmwnative.ts b/web/source/kmwnative.ts index 66b6a7ac8b..cd3ad45a13 100644 --- a/web/source/kmwnative.ts +++ b/web/source/kmwnative.ts @@ -204,7 +204,8 @@ namespace com.keyman.osk { * @param {number} edge -1 left edge, 1 right edge, else 0 */ VisualKeyboard.prototype.drawPreview = function(this: VisualKeyboard, canvas: HTMLCanvasElement, w: number, h: number, edge: number) { - let device = com.keyman.singleton.util.device; + let util = com.keyman.singleton.util; + let device = util.device; var ctx = canvas.getContext('2d'), dx = (canvas.width - w)/2, hMax = canvas.height, w0 = 0, w1 = dx, w2 = w + dx, w3 = w + 2 * dx, @@ -232,11 +233,9 @@ namespace com.keyman.osk { // Define appearance of preview (cannot be done directly in CSS) if(device.OS == 'Android') { var wx=(w1+w2)/2; - w1 = w2 = wx; - ctx.fillStyle = '#999'; - } else { - ctx.fillStyle = '#ffffff'; - } + w1 = w2 = wx; + } + ctx.fillStyle = device.styles.popupCanvasBackgroundColor; ctx.lineWidth = 1; ctx.strokeStyle = '#cccccc'; diff --git a/web/source/resources/osk/kmwosk.css b/web/source/resources/osk/kmwosk.css index dfbb0a2793..bc36d94fa0 100644 --- a/web/source/resources/osk/kmwosk.css +++ b/web/source/resources/osk/kmwosk.css @@ -64,6 +64,17 @@ .phone.ios .kmw-key.kmw-key-special-on {color:#000;background-color:#fdfdfe;} .phone.ios .kmw-key.kmw-key-touched {background-color:#88f;} +/* Probably best to make this its own CSS that can be optionally included? */ +@media (prefers-color-scheme: dark) { + .phone.ios .kmw-key-layer-group {background-color: #0f1319} + .phone.ios .kmw-key.kmw-key-default {color:#fff;background-color:#3d3d3e} + .phone.ios .kmw-key.kmw-key-shift, + .phone.ios .kmw-key.kmw-key-special {color:#fff;background-color:#595c62;} + .phone.ios .kmw-key.kmw-key-shift-on, + .phone.ios .kmw-key.kmw-key-special-on {color:#fff;background-color:#3d3d3e;} + .phone.ios .kmw-key.kmw-key-touched {background-color:#447;} +} + .ios .kmw-banner-bar { background-color: #cfd3d9; position: absolute; @@ -90,7 +101,28 @@ .phone.ios.kmw-osk-frame, .tablet.ios.kmw-osk-frame { background-color: #cfd3d9; -} +} + +/* Probably best to make this its own CSS that can be optionally included? */ +@media (prefers-color-scheme: dark) { + .ios .kmw-banner-bar { + background-color: #0f1319; + } + + .ios .kmw-banner-bar .kmw-suggest-option { + background-color: #3d3d3e; + border-bottom: solid 1px #8a8d90; + } + + .ios .kmw-suggestion-text { + color: #fff; + } + + .phone.ios.kmw-osk-frame, + .tablet.ios.kmw-osk-frame { + background-color: #0f1319; + } +} .phone.android .kmw-key-layer-group {background-color: #333;} .phone.android .kmw-key {border: none; border-bottom: solid 1px #8a8d90; box-shadow:none; border-radius: 3px;} @@ -155,6 +187,17 @@ .tablet.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;} +/* Probably best to make this its own CSS that can be optionally included? */ +@media (prefers-color-scheme: dark) { + .tablet.ios .kmw-key-layer-group {background-color: #0f1319} + .tablet.ios .kmw-key.kmw-key-default {color:#fff;background-color:#3d3d3e} + .tablet.ios .kmw-key.kmw-key-shift, + .tablet.ios .kmw-key.kmw-key-special {color:#fff;background-color:#595c62;} + .tablet.ios .kmw-key.kmw-key-shift-on, + .tablet.ios .kmw-key.kmw-key-special-on {color:#fff;background-color:#3d3d3e;} + .tablet.ios .kmw-key.kmw-key-touched {background-color:#447;} +} + .tablet .kmw-key-row {-webkit-touch-callout:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:rgba(0,0,0,0);} .tablet.android .kmw-key-layer-group {background-color: #b4b4b8; border-top: 1px solid #666;} @@ -268,19 +311,31 @@ body div.kmw-key-shift-on span.kmw-key-text {font-family:SpecialOSK !important;f #kmw-popup-callout { position:fixed; display:block; background-color: #fdfdfe; border-radius: 0 0 6px 6px; z-index:10001; pointer-events:none; - } +} /* Key preview styles */ div.ios div.kmw-keytip { position:fixed; left:0; top:0; width:3em; height:3em; background-color:rgb(0,0,0,0);overflow:visible; +} + +/* Dark mode - ensure text is colored appropriately for key tips. */ +@media (prefers-color-scheme: dark) { + div.ios div.kmw-keytip { + color:#fff; } + /* Style for callout used on phones */ + #kmw-popup-callout { + background-color: #0f1319; + } +} + div.android div.kmw-keytip { position:fixed; left:0; top:0; width:3em; height:3em; color:#fff; background-color: rgb(0,0,0,0); overflow:visible; - } +} div.android #keytip {background-color:#f00;} @@ -337,6 +392,12 @@ div.android #keytip {background-color:#f00;} .phone.android #kmw-popup-keys {border:none; border-radius: 2px; background-color:#ccc; padding:5px 5px 0 0;} .tablet.android #kmw-popup-keys {border:1px solid #eee; border-radius: 3px; background-color:#888; padding:8px 12px 4px 4px;} +@media (prefers-color-scheme: dark) { + #kmw-popup-keys { + background-color: #0f1319; + } +} + /* Filter (shim) to darken screen and highlight popup keys */ #kmw-popup-shim { position: fixed; width: 100%; height: 100%; bottom: 0; left: 0; @@ -358,6 +419,11 @@ div.android #keytip {background-color:#f00;} .kmw-spacebar-caption{font:0.6em Arial !important;color:rgba(0,0,0,0.15);} +/* Probably best to make this its own CSS that can be optionally included? */ +@media (prefers-color-scheme: dark) { + .kmw-spacebar-caption{color:rgba(255, 255, 255, 0.15);} +} + /* Static styles */ .desktop-static.kmw-osk-inner-frame{margin:0;background-color:#666;border:2px solid #ad4a28;box-sizing:border-box;-moz-box-sizing:border-box;} diff --git a/web/source/text/prediction/modelManager.ts b/web/source/text/prediction/modelManager.ts index 89025ce869..567b272ed6 100644 --- a/web/source/text/prediction/modelManager.ts +++ b/web/source/text/prediction/modelManager.ts @@ -108,9 +108,9 @@ namespace com.keyman.text.prediction { // Establishes KMW's platform 'capabilities', which limit the range of context a LMLayer // model may expect. let capabilities: Capabilities = { - maxLeftContextCodeUnits: 64, + maxLeftContextCodePoints: 64, // Since the apps don't yet support right-deletions. - maxRightContextCodeUnits: keyman.isEmbedded ? 0 : 64 + maxRightContextCodePoints: keyman.isEmbedded ? 0 : 64 } if(!this.canEnable()) { diff --git a/web/source/utils/styleConstants.ts b/web/source/utils/styleConstants.ts new file mode 100644 index 0000000000..6c835fb580 --- /dev/null +++ b/web/source/utils/styleConstants.ts @@ -0,0 +1,24 @@ +// Includes Device definitions, which may play a role in constant logic. +/// + +/* + * This file is intended for CSS-styling constants that see use with the OSK. + */ + +namespace com.keyman.utils { + /** + * Defines device-level constants used for CSS styling. + */ + export class StyleConstants { + constructor(device: Device) { + // popupCanvasBackgroundColor + if(device.OS == 'Android') { + this.popupCanvasBackgroundColor = '#999'; + } else { + this.popupCanvasBackgroundColor = device.colorScheme == 'dark' ? '#0f1319' : '#ffffff'; + } + } + + public readonly popupCanvasBackgroundColor: string; + } +} \ No newline at end of file diff --git a/windows/src/desktop/history.md b/windows/src/desktop/history.md index 8d73e535d1..0b19062949 100644 --- a/windows/src/desktop/history.md +++ b/windows/src/desktop/history.md @@ -2,7 +2,9 @@ ## 13.0 alpha * Start version 13.0 -* Add 64-bit version of kmcomp and kmcmpdll for cross-platform support (#2352) +* Feature: Add QRCode link popup to share an installed package (#2426) +* Feature: Hotkeys are now only triggered with left modifiers, avoiding conflicts with Right Alt (#2360) +* Bug Fix: SMP characters could break input in some applications (#2399) ## 2019-12-01 12.0.59 stable * Bug Fix: Using supplementary plane characters could cause rules to fail to match (#2398) diff --git a/windows/src/desktop/kmshell/xml/config.css b/windows/src/desktop/kmshell/xml/config.css index 19f0b095d7..e23dd6c403 100644 --- a/windows/src/desktop/kmshell/xml/config.css +++ b/windows/src/desktop/kmshell/xml/config.css @@ -170,7 +170,7 @@ html padding: 6px 4px 0 0; float: right; font-size: 12px; - font-weight: normal; + font-weight: normal; } .helplinks a { @@ -388,30 +388,30 @@ table tr } #keyboards .item .list_icon { - float: left; + float: left; margin: 2px 6px 4px 1px; } /* no keyboards installed box */ .info#keyboards { - position: absolute; + position: absolute; top: 25%; } .info#keyboards > div { - position: relative; - left: 0; - top: 0; - font-size: 13.3px; - margin: 0 12px; - border: solid 1px gray; - padding: 8px; + position: relative; + left: 0; + top: 0; + font-size: 13.3px; + margin: 0 12px; + border: solid 1px gray; + padding: 8px; background: #F5F6BE } .info#keyboards > div > img { - float: left; + float: left; margin: 0 8px 0 0; } @@ -555,8 +555,8 @@ table tr font-size: 16px; } #options -{ - left: 0; +{ + left: 0; top: 0; xpadding: 6px 4px 4px 4px; } @@ -597,11 +597,11 @@ table tr overflow: hidden; } -#options_base_keyboard_current { - display: inline-block; +#options_base_keyboard_current { + display: inline-block; padding-top: 5px; } - + /* Hotkey Styles */ #subcontent_hotkeys .list_item @@ -940,16 +940,16 @@ th } .keyboard-language-title { - color: #444444; - float:right; - padding-top: 5px; + color: #444444; + float:right; + padding-top: 5px; margin-right: 8px; text-decoration: none; } .keyboard-language-title .language-icon { - padding-top: 0px; - margin-top: -2px; + padding-top: 0px; + margin-top: -2px; margin-left: 8px; visibility: visible; } @@ -965,7 +965,7 @@ th padding-left: 8px; } -.keyboard-language-title:hover, +.keyboard-language-title:hover, .keyboard-language-title:active { text-decoration: underline; cursor: pointer; @@ -988,7 +988,7 @@ th } #keepintouch_content { - height: 100%; + height: 100%; overflow: hidden; } @@ -997,4 +997,44 @@ th width:100%; height:100%; border:none; +} + +/* QRCodes */ + +.qrcode .qrcode-back { + position: fixed; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(64,64,64,0.5); + z-index: 100; + display: none; +} + +.qrcode.qrcode-visible .qrcode-popup, +.qrcode.qrcode-visible .qrcode-back { + display: block; +} + +.qrcode .qrcode-popup { + position: fixed; + display: none; + width: 256px; + height: 324px; + margin: -180px 0 0 -128px; + left: 50%; + top: 50%; + border: solid 1px black; + background: white; + text-align: center; + z-index: 101; + padding: 10px; +} + +.qrcode .qrcode-caption { + display: block; + white-space: normal; + font-size: 12pt; + padding: 8px; } \ No newline at end of file diff --git a/windows/src/desktop/kmshell/xml/config.js b/windows/src/desktop/kmshell/xml/config.js index 06765cb713..9a6c4e2d87 100644 --- a/windows/src/desktop/kmshell/xml/config.js +++ b/windows/src/desktop/kmshell/xml/config.js @@ -285,3 +285,15 @@ function keyboard_checkclick(n,toggle) { li.focus(); return true; } + +/* QRCode popup */ + +function showKeyboardLink(id) { + var e = document.getElementById('qrcode-'+id); + e.className='qrcode qrcode-visible'; +} + +function hideKeyboardLink(id) { + var e = document.getElementById('qrcode-'+id); + e.className='qrcode'; +} \ No newline at end of file diff --git a/windows/src/desktop/kmshell/xml/keyman.xsl b/windows/src/desktop/kmshell/xml/keyman.xsl index e27139a12c..eea960d338 100644 --- a/windows/src/desktop/kmshell/xml/keyman.xsl +++ b/windows/src/desktop/kmshell/xml/keyman.xsl @@ -29,6 +29,7 @@ + diff --git a/windows/src/desktop/kmshell/xml/keyman_keyboardlist.xsl b/windows/src/desktop/kmshell/xml/keyman_keyboardlist.xsl index 368ddb8b19..eef5085982 100644 --- a/windows/src/desktop/kmshell/xml/keyman_keyboardlist.xsl +++ b/windows/src/desktop/kmshell/xml/keyman_keyboardlist.xsl @@ -275,6 +275,45 @@ + + + + + + + + javascript:showKeyboardLink('') + + +
+ qrcode- +
+ return hideKeyboardLink('') +
+
+
+ qrcode-img- +
+ + +
+
+ + +
+
 
diff --git a/windows/src/desktop/kmshell/xml/locale.xml b/windows/src/desktop/kmshell/xml/locale.xml index 9db433dc3a..0c2d02c19a 100644 --- a/windows/src/desktop/kmshell/xml/locale.xml +++ b/windows/src/desktop/kmshell/xml/locale.xml @@ -126,6 +126,11 @@ Create a user interface translation for Keyman Desktop at http://www.tavultesoft You do not have any keyboards installed. Click the Download Keyboard button to install a keyboard layout from the Keyman website. You can only uninstall the keyboard '%0:s' if you are an Administrator +Share keyboard +Scan this code to load this keyboard on another device or +share online + + General diff --git a/windows/src/desktop/kmshell/xml/qrcode.min.js b/windows/src/desktop/kmshell/xml/qrcode.min.js new file mode 100644 index 0000000000..993e88f396 --- /dev/null +++ b/windows/src/desktop/kmshell/xml/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/windows/src/developer/Makefile b/windows/src/developer/Makefile index 305867d869..cd56b60e4b 100644 --- a/windows/src/developer/Makefile +++ b/windows/src/developer/Makefile @@ -3,7 +3,7 @@ # -TARGETS=kmcmpdll kmcomp kmanalyze kmconvert tike samples setup inst +TARGETS=kmcmpdll kmcomp kmanalyze kmconvert tike samples setup inst kmdecomp MANIFESTS=kmcomp tike setup CLEANS=clean-developer @@ -19,6 +19,10 @@ kmcmpdll: cd $(ROOT)\src\developer\kmcmpdll $(MAKE) $(TARGET) +kmdecomp: + cd $(ROOT)\src\developer\kmdecomp + $(MAKE) $(TARGET) + kmcomp: kmcmpdll cd $(ROOT)\src\developer\kmcomp $(MAKE) $(TARGET) diff --git a/windows/src/developer/TIKE/Tike.dpr b/windows/src/developer/TIKE/Tike.dpr index 0a1babc728..03dc9a224c 100644 --- a/windows/src/developer/TIKE/Tike.dpr +++ b/windows/src/developer/TIKE/Tike.dpr @@ -283,10 +283,17 @@ uses Keyman.Developer.System.ProjectTemplate in '..\kmconvert\Keyman.Developer.System.ProjectTemplate.pas', Keyman.Developer.UI.Project.UfrmNewModelProjectParameters in 'project\Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas' {frmNewModelProjectParameters}, Keyman.Developer.System.Project.wordlistTsvProjectFile in 'project\Keyman.Developer.System.Project.wordlistTsvProjectFile.pas', - Keyman.Developer.UI.UfrmWordlistEditor in 'child\Keyman.Developer.UI.UfrmWordlistEditor.pas' {frmWordlistEditor}, + Keyman.Developer.UI.UframeWordlistEditor in 'child\Keyman.Developer.UI.UframeWordlistEditor.pas' {frameWordlistEditor}, Keyman.System.WordlistTsvFile in '..\..\global\delphi\lexicalmodels\Keyman.System.WordlistTsvFile.pas', Keyman.Developer.UI.Project.wordlistTsvProjectFileUI in 'project\Keyman.Developer.UI.Project.wordlistTsvProjectFileUI.pas', - Keyman.Developer.System.Project.WelcomeRenderer in 'project\Keyman.Developer.System.Project.WelcomeRenderer.pas'; + Keyman.Developer.System.Project.WelcomeRenderer in 'project\Keyman.Developer.System.Project.WelcomeRenderer.pas', + DelphiZXIngQRCode in '..\..\ext\zxingqrcode\Source\DelphiZXIngQRCode.pas', + Keyman.System.QRCode in '..\..\global\delphi\general\Keyman.System.QRCode.pas', + Keyman.Developer.UI.UfrmModelEditor in 'child\Keyman.Developer.UI.UfrmModelEditor.pas' {frmModelEditor}, + Keyman.Developer.System.LexicalModelParser in 'main\Keyman.Developer.System.LexicalModelParser.pas', + Keyman.Developer.System.LexicalModelParserTypes in 'main\Keyman.Developer.System.LexicalModelParserTypes.pas', + Keyman.Developer.UI.UfrmWordlistEditor in 'child\Keyman.Developer.UI.UfrmWordlistEditor.pas' {frmWordlistEditor}, + Keyman.Developer.UI.dmActionsModelEditor in 'actions\Keyman.Developer.UI.dmActionsModelEditor.pas' {modActionsModelEditor: TDataModule}; {$R *.RES} {$R ICONS.RES} @@ -314,8 +321,8 @@ begin if TikeActive then Exit; InitClasses; Application.CreateForm(TmodWebHttpServer, modWebHttpServer); - Application.CreateForm(TfrmKeymanDeveloper, frmKeymanDeveloper); - Application.Run; + Application.CreateForm(TfrmKeymanDeveloper, frmKeymanDeveloper); + Application.Run; end; finally FInitializeCEF.Free; diff --git a/windows/src/developer/TIKE/Tike.dproj b/windows/src/developer/TIKE/Tike.dproj index c355614097..77b8f26bd7 100644 --- a/windows/src/developer/TIKE/Tike.dproj +++ b/windows/src/developer/TIKE/Tike.dproj @@ -529,13 +529,30 @@ dfm - -
frmWordlistEditor
+ +
frameWordlistEditor
dfm
+ + + +
frmModelEditor
+ dfm +
+ + + +
frmWordlistEditor
+ dfm +
+ +
modActionsModelEditor
+ dfm + TDataModule +
Cfg_2 diff --git a/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm new file mode 100644 index 0000000000..086f56fe7c --- /dev/null +++ b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.dfm @@ -0,0 +1,29 @@ +object modActionsModelEditor: TmodActionsModelEditor + OldCreateOrder = False + Height = 150 + Width = 215 + object actionsModelEditor: TActionList + Images = frmKeymanDeveloper.lstImages + Left = 60 + Top = 24 + object actModelCompile: TAction + Category = 'Lexical Model' + Caption = '&Compile Model' + ImageIndex = 37 + ShortCut = 118 + OnExecute = actModelCompileExecute + OnUpdate = actModelCompileUpdate + end + object actModelIncludeDebugInformation: TAction + Category = 'Lexical Model' + Caption = 'Include &Debug Information' + OnExecute = actModelIncludeDebugInformationExecute + end + object actModelTest: TAction + Category = 'Lexical Model' + Caption = 'Test &Lexical Model' + OnExecute = actModelTestExecute + OnUpdate = actModelTestUpdate + end + end +end diff --git a/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas new file mode 100644 index 0000000000..594e25cde3 --- /dev/null +++ b/windows/src/developer/TIKE/actions/Keyman.Developer.UI.dmActionsModelEditor.pas @@ -0,0 +1,86 @@ +unit Keyman.Developer.UI.dmActionsModelEditor; + +interface + +uses + System.SysUtils, System.Classes, System.Actions, Vcl.ActnList; + +type + TmodActionsModelEditor = class(TDataModule) + actionsModelEditor: TActionList; + actModelCompile: TAction; + actModelTest: TAction; + actModelIncludeDebugInformation: TAction; + procedure actModelCompileExecute(Sender: TObject); + procedure actModelIncludeDebugInformationExecute(Sender: TObject); + procedure actModelTestExecute(Sender: TObject); + procedure actModelCompileUpdate(Sender: TObject); + procedure actModelTestUpdate(Sender: TObject); + private + { Private declarations } + public + { Public declarations } + end; + +var + modActionsModelEditor: TmodActionsModelEditor; + +implementation + +{%CLASSGROUP 'Vcl.Controls.TControl'} + +uses + UfrmMain, + UfrmMessages, + Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.UI.Project.ProjectFileUI, + Keyman.Developer.System.Project.modelTsProjectFile, + Keyman.Developer.UI.UfrmModelEditor; + +{$R *.dfm} + +function ActiveModelEditor: TfrmModelEditor; +begin + if Assigned(frmKeymanDeveloper.ActiveChild) and (frmKeymanDeveloper.ActiveChild is TfrmModelEditor) + then Result := frmKeymanDeveloper.ActiveChild as TfrmModelEditor + else Result := nil; +end; + +function ActiveModelProjectFile: TmodelTsProjectFile; +begin + Result := ActiveModelEditor.ProjectFile as TmodelTsProjectFile; +end; + +procedure TmodActionsModelEditor.actModelCompileExecute(Sender: TObject); +begin + frmMessages.Clear; // I4686 + + if ActiveModelEditor <> nil then + begin + (ActiveModelProjectFile.UI as TProjectFileUI).DoAction(pfaCompile, False); + end; +end; + +procedure TmodActionsModelEditor.actModelCompileUpdate(Sender: TObject); +begin + actModelCompile.Enabled := ActiveModelEditor <> nil; + frmKeymanDeveloper.mnuModel.Visible := actModelCompile.Enabled; +end; + +procedure TmodActionsModelEditor.actModelIncludeDebugInformationExecute( + Sender: TObject); +begin +// TODO: (ActiveModelProjectFile.UI as TmodeltsProjectFileUI).Debug := not (ActiveModelProjectFile.UI as TmodeltsProjectFileUI).Debug; // I4687 +end; + +procedure TmodActionsModelEditor.actModelTestExecute(Sender: TObject); +begin + (ActiveModelProjectFile.UI as TProjectFileUI).DoAction(pfaTestKeymanWeb, False); // I4687 +end; + +procedure TmodActionsModelEditor.actModelTestUpdate(Sender: TObject); +begin + actModelTest.Enabled := ActiveModelEditor <> nil; +end; + +end. diff --git a/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.dfm b/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.dfm index 15096c36e3..9cb7da5f24 100644 --- a/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.dfm +++ b/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.dfm @@ -23,7 +23,7 @@ object modActionsKeyboardEditor: TmodActionsKeyboardEditor end object actKeyboardCreatePackage: TAction Category = 'Keyboard' - Caption = '&Package for Distribution...' + Caption = 'Package for Distribution...' ImageIndex = 40 OnExecute = actKeyboardCreatePackageExecute end diff --git a/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.pas b/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.pas index a5a8c32d68..5f61ee1a87 100644 --- a/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.pas +++ b/windows/src/developer/TIKE/actions/dmActionsKeyboardEditor.pas @@ -146,6 +146,7 @@ uses Keyman.Developer.System.Project.kpsProjectFile, Keyman.Developer.System.Project.ProjectFile, Keyman.Developer.UI.Project.ProjectFileUI, + Keyman.Developer.UI.Project.UfrmProject, UframeTextEditor, UfrmKeymanWizard, UfrmDebug, @@ -441,12 +442,20 @@ begin else if ActivePackageEditor <> nil then begin (ActivePackageProjectFile.UI as TProjectFileUI).DoAction(pfaCompile, False); - end; + end + else if frmKeymanDeveloper.ActiveChild is TfrmProject then + (frmKeymanDeveloper.ActiveChild as TfrmProject).CompileAll; end; procedure TmodActionsKeyboardEditor.actKeyboardCompileUpdate(Sender: TObject); begin - actKeyboardCompile.Enabled := (ActiveEditor <> nil) or (ActivePackageEditor <> nil); + // TODO: Split Keyboard menu and package editor functions + actKeyboardCompile.Enabled := + (ActiveEditor <> nil) or + (ActivePackageEditor <> nil) or + (frmKeymanDeveloper.ActiveChild is TfrmProject); + frmKeymanDeveloper.mnuKeyboard.Visible := actKeyboardCompile.Enabled; + frmKeymanDeveloper.mnuDebug.Visible := ActiveEditor <> nil; end; procedure TmodActionsKeyboardEditor.actKeyboardCreatePackageExecute(Sender: TObject); diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm new file mode 100644 index 0000000000..7cea795c20 --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.dfm @@ -0,0 +1,87 @@ +inherited frameWordlistEditor: TframeWordlistEditor + BorderIcons = [] + BorderStyle = bsNone + Caption = 'frameWordlistEditor' + ClientHeight = 338 + ClientWidth = 651 + OnResize = FormResize + ExplicitWidth = 651 + ExplicitHeight = 338 + PixelsPerInch = 96 + TextHeight = 13 + object pages: TPageControl + Left = 0 + Top = 0 + Width = 651 + Height = 338 + ActivePage = pageDesign + Align = alClient + TabOrder = 0 + TabPosition = tpBottom + OnChanging = pagesChanging + ExplicitWidth = 635 + ExplicitHeight = 299 + object pageDesign: TTabSheet + Caption = 'Design' + ImageIndex = -1 + ExplicitLeft = 0 + ExplicitTop = 0 + ExplicitWidth = 627 + ExplicitHeight = 273 + object gridWordlist: TStringGridEditControlled + Left = 0 + Top = 0 + Width = 643 + Height = 271 + Align = alClient + ColCount = 3 + DefaultRowHeight = 16 + DefaultDrawing = False + FixedCols = 0 + Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goEditing, goTabs, goAlwaysShowEditor] + TabOrder = 0 + OnClick = gridWordlistClick + OnDrawCell = gridWordlistDrawCell + OnSetEditText = gridWordlistSetEditText + ExplicitWidth = 627 + ExplicitHeight = 232 + end + object panGridControls: TPanel + Left = 0 + Top = 271 + Width = 643 + Height = 41 + Align = alBottom + TabOrder = 1 + ExplicitTop = 232 + ExplicitWidth = 627 + object cmdDeleteRow: TButton + Left = 0 + Top = 6 + Width = 75 + Height = 25 + Caption = '&Delete row' + TabOrder = 0 + OnClick = cmdDeleteRowClick + end + object cmdSortByFrequency: TButton + Left = 81 + Top = 6 + Width = 104 + Height = 25 + Caption = '&Sort by frequency' + TabOrder = 1 + OnClick = cmdSortByFrequencyClick + end + end + end + object pageCode: TTabSheet + Caption = 'Code' + ImageIndex = -1 + ExplicitLeft = 0 + ExplicitTop = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 + end + end +end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas new file mode 100644 index 0000000000..c8739c0e59 --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas @@ -0,0 +1,340 @@ +unit Keyman.Developer.UI.UframeWordlistEditor; + +interface + +uses + System.Classes, + System.SysUtils, + System.Variants, + Winapi.Messages, + Winapi.Windows, + Vcl.ComCtrls, + Vcl.Controls, + Vcl.Dialogs, + Vcl.Forms, + Vcl.Graphics, + Vcl.Grids, + + StringGridEditControlled, + + Keyman.System.WordlistTsvFile, + UframeTextEditor, + UfrmMDIEditor, Vcl.StdCtrls, Vcl.ExtCtrls, UfrmTike; + +type + TframeWordlistEditor = class(TTIKEForm) + pages: TPageControl; + pageDesign: TTabSheet; + pageCode: TTabSheet; + gridWordlist: TStringGridEditControlled; + panGridControls: TPanel; + cmdDeleteRow: TButton; + cmdSortByFrequency: TButton; + procedure pagesChanging(Sender: TObject; var AllowChange: Boolean); + procedure FormCreate(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure FormResize(Sender: TObject); + procedure gridWordlistSetEditText(Sender: TObject; ACol, ARow: Integer; + const Value: string); + procedure gridWordlistDrawCell(Sender: TObject; ACol, ARow: Integer; + ARect: TRect; State: TGridDrawState); + procedure cmdDeleteRowClick(Sender: TObject); + procedure gridWordlistClick(Sender: TObject); + procedure cmdSortByFrequencyClick(Sender: TObject); + private + FWordlist: TWordlistTsvFile; + frameSource: TframeTextEditor; + FSetup: Integer; + FModified: Boolean; + FOnModifiedChanged: TNotifyEvent; + FFilename: string; + procedure UpdateData; + function MoveCodeToWordlist: Boolean; + procedure FillCode; + procedure FillGrid; + function MoveDesignToWordlist: Boolean; + procedure SourceChanged(Sender: TObject); + procedure FillGridNewRow; + procedure EnableControls; + procedure SetModified(const Value: Boolean); + protected + function GetHelpTopic: string; override; + function DoOpenFile: Boolean; + function DoSaveFile: Boolean; + public + procedure FindError(const Filename: string; s: string; line: Integer); // I4081 + function LoadFromFile(const Filename: string): Boolean; + function SaveToFile(const Filename: string): Boolean; + property Filename: string read FFilename; + property Modified: Boolean read FModified write SetModified; + property OnModifiedChanged: TNotifyEvent read FOnModifiedChanged write FOnModifiedChanged; + end; + +implementation + +{$R *.dfm} + +uses + Keyman.Developer.System.HelpTopics, + TextFileFormat, + UfrmMain, + UfrmMessages; + +const + ColWidth_Word = 120; + ColWidth_Frequency = 64; + S_AddRowText = 'Add word...'; + +{ TfrmWordlistEditor } + +procedure TframeWordlistEditor.FormCreate(Sender: TObject); +begin + inherited; + Inc(FSetup); + try + FWordlist := TWordlistTsvFile.Create; + frameSource := TframeTextEditor.Create(Self); + frameSource.Parent := pageCode; + frameSource.Align := alClient; + frameSource.EditorFormat := efWordlistTsv; + frameSource.Visible := True; + frameSource.OnChanged := SourceChanged; + frameSource.TextFileFormat := tffUTF8; +//TODO: frameSource.UseTabs := True; + pages.ActivePage := pageDesign; + + gridWordlist.Cells[0, 0] := 'Word Form'; + gridWordlist.Cells[1, 0] := 'Count'; + gridWordlist.Cells[2, 0] := 'Comment'; + gridWordlist.ColWidths[0] := ColWidth_Word; + gridWordlist.ColWidths[1] := ColWidth_Frequency; + gridWordlist.ColWidths[2] := gridWordlist.ClientWidth - ColWidth_Word - ColWidth_Frequency - 2; + finally + Dec(FSetup); + end; +end; + +procedure TframeWordlistEditor.FormDestroy(Sender: TObject); +begin + inherited; + FWordlist.Free; +end; + +procedure TframeWordlistEditor.FormResize(Sender: TObject); +begin + inherited; + gridWordlist.ColWidths[2] := gridWordlist.ClientWidth - ColWidth_Word - ColWidth_Frequency - 2; +end; + +procedure TframeWordlistEditor.cmdDeleteRowClick(Sender: TObject); +begin + FWordlist.RemoveWord(gridWordlist.Row-1); + FillGrid; + EnableControls; + Modified := True; +end; + +procedure TframeWordlistEditor.cmdSortByFrequencyClick(Sender: TObject); +begin + FWordlist.SortByFrequency; + FillGrid; + Modified := True; +end; + +function TframeWordlistEditor.DoOpenFile: Boolean; +begin + if FileExists(FileName) then + begin + FWordlist.LoadFromFile(FileName); + end; + UpdateData; + Result := True; +end; + +function TframeWordlistEditor.DoSaveFile: Boolean; +begin + MoveCodeToWordlist; // Does nothing if not in code page + FWordlist.SaveToFile(FileName); + Result := True; +end; + +procedure TframeWordlistEditor.EnableControls; +var + e: Boolean; +begin + e := gridWordlist.Row < gridWordlist.RowCount - 1; + cmdDeleteRow.Enabled := e; + cmdSortByFrequency.Enabled := e; +end; + +procedure TframeWordlistEditor.FindError(const Filename: string; s: string; line: Integer); +begin + // +end; + +function TframeWordlistEditor.GetHelpTopic: string; +begin + Result := SHelpTopic_Context_WordlistEditor; +end; + +procedure TframeWordlistEditor.gridWordlistClick(Sender: TObject); +begin + EnableControls; +end; + +procedure TframeWordlistEditor.gridWordlistDrawCell(Sender: TObject; ACol, + ARow: Integer; ARect: TRect; State: TGridDrawState); +var + LText: string; +begin + inherited; + if ARow = gridWordlist.RowCount - 1 then + begin + // Drawing last row + gridWordlist.Canvas.Font.Style := [fsItalic]; + end + else if gridWordlist.Cells[0, ARow] = '#' then + begin + // Drawing a comment cell + gridWordlist.Canvas.Font.Style := [fsBold]; + end + else + gridWordlist.Canvas.Font.Style := []; + + LText := gridWordlist.Cells[ACol, ARow]; + gridWordlist.Canvas.TextRect(ARect, ARect.Left+2, + ARect.Top+((ARect.Height - gridWordlist.Canvas.TextHeight(LText)) div 2), LText); +end; + +procedure TframeWordlistEditor.gridWordlistSetEditText(Sender: TObject; ACol, + ARow: Integer; const Value: string); +var + w: TWordlistWord; +begin + if FSetup > 0 then Exit; + Inc(FSetup); + try + Modified := True; + + if ARow = gridWordlist.RowCount - 1 then + begin + if (Value = '') or (Value = S_AddRowText) then + Exit; + + w.Word := Value; + w.Frequency := 0; + w.Comment := ''; + FWordlist.AddWord(w); + gridWordlist.RowCount := gridWordlist.RowCount + 1; + FillGridNewRow; + end + else + begin + w := FWordlist.Word[ARow-1]; + case ACol of + 0: w.Word := Value; + 1: w.Frequency := StrToIntDef(Value, 0); + 2: w.Comment := Value; + end; + FWordlist.Word[ARow-1] := w; + end; + finally + Dec(FSetup); + end; +end; + +procedure TframeWordlistEditor.pagesChanging(Sender: TObject; + var AllowChange: Boolean); +begin + if FSetup > 0 then Exit; + + if pages.ActivePage = pageCode then + AllowChange := MoveCodeToWordlist + else + AllowChange := MoveDesignToWordlist +end; + +function TframeWordlistEditor.MoveCodeToWordlist: Boolean; +begin + if pages.ActivePage <> pageCode then + Exit(False); + + FWordlist.Text := frameSource.EditorText; + FillGrid; + Result := True; +end; + +function TframeWordlistEditor.MoveDesignToWordlist: Boolean; +begin + FillCode; + Result := True; +end; + +function TframeWordlistEditor.LoadFromFile(const Filename: string): Boolean; +begin + FFilename := Filename; + Result := DoOpenFile; +end; + +procedure TframeWordlistEditor.UpdateData; +begin + if pages.ActivePage = pageCode + then FillCode + else FillGrid; +end; + +function TframeWordlistEditor.SaveToFile(const Filename: string): Boolean; +begin + FFilename := Filename; + Result := DoSaveFile; +end; + +procedure TframeWordlistEditor.SetModified(const Value: Boolean); +begin + if FModified <> Value then + begin + FModified := Value; + if Assigned(FOnModifiedChanged) then + FOnModifiedChanged(Self); + end; +end; + +procedure TframeWordlistEditor.SourceChanged(Sender: TObject); +begin + if FSetup = 0 then + Modified := True; +end; + +procedure TframeWordlistEditor.FillCode; +begin + frameSource.EditorText := FWordlist.Text; +end; + +procedure TframeWordlistEditor.FillGrid; +var + i: Integer; + w: TWordlistWord; +begin + gridWordlist.RowCount := FWordlist.Count + 2; + for i := 0 to FWordlist.Count - 1 do + begin + w := FWordlist.Word[i]; + gridWordlist.Cells[0, i+1] := w.Word; + if w.Frequency = 0 + then gridWordlist.Cells[1, i+1] := '' + else gridWordlist.Cells[1, i+1] := IntToStr(w.Frequency); + gridWordlist.Cells[2, i+1] := w.Comment; + end; + FillGridNewRow; + gridWordlist.FixedRows := 1; +end; + +procedure TframeWordlistEditor.FillGridNewRow; +begin + gridWordlist.Cells[0, gridWordlist.RowCount - 1] := S_AddRowText; + gridWordlist.Cells[1, gridWordlist.RowCount - 1] := ''; + gridWordlist.Cells[2, gridWordlist.RowCount - 1] := ''; + EnableControls; +end; + +end. diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm new file mode 100644 index 0000000000..82d20a01c9 --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm @@ -0,0 +1,419 @@ +inherited frmModelEditor: TfrmModelEditor + Left = 0 + Top = 0 + Caption = 'frmModelEditor' + ClientHeight = 708 + ClientWidth = 712 + Font.Name = 'Tahoma' + ExplicitWidth = 712 + ExplicitHeight = 708 + PixelsPerInch = 96 + TextHeight = 13 + object pages: TLeftTabbedPageControl + Left = 0 + Top = 0 + Width = 712 + Height = 708 + ActivePage = pageCompile + Align = alClient + Font.Charset = ANSI_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Segoe UI' + Font.Style = [] + Images = modActionsMain.ilEditorPages + MultiLine = True + OwnerDraw = True + ParentFont = False + TabHeight = 90 + TabOrder = 0 + TabPosition = tpLeft + TabWidth = 60 + OnChanging = pagesChanging + object pageDetails: TTabSheet + Caption = 'Details' + ImageIndex = 2 + object sbDetails: TScrollBox + Left = 0 + Top = 0 + Width = 619 + Height = 708 + Align = alClient + BevelInner = bvNone + BevelOuter = bvNone + BorderStyle = bsNone + Color = 14211288 + ParentColor = False + TabOrder = 0 + object panWordlists: TPanel + AlignWithMargins = True + Left = 4 + Top = 181 + Width = 611 + Height = 232 + Margins.Left = 4 + Margins.Top = 4 + Margins.Right = 4 + Margins.Bottom = 4 + Align = alTop + BevelOuter = bvNone + TabOrder = 1 + DesignSize = ( + 611 + 232) + object lblWordlists: TLabel + AlignWithMargins = True + Left = 12 + Top = 4 + Width = 64 + Height = 17 + Caption = 'Wordlists' + Font.Charset = ANSI_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object lblReadOnly: TLabel + Left = 15 + Top = 200 + Width = 415 + Height = 13 + Caption = + 'The editor was unable to parse the source file. Details shown he' + + 're are read-only.' + end + object gridWordlists: TStringGrid + Left = 12 + Top = 27 + Width = 485 + Height = 160 + Anchors = [akLeft, akTop, akRight] + ColCount = 1 + FixedCols = 0 + FixedRows = 0 + Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRowSelect] + TabOrder = 0 + OnDblClick = gridWordlistsDblClick + ColWidths = ( + 271) + RowHeights = ( + 24 + 24 + 24 + 24 + 24) + end + object cmdAddWordlist: TButton + Left = 503 + Top = 27 + Width = 101 + Height = 25 + Anchors = [akTop, akRight] + Caption = '&Add...' + TabOrder = 1 + OnClick = cmdAddWordlistClick + end + object cmdRemoveWordlist: TButton + Left = 503 + Top = 58 + Width = 100 + Height = 25 + Anchors = [akTop, akRight] + Caption = 'Remove' + TabOrder = 2 + OnClick = cmdRemoveWordlistClick + end + end + object panBasicInformation: TPanel + AlignWithMargins = True + Left = 4 + Top = 6 + Width = 611 + Height = 167 + Margins.Left = 4 + Margins.Top = 6 + Margins.Right = 4 + Margins.Bottom = 4 + Align = alTop + AutoSize = True + BevelOuter = bvNone + ParentColor = True + TabOrder = 0 + DesignSize = ( + 611 + 167) + object lblFormat: TLabel + Left = 12 + Top = 30 + Width = 36 + Height = 13 + Caption = '&Format' + end + object lblBasicInformation: TLabel + AlignWithMargins = True + Left = 12 + Top = 3 + Width = 123 + Height = 17 + Caption = 'Basic Information' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object lblWordBreaker: TLabel + Left = 12 + Top = 56 + Width = 71 + Height = 13 + Caption = '&Word breaker' + end + object lblComments: TLabel + Left = 12 + Top = 83 + Width = 50 + Height = 13 + Caption = 'Comme&nts' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + ParentFont = False + end + object cbFormat: TComboBox + Left = 98 + Top = 26 + Width = 145 + Height = 21 + Style = csDropDownList + TabOrder = 0 + OnClick = cbFormatClick + Items.Strings = ( + 'Wordlist (trie-1.0)' + 'Custom (custom-1.0)') + end + object cbWordBreaker: TComboBox + Left = 98 + Top = 53 + Width = 145 + Height = 21 + Style = csDropDownList + TabOrder = 1 + OnClick = cbWordBreakerClick + Items.Strings = ( + 'default' + 'ascii' + 'custom') + end + object memoComments: TMemo + Left = 98 + Top = 80 + Width = 506 + Height = 87 + Anchors = [akLeft, akTop, akRight] + BevelOuter = bvNone + TabOrder = 2 + OnChange = memoCommentsChange + end + end + end + end + object pageSource: TTabSheet + Caption = 'Source' + ImageIndex = 9 + end + object pageCompile: TTabSheet + Caption = 'Build' + ImageIndex = 1 + object Panel1: TPanel + Left = 0 + Top = 0 + Width = 619 + Height = 708 + Align = alClient + BevelOuter = bvNone + Color = 14211288 + ParentBackground = False + TabOrder = 0 + object lblCongrats: TLabel + Left = 10 + Top = 13 + Width = 333 + Height = 13 + Caption = + 'The keyboard must be compiled in order to distribute or install ' + + 'it' + end + object Label5: TLabel + Left = 10 + Top = 79 + Width = 82 + Height = 13 + Caption = 'Target filename:' + end + object cmdCompile: TButton + Left = 10 + Top = 40 + Width = 137 + Height = 25 + Action = modActionsModelEditor.actModelCompile + TabOrder = 0 + end + object cmdAddToProject: TButton + Left = 297 + Top = 40 + Width = 137 + Height = 25 + Action = modActionsMain.actProjectAddCurrentEditorFile + TabOrder = 1 + end + object cmdOpenContainingFolder2: TButton + Left = 153 + Top = 40 + Width = 138 + Height = 25 + Caption = '&Open Containing Folder' + TabOrder = 2 + OnClick = cmdOpenContainingFolder2Click + end + object panBuildLexicalModel: TPanel + Left = 10 + Top = 112 + Width = 471 + Height = 353 + BevelOuter = bvNone + Color = 15921906 + ParentBackground = False + TabOrder = 3 + object lblDebugHostCaption: TLabel + Left = 12 + Top = 158 + Width = 252 + Height = 13 + Caption = 'Debug host is active at following web addresses:' + end + object lblCrossPlatform: TLabel + Left = 12 + Top = 14 + Width = 237 + Height = 17 + Caption = 'Test Lexical Model in Web Browser' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object Label1: TLabel + Left = 12 + Top = 48 + Width = 105 + Height = 13 + Caption = 'Keyboard for testing' + end + object Label2: TLabel + Left = 12 + Top = 72 + Width = 389 + Height = 26 + Caption = + 'Optionally, select a compiled keyboard with which to test this l' + + 'exical model. Any keyboards already loaded in the web debugger w' + + 'ill also be available.' + WordWrap = True + end + object imgQRCode: TImage + Left = 309 + Top = 152 + Width = 128 + Height = 128 + Proportional = True + Stretch = True + end + object cmdTestLexicalModel: TButton + Left = 12 + Top = 122 + Width = 150 + Height = 25 + Action = modActionsModelEditor.actModelTest + TabOrder = 2 + end + object cmdOpenDebugHost: TButton + Left = 12 + Top = 286 + Width = 189 + Height = 25 + Caption = 'Open &debugger in local browser' + TabOrder = 3 + OnClick = cmdOpenDebugHostClick + end + object lbDebugHosts: TListBox + Left = 12 + Top = 183 + Width = 289 + Height = 97 + ItemHeight = 13 + TabOrder = 4 + OnClick = lbDebugHostsClick + end + object cmdSendURLsToEmail: TButton + Left = 12 + Top = 317 + Width = 189 + Height = 25 + Caption = 'Send addresses to &email...' + TabOrder = 5 + OnClick = cmdSendURLsToEmailClick + end + object editTestKeyboard: TEdit + Left = 123 + Top = 45 + Width = 158 + Height = 21 + TabOrder = 0 + OnChange = editTestKeyboardChange + end + object cmdBrowseTestKeyboard: TButton + Left = 287 + Top = 43 + Width = 75 + Height = 25 + Caption = '&Browse...' + TabOrder = 1 + OnClick = cmdBrowseTestKeyboardClick + end + end + object editOutPath: TEdit + Left = 98 + Top = 76 + Width = 383 + Height = 21 + TabStop = False + ParentColor = True + ReadOnly = True + TabOrder = 4 + end + end + end + end + object dlgAddWordlist: TOpenDialog + Options = [ofHideReadOnly, ofPathMustExist, ofEnableSizing] + Title = 'Create or Add Wordlist' + Left = 344 + Top = 344 + end + object dlgBrowseTestKeyboard: TOpenDialog + DefaultExt = 'js' + Filter = 'Compiled keyboard files (*.js)|*.js|All files (*.*)|*.*' + Options = [ofHideReadOnly, ofPathMustExist, ofFileMustExist, ofEnableSizing] + Title = 'Select compiled keyboard to test with' + Left = 488 + Top = 120 + end +end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas new file mode 100644 index 0000000000..a989ba321e --- /dev/null +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas @@ -0,0 +1,596 @@ +//TODO: CEF destruction is incorrect in this unit +unit Keyman.Developer.UI.UfrmModelEditor; + +interface + +uses + System.Classes, + System.Generics.Collections, + System.SysUtils, + System.Variants, + Vcl.ComCtrls, + Vcl.Controls, + Vcl.Dialogs, + Vcl.ExtCtrls, + Vcl.Forms, + Vcl.Graphics, + Vcl.Grids, + Vcl.StdCtrls, + Winapi.Messages, + Winapi.Windows, + + Keyman.Developer.UI.dmActionsModelEditor, + dmActionsMain, + LeftTabbedPageControl, + UfrmMDIEditor, + UframeTextEditor, + Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.System.Project.modeltsProjectFile, + Keyman.Developer.UI.Project.modeltsProjectFileUI, + + Keyman.Developer.UI.UframeWordlistEditor, + Keyman.Developer.System.LexicalModelParser, + Keyman.Developer.System.LexicalModelParserTypes; + +type + TfrmModelEditor = class(TfrmTikeEditor) + pages: TLeftTabbedPageControl; + pageDetails: TTabSheet; + sbDetails: TScrollBox; + panWordlists: TPanel; + lblWordlists: TLabel; + gridWordlists: TStringGrid; + cmdAddWordlist: TButton; + cmdRemoveWordlist: TButton; + panBasicInformation: TPanel; + lblFormat: TLabel; + lblBasicInformation: TLabel; + pageSource: TTabSheet; + pageCompile: TTabSheet; + Panel1: TPanel; + lblCongrats: TLabel; + cmdCompile: TButton; + cmdAddToProject: TButton; + cmdOpenContainingFolder2: TButton; + panBuildLexicalModel: TPanel; + cbFormat: TComboBox; + cbWordBreaker: TComboBox; + lblWordBreaker: TLabel; + memoComments: TMemo; + lblComments: TLabel; + lblDebugHostCaption: TLabel; + lblCrossPlatform: TLabel; + cmdTestLexicalModel: TButton; + cmdOpenDebugHost: TButton; + lbDebugHosts: TListBox; + cmdSendURLsToEmail: TButton; + Label1: TLabel; + editTestKeyboard: TEdit; + cmdBrowseTestKeyboard: TButton; + Label2: TLabel; + lblReadOnly: TLabel; + dlgAddWordlist: TOpenDialog; + dlgBrowseTestKeyboard: TOpenDialog; + Label5: TLabel; + editOutPath: TEdit; + imgQRCode: TImage; + procedure FormDestroy(Sender: TObject); + procedure cmdAddWordlistClick(Sender: TObject); + procedure cmdRemoveWordlistClick(Sender: TObject); + procedure gridWordlistsDblClick(Sender: TObject); + procedure pagesChanging(Sender: TObject; var AllowChange: Boolean); + procedure FormCreate(Sender: TObject); + procedure cbFormatClick(Sender: TObject); + procedure cbWordBreakerClick(Sender: TObject); + procedure memoCommentsChange(Sender: TObject); + procedure cmdOpenContainingFolder2Click(Sender: TObject); + procedure cmdOpenDebugHostClick(Sender: TObject); + procedure cmdSendURLsToEmailClick(Sender: TObject); + procedure cmdBrowseTestKeyboardClick(Sender: TObject); + procedure editTestKeyboardChange(Sender: TObject); + procedure lbDebugHostsClick(Sender: TObject); + private + type + TWordlist = class + Frame: TframeWordlistEditor; + Tab: TTabSheet; + Filename: string; + destructor Destroy; override; + end; + TWordlists = class(TObjectList) + public + function IndexOfFilename(const Filename: string): Integer; + end; + private + model: TStrings; + parser: TLexicalModelParser; + wordlists: TWordlists; + frameSource: TframeTextEditor; + FSetup: Integer; + procedure FillDetails; + procedure EnableControls; + procedure UpdateWordlistTabs; + function AddWordlistTab( + const WordlistFilename: string): TfrmModelEditor.TWordlist; + function MoveDesignToSource: Boolean; + function MoveSourceToDesign: Boolean; + procedure SourceChanged(Sender: TObject); + function CheckModifiedWordlistsForRemoval( + newParser: TLexicalModelParser): Boolean; + procedure UpdateQRCode; + { Private declarations } + protected + function GetHelpTopic: string; override; + function DoOpenFile: Boolean; override; + function DoSaveFile: Boolean; override; + function GetFileNameFilter: string; override; + function GetDefaultExt: string; override; + + function GetProjectFile: TProjectFile; override; + + public + procedure FindError(const Filename: string; s: string; line: Integer); override; // I4081 + procedure NotifyStartedWebDebug; + end; + +implementation + +uses + System.UITypes, + Keyman.Developer.System.Project.modeltsProjectFileAction, + Keyman.System.QRCode, + TextFileFormat, + UmodWebHttpServer, + UfrmSendURLsToEmail, + utilexecute, + utilsystem; + +{$R *.dfm} + +function FormatToIndex(format: TLexicalModelFormat): Integer; +begin + Result := Ord(format) - 1; // unknown = -1 +end; + +function WordBreakerToIndex(wordBreaker: TLexicalModelWordBreaker): Integer; +begin + Result := Ord(wordBreaker) - 1; // unknown = -1 +end; + +function FormatFromIndex(format: Integer): TLexicalModelFormat; +begin + Result := TLexicalModelFormat(format+1); // unknown = -1 +end; + +function WordBreakerFromIndex(wordBreaker: Integer): TLexicalModelWordBreaker; +begin + Result := TLexicalModelWordBreaker(wordBreaker+1); // unknown = -1 +end; + +{ TfrmModelEditor } + +function TfrmModelEditor.DoOpenFile: Boolean; +begin + model := TStringList.Create; + model.LoadFromFile(FileName); + parser := TLexicalModelParser.Create(model.Text); + wordlists := TWordlists.Create; + + Inc(FSetup); + try + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + + Result := True; +end; + +function TfrmModelEditor.DoSaveFile: Boolean; +var + wordlist: TWordlist; +begin + for wordlist in wordlists do + begin + wordlist.Frame.SaveToFile(wordlist.Frame.Filename); + end; + + model.Text := parser.Text; + model.SaveToFile(FileName); + + editOutPath.Text := (ProjectFile as TmodelTsProjectFile).TargetFilename; // I4688 + + Result := True; +end; + +procedure TfrmModelEditor.editTestKeyboardChange(Sender: TObject); +begin + if FSetup > 0 then + Exit; + (ProjectFile as TmodelTsProjectFile).TestKeyboard := editTestKeyboard.Text; +end; + +procedure TfrmModelEditor.EnableControls; +var + e: Boolean; +begin + e := parser.IsEditable; + + { Details tab } + + lblFormat.Enabled := e; + cbFormat.Enabled := e; + lblWordBreaker.Enabled := e; + cbWordBreaker.Enabled := e; + lblComments.Enabled := e; + memoComments.Enabled := e; + cmdAddWordlist.Enabled := e; + gridWordlists.Enabled := e and (parser.Wordlists.Count > 0); + cmdRemoveWordlist.Enabled := e and (parser.Wordlists.Count > 0); + lblReadOnly.Visible := not e; + + { Build tab } + cmdOpenDebugHost.Enabled := lbDebugHosts.ItemIndex >= 0; + cmdSendURLsToEmail.Enabled := lbDebugHosts.Items.Count > 0; // I4506 +end; + +procedure TfrmModelEditor.NotifyStartedWebDebug; +begin + lbDebugHosts.Clear; + modWebHttpServer.GetURLs(lbDebugHosts.Items); + if lbDebugHosts.Items.Count > 0 then + lbDebugHosts.ItemIndex := 0; + UpdateQRCode; + EnableControls; +end; + +procedure TfrmModelEditor.FindError(const Filename: string; s: string; + line: Integer); +begin + inherited; + // TODO +end; + +procedure TfrmModelEditor.FormCreate(Sender: TObject); +begin + inherited; + Inc(FSetup); + try + frameSource := TframeTextEditor.Create(Self); + frameSource.Parent := pageSource; + + frameSource.Align := alClient; + frameSource.EditorFormat := efJS; + frameSource.Visible := True; + frameSource.OnChanged := SourceChanged; + frameSource.TextFileFormat := tffUTF8; + + pages.ActivePage := pageDetails; + finally + Dec(FSetup); + end; +end; + +procedure TfrmModelEditor.FormDestroy(Sender: TObject); +begin + inherited; + FreeAndNil(parser); + FreeAndNil(model); + FreeAndNil(wordlists); +end; + +function TfrmModelEditor.GetDefaultExt: string; +begin + Result := '.model.ts'; // TODO: test if this actually works! +end; + +function TfrmModelEditor.GetFileNameFilter: string; +begin + Result := 'Lexical model files (*.model.ts)|*.model.ts|All files (*.*)|*.*'; +end; + +function TfrmModelEditor.GetHelpTopic: string; +begin + Result := ''; //SHelpTopic_Context_WordlistEditor; // TODO: use a better topic +end; + +function TfrmModelEditor.GetProjectFile: TProjectFile; +begin + Result := inherited GetProjectFile; + if not Assigned(Result) then + begin + FStandaloneProjectFile := TmodelTsProjectFileAction.Create(nil, FileName, nil); + Result := FStandaloneProjectFile; + end; +end; + +procedure TfrmModelEditor.gridWordlistsDblClick(Sender: TObject); +begin + pages.ActivePageIndex := gridWordlists.Row + 1; +end; + +procedure TfrmModelEditor.lbDebugHostsClick(Sender: TObject); +begin + UpdateQRCode; +end; + +procedure TfrmModelEditor.memoCommentsChange(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.Comment := memoComments.Text; + Modified := True; +end; + +procedure TfrmModelEditor.SourceChanged(Sender: TObject); +begin + if FSetup > 0 then + Exit; + Modified := True; +end; + +function TfrmModelEditor.CheckModifiedWordlistsForRemoval(newParser: TLexicalModelParser): Boolean; +var + wordlist: TWordlist; +begin + for wordlist in wordlists do + begin + if (newParser.Wordlists.IndexOf(wordlist.Filename) < 0) and + wordlist.Frame.Modified then + begin + case MessageDlg(Format('The wordlist %s has been removed but has been modified. Save changes before removal?', + [ExtractFileName(wordlist.Filename)]), mtConfirmation, mbYesNoCancel, 0) of + mrYes: wordlist.Frame.SaveToFile(wordlist.Frame.Filename); + mrNo: ; + mrCancel: Exit(False); + end; + end; + end; + + Result := True; +end; + +function TfrmModelEditor.MoveSourceToDesign: Boolean; +var + newParser: TLexicalModelParser; +begin + Inc(FSetup); + try + newParser := TLexicalModelParser.Create(frameSource.EditorText); + if not CheckModifiedWordlistsForRemoval(newParser) then + begin + FreeAndNil(newParser); + Exit(False); + end; + FreeAndNil(parser); + parser := newParser; + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + Result := True; +end; + +function TfrmModelEditor.MoveDesignToSource: Boolean; +begin + frameSource.EditorText := parser.Text; + Result := True; +end; + +procedure TfrmModelEditor.pagesChanging(Sender: TObject; + var AllowChange: Boolean); +begin + if FSetup > 0 then Exit; + + if pages.ActivePage = pageSource then + AllowChange := MoveSourceToDesign + else + AllowChange := MoveDesignToSource +end; + +{ --------- Details tab --------- } + +procedure TfrmModelEditor.FillDetails; +var + i: Integer; +begin + cbFormat.ItemIndex := FormatToIndex(parser.Format); + cbWordBreaker.ItemIndex := WordBreakerToIndex(parser.WordBreaker); + memoComments.Text := parser.Comment; + + if parser.Wordlists.Count = 0 then + begin + gridWordlists.RowCount := 1; + gridWordlists.Cells[0, 0] := ''; + end + else + begin + gridWordlists.RowCount := parser.Wordlists.Count; + for i := 0 to parser.Wordlists.Count - 1 do + begin + gridWordlists.Cells[0, i] := parser.Wordlists[i]; + end; + end; + + editOutPath.Text := (ProjectFile as TmodelTsProjectFile).TargetFilename; // I4688 + editTestKeyboard.Text := (ProjectFile as TmodelTsProjectFile).TestKeyboard; + + EnableControls; +end; + +procedure TfrmModelEditor.UpdateWordlistTabs; +var + i, n: Integer; + wordlist: TWordlist; +begin + // Remove tabs that are no longer listed + for i := wordlists.Count - 1 downto 0 do + begin + if parser.Wordlists.IndexOf(wordlists[i].Filename) < 0 then + wordlists.Delete(i); + end; + + // Add new tabs + for i := 0 to parser.Wordlists.Count - 1 do + begin + n := wordlists.IndexOfFilename(parser.Wordlists[i]); + if n < 0 then + begin + wordlist := AddWordlistTab(parser.Wordlists[i]); + wordlist.Tab.PageIndex := i + 1; + end + else + wordlists[n].Tab.PageIndex := i + 1; + end; +end; + +function TfrmModelEditor.AddWordlistTab(const WordlistFilename: string): TfrmModelEditor.TWordlist; +begin + Result := TWordlist.Create; + + wordlists.Add(Result); + Result.Filename := WordlistFilename; + + Result.Tab := TTabSheet.Create(Self); + Result.Tab.TabVisible := True; + Result.Tab.ImageIndex := 17; // 'spreadsheet' + Result.Tab.Caption := ExtractFileName(Result.Filename); + Result.Tab.PageControl := pages; + + Result.Frame := TframeWordlistEditor.Create(Self); + Result.Frame.LoadFromFile(ExtractFilePath(Filename) + WordlistFilename); + Result.Frame.Align := alClient; + + Result.Frame.Parent := Result.Tab; + Result.Frame.Visible := True; +end; + +{ TfrmModelEditor.TWordlists } + +function TfrmModelEditor.TWordlists.IndexOfFilename( + const Filename: string): Integer; +begin + for Result := 0 to Count - 1 do + if SameText(Items[Result].Filename, Filename) then + Exit; + Result := -1; +end; + +{ Wordlist management } + +procedure TfrmModelEditor.cbFormatClick(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.Format := FormatFromIndex(cbFormat.ItemIndex); + Modified := True; +end; + +procedure TfrmModelEditor.cbWordBreakerClick(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.WordBreaker := WordBreakerFromIndex(cbWordBreaker.ItemIndex); + Modified := True; +end; + +procedure TfrmModelEditor.cmdAddWordlistClick(Sender: TObject); +begin + if dlgAddWordlist.Execute then + begin + parser.Wordlists.Add(ExtractRelativePath(Filename, dlgAddWordlist.FileName)); + Inc(FSetup); + try + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + Modified := True; + end; +end; + +procedure TfrmModelEditor.cmdBrowseTestKeyboardClick(Sender: TObject); +begin + if dlgBrowseTestKeyboard.Execute then + editTestKeyboard.Text := dlgBrowseTestKeyboard.FileName; +end; + +procedure TfrmModelEditor.cmdOpenContainingFolder2Click(Sender: TObject); +begin + OpenContainingFolder(FileName); +end; + +procedure TfrmModelEditor.cmdOpenDebugHostClick(Sender: TObject); +begin + TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]); +end; + +procedure TfrmModelEditor.cmdRemoveWordlistClick(Sender: TObject); +var + wordlist: TWordlist; +begin + wordlist := wordlists[gridWordlists.Row]; + if wordlist.Frame.Modified then + begin + case MessageDlg(Format('Save changes to wordlist %s before removing it?', + [ExtractFileName(wordlist.Filename)]), + mtConfirmation, mbYesNoCancel, 0) of + mrYes: wordlist.Frame.SaveToFile(wordlist.Frame.Filename); + mrNo: ; + mrCancel: Exit; + end; + end; + + parser.Wordlists.Delete(gridWordlists.Row); + Inc(FSetup); + try + FillDetails; + UpdateWordlistTabs; + finally + Dec(FSetup); + end; + Modified := True; +end; + +procedure TfrmModelEditor.cmdSendURLsToEmailClick(Sender: TObject); +begin + with TfrmSendURLsToEmail.Create(Application.MainForm) do + try + //TODO: KeyboardName := Self.FKeyboardParser.GetSystemStoreValue(ssName); + Hosts.Assign(lbDebugHosts.Items); + ShowModal; + finally + Free; + end; +end; + +{ TfrmModelEditor.TWordlist } + +destructor TfrmModelEditor.TWordlist.Destroy; +begin + Frame.Free; + Tab.Free; + inherited Destroy; +end; + +procedure TfrmModelEditor.UpdateQRCode; +var + b: Vcl.Graphics.TBitmap; +begin + imgQRCode.Picture := nil; + if lbDebugHosts.ItemIndex >= 0 then + begin + b := Vcl.Graphics.TBitmap.Create; + try + DrawQRCode(lbDebugHosts.Items[lbDebugHosts.ItemIndex], b); + imgQRCode.Picture.Bitmap := b; + finally + b.Free; + end; + end; +end; + +end. diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.dfm index b7bd1388c6..428f6fdfcb 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.dfm +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.dfm @@ -5,74 +5,8 @@ inherited frmWordlistEditor: TfrmWordlistEditor ClientHeight = 299 ClientWidth = 635 Font.Name = 'Tahoma' - OnResize = FormResize ExplicitWidth = 635 ExplicitHeight = 299 PixelsPerInch = 96 TextHeight = 13 - object pages: TPageControl - Left = 0 - Top = 0 - Width = 635 - Height = 299 - ActivePage = pageDesign - Align = alClient - TabOrder = 0 - TabPosition = tpBottom - OnChanging = pagesChanging - object pageDesign: TTabSheet - Caption = 'Design' - ImageIndex = -1 - object gridWordlist: TStringGridEditControlled - Left = 0 - Top = 0 - Width = 627 - Height = 232 - Align = alClient - ColCount = 3 - DefaultRowHeight = 16 - DefaultDrawing = False - FixedCols = 0 - Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goEditing, goTabs, goAlwaysShowEditor] - TabOrder = 0 - OnClick = gridWordlistClick - OnDrawCell = gridWordlistDrawCell - OnSetEditText = gridWordlistSetEditText - ExplicitHeight = 273 - end - object panGridControls: TPanel - Left = 0 - Top = 232 - Width = 627 - Height = 41 - Align = alBottom - TabOrder = 1 - ExplicitLeft = 224 - ExplicitTop = 120 - ExplicitWidth = 185 - object cmdDeleteRow: TButton - Left = 0 - Top = 6 - Width = 75 - Height = 25 - Caption = '&Delete row' - TabOrder = 0 - OnClick = cmdDeleteRowClick - end - object cmdSortByFrequency: TButton - Left = 81 - Top = 6 - Width = 104 - Height = 25 - Caption = '&Sort by frequency' - TabOrder = 1 - OnClick = cmdSortByFrequencyClick - end - end - end - object pageCode: TTabSheet - Caption = 'Code' - ImageIndex = -1 - end - end end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas index 29d534a648..b1795f6069 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas @@ -13,46 +13,17 @@ uses Vcl.Dialogs, Vcl.Forms, Vcl.Graphics, - Vcl.Grids, - StringGridEditControlled, - - Keyman.System.WordlistTsvFile, - UframeTextEditor, - UfrmMDIEditor, Vcl.StdCtrls, Vcl.ExtCtrls; + Keyman.Developer.UI.UframeWordlistEditor, + UfrmMDIEditor; type TfrmWordlistEditor = class(TfrmTikeEditor) - pages: TPageControl; - pageDesign: TTabSheet; - pageCode: TTabSheet; - gridWordlist: TStringGridEditControlled; - panGridControls: TPanel; - cmdDeleteRow: TButton; - cmdSortByFrequency: TButton; - procedure pagesChanging(Sender: TObject; var AllowChange: Boolean); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); - procedure FormResize(Sender: TObject); - procedure gridWordlistSetEditText(Sender: TObject; ACol, ARow: Integer; - const Value: string); - procedure gridWordlistDrawCell(Sender: TObject; ACol, ARow: Integer; - ARect: TRect; State: TGridDrawState); - procedure cmdDeleteRowClick(Sender: TObject); - procedure gridWordlistClick(Sender: TObject); - procedure cmdSortByFrequencyClick(Sender: TObject); private - FWordlist: TWordlistTsvFile; - frameSource: TframeTextEditor; - FSetup: Integer; - procedure UpdateData; - function MoveCodeToWordlist: Boolean; - procedure FillCode; - procedure FillGrid; - function MoveDesignToWordlist: Boolean; - procedure SourceChanged(Sender: TObject); - procedure FillGridNewRow; - procedure EnableControls; + frame: TframeWordlistEditor; + procedure FrameModified(Sender: TObject); protected function GetHelpTopic: string; override; function DoOpenFile: Boolean; override; @@ -74,97 +45,41 @@ uses UfrmMain, UfrmMessages; -const - ColWidth_Word = 120; - ColWidth_Frequency = 64; - S_AddRowText = 'Add word...'; - { TfrmWordlistEditor } procedure TfrmWordlistEditor.FormCreate(Sender: TObject); begin inherited; - Inc(FSetup); - try - FWordlist := TWordlistTsvFile.Create; - frameSource := TframeTextEditor.Create(Self); - frameSource.Parent := pageCode; - frameSource.Align := alClient; - frameSource.EditorFormat := efWordlistTsv; - frameSource.Visible := True; - frameSource.OnChanged := SourceChanged; - frameSource.TextFileFormat := tffUTF8; -//TODO: frameSource.UseTabs := True; - pages.ActivePage := pageDesign; - - gridWordlist.Cells[0, 0] := 'Word Form'; - gridWordlist.Cells[1, 0] := 'Count'; - gridWordlist.Cells[2, 0] := 'Comment'; - gridWordlist.ColWidths[0] := ColWidth_Word; - gridWordlist.ColWidths[1] := ColWidth_Frequency; - gridWordlist.ColWidths[2] := gridWordlist.ClientWidth - ColWidth_Word - ColWidth_Frequency - 2; - finally - Dec(FSetup); - end; + frame := TframeWordlistEditor.Create(Self); + frame.Align := alClient; + frame.Parent := Self; + frame.OnModifiedChanged := FrameModified; end; procedure TfrmWordlistEditor.FormDestroy(Sender: TObject); begin inherited; - FWordlist.Free; + frame.Free; end; -procedure TfrmWordlistEditor.FormResize(Sender: TObject); +procedure TfrmWordlistEditor.FrameModified(Sender: TObject); begin - inherited; - gridWordlist.ColWidths[2] := gridWordlist.ClientWidth - ColWidth_Word - ColWidth_Frequency - 2; -end; - -procedure TfrmWordlistEditor.cmdDeleteRowClick(Sender: TObject); -begin - FWordlist.RemoveWord(gridWordlist.Row-1); - FillGrid; - EnableControls; - Modified := True; -end; - -procedure TfrmWordlistEditor.cmdSortByFrequencyClick(Sender: TObject); -begin - FWordlist.SortByFrequency; - FillGrid; - Modified := True; + Modified := frame.Modified; end; function TfrmWordlistEditor.DoOpenFile: Boolean; begin - RefreshOptions; - if FileExists(FileName) then - begin - FWordlist.LoadFromFile(FileName); - UpdateData; - end; - Result := True; + Result := frame.LoadFromFile(Filename); end; function TfrmWordlistEditor.DoSaveFile: Boolean; begin - MoveCodeToWordlist; // Does nothing if not in code page - FWordlist.SaveToFile(FileName); - Result := True; -end; - -procedure TfrmWordlistEditor.EnableControls; -var - e: Boolean; -begin - e := gridWordlist.Row < gridWordlist.RowCount - 1; - cmdDeleteRow.Enabled := e; - cmdSortByFrequency.Enabled := e; + Result := frame.SaveToFile(Filename); end; procedure TfrmWordlistEditor.FindError(const Filename: string; s: string; line: Integer); begin - // + frame.FindError(Filename, s, line); end; function TfrmWordlistEditor.GetDefaultExt: string; @@ -182,142 +97,4 @@ begin Result := SHelpTopic_Context_WordlistEditor; end; -procedure TfrmWordlistEditor.gridWordlistClick(Sender: TObject); -begin - EnableControls; -end; - -procedure TfrmWordlistEditor.gridWordlistDrawCell(Sender: TObject; ACol, - ARow: Integer; ARect: TRect; State: TGridDrawState); -var - LText: string; -begin - inherited; - if ARow = gridWordlist.RowCount - 1 then - begin - // Drawing last row - gridWordlist.Canvas.Font.Style := [fsItalic]; - end - else if gridWordlist.Cells[0, ARow] = '#' then - begin - // Drawing a comment cell - gridWordlist.Canvas.Font.Style := [fsBold]; - end - else - gridWordlist.Canvas.Font.Style := []; - - LText := gridWordlist.Cells[ACol, ARow]; - gridWordlist.Canvas.TextRect(ARect, ARect.Left+2, - ARect.Top+((ARect.Height - gridWordlist.Canvas.TextHeight(LText)) div 2), LText); -end; - -procedure TfrmWordlistEditor.gridWordlistSetEditText(Sender: TObject; ACol, - ARow: Integer; const Value: string); -var - w: TWordlistWord; -begin - if FSetup > 0 then Exit; - Inc(FSetup); - try - Modified := True; - - if ARow = gridWordlist.RowCount - 1 then - begin - if (Value = '') or (Value = S_AddRowText) then - Exit; - - w.Word := Value; - w.Frequency := 0; - w.Comment := ''; - FWordlist.AddWord(w); - gridWordlist.RowCount := gridWordlist.RowCount + 1; - FillGridNewRow; - end - else - begin - w := FWordlist.Word[ARow-1]; - case ACol of - 0: w.Word := Value; - 1: w.Frequency := StrToIntDef(Value, 0); - 2: w.Comment := Value; - end; - FWordlist.Word[ARow-1] := w; - end; - finally - Dec(FSetup); - end; -end; - -procedure TfrmWordlistEditor.pagesChanging(Sender: TObject; - var AllowChange: Boolean); -begin - if FSetup > 0 then Exit; - - if pages.ActivePage = pageCode then - AllowChange := MoveCodeToWordlist - else - AllowChange := MoveDesignToWordlist -end; - -function TfrmWordlistEditor.MoveCodeToWordlist: Boolean; -begin - if pages.ActivePage <> pageCode then - Exit(False); - - FWordlist.Text := frameSource.EditorText; - FillGrid; - Result := True; -end; - -function TfrmWordlistEditor.MoveDesignToWordlist: Boolean; -begin - FillCode; - Result := True; -end; - -procedure TfrmWordlistEditor.UpdateData; -begin - if pages.ActivePage = pageCode - then FillCode - else FillGrid; -end; - -procedure TfrmWordlistEditor.SourceChanged(Sender: TObject); -begin - if FSetup = 0 then - Modified := True; -end; - -procedure TfrmWordlistEditor.FillCode; -begin - frameSource.EditorText := FWordlist.Text; -end; - -procedure TfrmWordlistEditor.FillGrid; -var - i: Integer; - w: TWordlistWord; -begin - gridWordlist.RowCount := FWordlist.Count + 2; - for i := 0 to FWordlist.Count - 1 do - begin - w := FWordlist.Word[i]; - gridWordlist.Cells[0, i+1] := w.Word; - if w.Frequency = 0 - then gridWordlist.Cells[1, i+1] := '' - else gridWordlist.Cells[1, i+1] := IntToStr(w.Frequency); - gridWordlist.Cells[2, i+1] := w.Comment; - end; - FillGridNewRow; - gridWordlist.FixedRows := 1; -end; - -procedure TfrmWordlistEditor.FillGridNewRow; -begin - gridWordlist.Cells[0, gridWordlist.RowCount - 1] := S_AddRowText; - gridWordlist.Cells[1, gridWordlist.RowCount - 1] := ''; - gridWordlist.Cells[2, gridWordlist.RowCount - 1] := ''; - EnableControls; -end; - end. diff --git a/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm b/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm index d83cf928bc..38de6cb78f 100644 --- a/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm +++ b/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm @@ -198,9 +198,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageDetails: TTabSheet Caption = 'Details' ImageIndex = 2 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object sbDetails: TScrollBox Left = 0 Top = 0 @@ -253,7 +250,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard BevelOuter = bvNone ParentColor = True TabOrder = 2 - ExplicitTop = 189 object lblWinLangTitle: TLabel Left = 12 Top = 5 @@ -318,7 +314,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard AutoSize = True BevelOuter = bvNone TabOrder = 5 - ExplicitTop = 569 object lblComments: TLabel Left = 12 Top = 3 @@ -350,7 +345,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard Align = alTop BevelOuter = bvNone TabOrder = 6 - ExplicitTop = 656 object Label9: TLabel AlignWithMargins = True Left = 12 @@ -424,7 +418,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard AutoSize = True BevelOuter = bvNone TabOrder = 4 - ExplicitTop = 442 object panMessage: TPanel Left = 0 Top = 42 @@ -598,7 +591,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard Align = alTop BevelOuter = bvNone TabOrder = 3 - ExplicitTop = 378 object lblISOLangTitle: TLabel Left = 12 Top = 3 @@ -729,9 +721,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayout: TTabSheet Caption = 'Layout' ImageIndex = 5 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object pagesLayout: TPageControl Left = 0 Top = 0 @@ -746,10 +735,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayoutDesign: TTabSheet Caption = 'Design' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object panLayoutSimple: TPanel Left = 0 Top = 0 @@ -973,10 +958,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayoutCode: TTabSheet Caption = 'Code' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end end end @@ -1023,16 +1004,10 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageOnScreenKeyboard: TTabSheet Caption = 'On-Screen' ImageIndex = 7 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageTouchLayout: TTabSheet Caption = 'Touch Layout' ImageIndex = 16 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object pagesTouchLayout: TPageControl Left = 0 Top = 0 @@ -1047,55 +1022,32 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageTouchLayoutDesign: TTabSheet Caption = 'Design' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageTouchLayoutCode: TTabSheet Caption = 'Code' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end end end object pageIncludeCodes: TTabSheet Caption = 'Char Codes' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWEmbedJS: TTabSheet Caption = 'Embedded JS' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWEmbedCSS: TTabSheet Caption = 'Embedded CSS' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWHelp: TTabSheet Caption = 'Embedded Help' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Build' ImageIndex = 1 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object Panel1: TPanel Left = 0 Top = 0 @@ -1106,6 +1058,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Color = 14211288 ParentBackground = False TabOrder = 0 + ExplicitTop = -3 object lblCongrats: TLabel Left = 10 Top = 13 @@ -1205,7 +1158,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard object panBuildKMW: TPanel Left = 323 Top = 96 - Width = 310 + Width = 454 Height = 265 BevelOuter = bvNone Color = 15921906 @@ -1231,6 +1184,14 @@ inherited frmKeymanWizard: TfrmKeymanWizard Font.Style = [fsBold] ParentFont = False end + object imgQRCode: TImage + Left = 315 + Top = 64 + Width = 128 + Height = 128 + Proportional = True + Stretch = True + end object cmdTestKeymanWeb: TButton Left = 12 Top = 34 @@ -1255,6 +1216,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Height = 97 ItemHeight = 13 TabOrder = 3 + OnClick = lbDebugHostsClick end object cmdSendURLsToEmail: TButton Left = 12 diff --git a/windows/src/developer/TIKE/child/UfrmKeymanWizard.pas b/windows/src/developer/TIKE/child/UfrmKeymanWizard.pas index 7a9ee0fc72..3ea7d889a5 100644 --- a/windows/src/developer/TIKE/child/UfrmKeymanWizard.pas +++ b/windows/src/developer/TIKE/child/UfrmKeymanWizard.pas @@ -266,6 +266,7 @@ type panLanguageKeyman10: TPanel; lblLanguageKeyman10Note: TLabel; lblLanguageKeyman10Title: TLabel; + imgQRCode: TImage; procedure FormCreate(Sender: TObject); procedure editNameChange(Sender: TObject); procedure editCopyrightChange(Sender: TObject); @@ -330,6 +331,7 @@ type procedure pagesTouchLayoutChange(Sender: TObject); procedure pagesTouchLayoutChanging(Sender: TObject; var AllowChange: Boolean); + procedure lbDebugHostsClick(Sender: TObject); private frameSource: TframeTextEditor; @@ -466,6 +468,7 @@ type function ShouldShowLanguageControls(field: TSystemStore): Boolean; procedure OrderDetailsPanels; function ValidateFileHasNoUnicodeCharacters(sw: WideString): Boolean; + procedure UpdateQRCode; protected function GetHelpTopic: string; override; @@ -553,6 +556,7 @@ uses System.Win.ComObj, Keyman.Developer.System.HelpTopics, + Keyman.System.QRCode, Keyman.UI.FontUtils, CharacterDragObject, @@ -2105,6 +2109,11 @@ begin Result := True; end; +procedure TfrmKeymanWizard.lbDebugHostsClick(Sender: TObject); +begin + UpdateQRCode; +end; + procedure TfrmKeymanWizard.lbWinLang_SupportedClick(Sender: TObject); begin EnableControls; @@ -2557,6 +2566,7 @@ begin modWebHttpServer.GetURLs(lbDebugHosts.Items); if lbDebugHosts.Items.Count > 0 then lbDebugHosts.ItemIndex := 0; + UpdateQRCode; EnableControls; end; @@ -3330,6 +3340,23 @@ begin chkVKOnlyUseWithUnderlyingLayout.Caption := '&Only use this layout with the underlying keyboard "'+t+'" ('+s+')'; end;*) +procedure TfrmKeymanWizard.UpdateQRCode; +var + b: TBitmap; +begin + imgQRCode.Picture := nil; + if lbDebugHosts.ItemIndex >= 0 then + begin + b := TBitmap.Create; + try + DrawQRCode(lbDebugHosts.Items[lbDebugHosts.ItemIndex], b); + imgQRCode.Picture.Bitmap := b; + finally + b.Free; + end; + end; +end; + end. diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm index e2561dce1f..6c74d2f359 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm @@ -183,7 +183,7 @@ inherited frmPackageEditor: TfrmPackageEditor Top = 0 Width = 965 Height = 622 - ActivePage = pageLexicalModels + ActivePage = pageFiles Align = alClient Images = modActionsMain.ilEditorPages MultiLine = True @@ -1119,6 +1119,9 @@ inherited frmPackageEditor: TfrmPackageEditor object pageSource: TTabSheet Caption = 'Source' ImageIndex = 9 + ExplicitLeft = 0 + ExplicitWidth = 0 + ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Compile' @@ -1210,7 +1213,7 @@ inherited frmPackageEditor: TfrmPackageEditor object panBuildMobile: TPanel Left = 328 Top = 162 - Width = 310 + Width = 457 Height = 265 BevelOuter = bvNone Color = 15921906 @@ -1236,6 +1239,14 @@ inherited frmPackageEditor: TfrmPackageEditor Font.Style = [fsBold] ParentFont = False end + object imgQRCode: TImage + Left = 315 + Top = 64 + Width = 128 + Height = 128 + Proportional = True + Stretch = True + end object cmdStartTestOnline: TButton Left = 12 Top = 34 @@ -1261,6 +1272,7 @@ inherited frmPackageEditor: TfrmPackageEditor Height = 97 ItemHeight = 13 TabOrder = 1 + OnClick = lbDebugHostsClick end end object panBuildDesktop: TPanel diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.pas b/windows/src/developer/TIKE/child/UfrmPackageEditor.pas index b956540872..ffdd6c8e49 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.pas +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.pas @@ -197,6 +197,7 @@ type cmdLexicalModelLanguageEdit: TButton; chkLexicalModelRTL: TCheckBox; editLexicalModelFilename: TEdit; + imgQRCode: TImage; procedure cmdCloseClick(Sender: TObject); procedure cmdAddFileClick(Sender: TObject); procedure cmdRemoveFileClick(Sender: TObject); @@ -254,6 +255,7 @@ type procedure cmdLexicalModelLanguageRemoveClick(Sender: TObject); procedure chkLexicalModelRTLClick(Sender: TObject); procedure editLexicalModelDescriptionChange(Sender: TObject); + procedure lbDebugHostsClick(Sender: TObject); private pack: TKPSFile; FSetup: Integer; @@ -300,6 +302,7 @@ type function ShowAddLanguageForm(grid: TStringGrid; langs: TPackageKeyboardLanguageList): Boolean; procedure RefreshLexicalModelList; + procedure UpdateQRCode; protected function GetHelpTopic: string; override; @@ -338,6 +341,7 @@ uses KeymanVersion, Keyman.System.PackageInfoRefreshKeyboards, Keyman.System.PackageInfoRefreshLexicalModels, + Keyman.System.QRCode, Keyman.System.KeyboardUtils, Keyman.System.LexicalModelUtils, kmxfileconsts, @@ -758,6 +762,11 @@ end; function SHGetFileInfoW(pszPath: PWideChar; dwFileAttributes: DWORD; var psfi: TSHFileInfoW; cbFileInfo, uFlags: UINT): DWORD; stdcall; external shell32; +procedure TfrmPackageEditor.lbDebugHostsClick(Sender: TObject); +begin + UpdateQRCode; +end; + procedure TfrmPackageEditor.lbFilesClick(Sender: TObject); var e: Boolean; @@ -1337,6 +1346,7 @@ begin modWebHttpServer.GetURLs(lbDebugHosts.Items); if lbDebugHosts.Items.Count > 0 then lbDebugHosts.ItemIndex := 0; + UpdateQRCode; EnableCompileTabControls; end; @@ -1662,7 +1672,7 @@ var FHasDesktopTarget, FHasMobileTarget: Boolean; begin FHasDesktopTarget := False; - FHasMobileTarget := False; + FHasMobileTarget := pack.LexicalModels.Count > 0; for k in pack.Keyboards do begin for i := 0 to pack.Files.Count - 1 do @@ -1958,5 +1968,22 @@ begin Modified := True; end; +procedure TfrmPackageEditor.UpdateQRCode; +var + b: TBitmap; +begin + imgQRCode.Picture := nil; + if lbDebugHosts.ItemIndex >= 0 then + begin + b := TBitmap.Create; + try + DrawQRCode(lbDebugHosts.Items[lbDebugHosts.ItemIndex] + '/packages.html', b); + imgQRCode.Picture.Bitmap := b; + finally + b.Free; + end; + end; +end; + end. diff --git a/windows/src/developer/TIKE/debug/UfrmDebugStatus_CallStack.pas b/windows/src/developer/TIKE/debug/UfrmDebugStatus_CallStack.pas index 257708f14b..4dc8d1bde6 100644 --- a/windows/src/developer/TIKE/debug/UfrmDebugStatus_CallStack.pas +++ b/windows/src/developer/TIKE/debug/UfrmDebugStatus_CallStack.pas @@ -106,7 +106,7 @@ begin if not Assigned(lbCallStack.Items.Objects[lbCallStack.ItemIndex]) then Exit; rule := lbCallStack.Items.Objects[lbCallStack.ItemIndex] as TDebugEventRuleData; if not SetEditorCursorLine(rule.Line) - then ShowMessage('TIKE could not find the line for this rule.') + then ShowMessage('Keyman Developer could not find the line for this rule.') else EditorMemo.SetFocus; end; diff --git a/windows/src/developer/TIKE/debug/UfrmDebuggerInput.dfm b/windows/src/developer/TIKE/debug/UfrmDebuggerInput.dfm index 83b26ec904..f5195a243b 100644 --- a/windows/src/developer/TIKE/debug/UfrmDebuggerInput.dfm +++ b/windows/src/developer/TIKE/debug/UfrmDebuggerInput.dfm @@ -3,7 +3,7 @@ inherited frmDebuggerInput: TfrmDebuggerInput Top = 325 Width = 258 Height = 157 - Caption = 'TIKE Debug Input' + Caption = 'Keyman Developer Debug Input' Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText @@ -33,7 +33,7 @@ inherited frmDebuggerInput: TfrmDebuggerInput Width = 205 Height = 40 Alignment = taCenter - Caption = 'The TIKE debugger is awaiting input' + Caption = 'The Keyman Developer debugger is awaiting input' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -16 diff --git a/windows/src/developer/TIKE/dialogs/UfrmMustIncludeDebug.dfm b/windows/src/developer/TIKE/dialogs/UfrmMustIncludeDebug.dfm index f688612ba1..adf295207c 100644 --- a/windows/src/developer/TIKE/dialogs/UfrmMustIncludeDebug.dfm +++ b/windows/src/developer/TIKE/dialogs/UfrmMustIncludeDebug.dfm @@ -3,7 +3,7 @@ inherited frmMustIncludeDebug: TfrmMustIncludeDebug Top = 114 BorderIcons = [biSystemMenu] BorderStyle = bsDialog - Caption = 'TIKE Debugger' + Caption = 'Keyman Developer Debugger' ClientHeight = 133 ClientWidth = 393 Color = clBtnFace diff --git a/windows/src/developer/TIKE/dialogs/UfrmNew.pas b/windows/src/developer/TIKE/dialogs/UfrmNew.pas index 8e2b668430..ecb0adea70 100644 --- a/windows/src/developer/TIKE/dialogs/UfrmNew.pas +++ b/windows/src/developer/TIKE/dialogs/UfrmNew.pas @@ -90,7 +90,7 @@ uses Keyman.Developer.System.Project.ProjectFile, Keyman.System.KeyboardUtils, Keyman.System.LexicalModelUtils, - shlobj, + KeymanDeveloperOptions, utilsystem; {$R *.DFM} @@ -218,7 +218,7 @@ var begin inherited; if FGlobalProject.Untitled - then FRootPath := GetFolderPath(CSIDL_PERSONAL) + then FRootPath := FKeymanDeveloperOptions.DefaultProjectPath else FRootPath := ExtractFilePath(FGlobalProject.FileName); lvItems.Selected := lvItems.Items[0]; diff --git a/windows/src/developer/TIKE/dialogs/UfrmNewFileDetails.pas b/windows/src/developer/TIKE/dialogs/UfrmNewFileDetails.pas index c9a70d6eee..9588f9caa8 100644 --- a/windows/src/developer/TIKE/dialogs/UfrmNewFileDetails.pas +++ b/windows/src/developer/TIKE/dialogs/UfrmNewFileDetails.pas @@ -66,11 +66,8 @@ type implementation uses - ShlObj, - - Keyman.Developer.System.HelpTopics, - - utilsystem; + KeymanDeveloperOptions, + Keyman.Developer.System.HelpTopics; {$R *.DFM} @@ -109,7 +106,7 @@ end; procedure TfrmNewFileDetails.SetBaseFileName(Value: string); begin if ExtractFilePath(Value) = '' // I4798 - then editFilePath.Text := ExcludeTrailingPathDelimiter(GetFolderPath(CSIDL_PERSONAL)) // I4798 + then editFilePath.Text := ExcludeTrailingPathDelimiter(FKeymanDeveloperOptions.DefaultProjectPath) // I4798 else editFilePath.Text := ExtractFileDir(Value); // I4798 editFileName.Text := ''; // I4798 end; diff --git a/windows/src/developer/TIKE/dialogs/UfrmOptions.dfm b/windows/src/developer/TIKE/dialogs/UfrmOptions.dfm index 9b80a80085..856b16b4e7 100644 --- a/windows/src/developer/TIKE/dialogs/UfrmOptions.dfm +++ b/windows/src/developer/TIKE/dialogs/UfrmOptions.dfm @@ -30,7 +30,7 @@ inherited frmOptions: TfrmOptions ExplicitHeight = 0 object cmdProxySettings: TButton Left = 8 - Top = 160 + Top = 209 Width = 125 Height = 25 Caption = '&Proxy Settings...' @@ -40,29 +40,29 @@ inherited frmOptions: TfrmOptions object gbExternalEditor: TGroupBox Left = 8 Top = 71 - Width = 237 + Width = 401 Height = 45 Caption = '&External Editor Path' TabOrder = 1 object editExternalEditorPath: TEdit Left = 8 Top = 16 - Width = 141 + Width = 311 Height = 21 TabOrder = 0 end object cmdBrowseExternalEditor: TButton - Left = 156 + Left = 325 Top = 16 - Width = 73 + Width = 69 Height = 21 Caption = '&Browse...' TabOrder = 1 end end object cmdSMTPSettings: TButton - Left = 139 - Top = 160 + Left = 8 + Top = 240 Width = 125 Height = 25 Caption = 'S&MTP Settings...' @@ -79,7 +79,7 @@ inherited frmOptions: TfrmOptions end object cmdResetToolWindows: TButton Left = 8 - Top = 129 + Top = 178 Width = 125 Height = 25 Caption = '&Reset tool windows' @@ -94,6 +94,30 @@ inherited frmOptions: TfrmOptions Caption = '&Allow multiple instances of Keyman Developer' TabOrder = 5 end + object gbDefaultProjectPath: TGroupBox + Left = 8 + Top = 122 + Width = 401 + Height = 45 + Caption = '&Default Project Folder' + TabOrder = 6 + object editDefaultProjectPath: TEdit + Left = 8 + Top = 16 + Width = 311 + Height = 21 + TabOrder = 0 + end + object cmdBrowseDefaultProjectPath: TButton + Left = 325 + Top = 16 + Width = 69 + Height = 21 + Caption = '&Browse...' + TabOrder = 1 + OnClick = cmdBrowseDefaultProjectPathClick + end + end end object tabEditor: TTabSheet Caption = 'Editor' @@ -417,4 +441,11 @@ inherited frmOptions: TfrmOptions Left = 212 Top = 403 end + object dlgBrowseDefaultProjectPath: TBrowse4Folder + InitialDir = '' + Root = Desktop + Title = 'Browse for Default Project Folder' + Left = 176 + Top = 400 + end end diff --git a/windows/src/developer/TIKE/dialogs/UfrmOptions.pas b/windows/src/developer/TIKE/dialogs/UfrmOptions.pas index 1e89cae625..c687525f76 100644 --- a/windows/src/developer/TIKE/dialogs/UfrmOptions.pas +++ b/windows/src/developer/TIKE/dialogs/UfrmOptions.pas @@ -41,7 +41,7 @@ uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, // Themes, StdCtrls, ComCtrls, ExtCtrls, - UfrmTike, UserMessages, PaintPanel; + UfrmTike, UserMessages, PaintPanel, Browse4Folder; type TfrmOptions = class(TTikeForm) @@ -94,6 +94,10 @@ type lblEditorCustomTheme: TLabel; lblEditorTheme: TLabel; chkAllowMultipleInstances: TCheckBox; + gbDefaultProjectPath: TGroupBox; + editDefaultProjectPath: TEdit; + cmdBrowseDefaultProjectPath: TButton; + dlgBrowseDefaultProjectPath: TBrowse4Folder; procedure FormCreate(Sender: TObject); procedure cmdOKClick(Sender: TObject); procedure cmdDefaultFontClick(Sender: TObject); @@ -105,6 +109,7 @@ type procedure cmdSMTPSettingsClick(Sender: TObject); procedure cmdResetToolWindowsClick(Sender: TObject); procedure cbEditorThemeClick(Sender: TObject); + procedure cmdBrowseDefaultProjectPathClick(Sender: TObject); private FDefaultFont, FQuotedFont: TFont; { Private declarations } @@ -120,6 +125,7 @@ implementation uses System.JSON, + System.UITypes, Keyman.Developer.System.HelpTopics, @@ -180,6 +186,7 @@ begin chkOpenKeyboardFilesInSourceView.Checked := OpenKeyboardFilesInSourceView; // I4751 editExternalEditorPath.Text := ExternalEditorPath; + editDefaultProjectPath.Text := DefaultProjectPath; editIndentSize.Text := IntToStr(IndentSize); @@ -237,6 +244,19 @@ procedure TfrmOptions.cmdOKClick(Sender: TObject); var i: Integer; begin + if not DirectoryExists(editDefaultProjectPath.Text) then + begin + if MessageDlg('The default project folder '+editDefaultProjectPath.Text+' does not exist. Do you want to create it now?', + mtConfirmation, mbYesNoCancel, 0) <> mrYes then Exit; + + if not ForceDirectories(editDefaultProjectPath.Text) then + begin + ShowMessage('Keyman Developer was unable to create the default project folder '+editDefaultProjectPath.Text+ + '. The error was: '+SysErrorMessage(GetLastError)); + Exit; + end; + end; + with FKeymanDeveloperOptions do begin UseOldDebugger := chkUseOldDebugger.Checked; @@ -262,6 +282,7 @@ begin OpenKeyboardFilesInSourceView := chkOpenKeyboardFilesInSourceView.Checked; // I4751 ExternalEditorPath := editExternalEditorPath.Text; + DefaultProjectPath := IncludeTrailingPathDelimiter(editDefaultProjectPath.Text); Write; end; @@ -382,6 +403,13 @@ begin lblEditorCustomTheme.Caption := ''; end; +procedure TfrmOptions.cmdBrowseDefaultProjectPathClick(Sender: TObject); +begin + dlgBrowseDefaultProjectPath.InitialDir := editDefaultProjectPath.Text; + if dlgBrowseDefaultProjectPath.Execute then + editDefaultProjectPath.Text := dlgBrowseDefaultProjectPath.FileName; +end; + procedure TfrmOptions.cmdCharMapRebuildDatabaseClick(Sender: TObject); var FUnicodeSourcePath: WideString; diff --git a/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas b/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas index 27d36a816c..9ad35f5bc5 100644 --- a/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas +++ b/windows/src/developer/TIKE/http/Keyman.Developer.System.HttpServer.Debugger.pas @@ -54,13 +54,23 @@ type property Name: string read FName; end; + TWebDebugModelInfo = class + strict private + FFilename: string; + public + constructor Create(const AFilename: string); + property Filename: string read FFilename; + end; + TDebuggerHttpResponder = class(TBaseHttpResponder) private - FKeyboardsCS, FPackagesCS: TCriticalSection; // I4036 + FModelsCS, FKeyboardsCS, FPackagesCS: TCriticalSection; // I4036 + FModels: TObjectDictionary; FKeyboards: TObjectDictionary; // I4063 FPackages: TObjectDictionary; function GetKeyboardStoredFileName(const WebFilename: string): string; function GetPackageStoredFileName(const WebFilename: string): string; + function GetModelStoredFileName(const WebFilename: string): string; public constructor Create; destructor Destroy; override; @@ -68,6 +78,8 @@ type procedure UnregisterKeyboard(const Filename: string); procedure RegisterPackage(const Filename, Name: string); procedure UnregisterPackage(const Filename: string); + procedure RegisterModel(const Filename: string); + procedure UnregisterModel(const Filename: string); procedure ProcessRequest(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo); @@ -114,6 +126,9 @@ begin FPackagesCS := TCriticalSection.Create; FPackages := TObjectDictionary.Create; // I4063 + + FModelsCS := TCriticalSection.Create; + FModels := TObjectDictionary.Create; // I4063 end; destructor TDebuggerHttpResponder.Destroy; @@ -124,6 +139,9 @@ begin FreeAndNil(FPackages); FreeAndNil(FPackagesCS); // I4036 + FreeAndNil(FModels); + FreeAndNil(FModelsCS); // I4036 + inherited Destroy; end; @@ -139,6 +157,18 @@ begin end; end; +function TDebuggerHttpResponder.GetModelStoredFileName( + const WebFilename: string): string; +begin + FModelsCS.Enter; // I4036 + try + if FModels.ContainsKey(WebFilename) then Result := FModels[WebFilename].Filename + else Result := ''; + finally + FModelsCS.Leave; + end; +end; + function TDebuggerHttpResponder.GetPackageStoredFileName( const WebFilename: string): string; begin @@ -214,9 +244,10 @@ procedure TDebuggerHttpResponder.ProcessRequest(AContext: TIdContext; key: string; response: string; value: TWebDebugKeyboardInfo; - src: string; + id, src: string; n: Integer; srcVersion: string; + model: string; begin // Get dynamic keyboard registration @@ -263,6 +294,17 @@ procedure TDebuggerHttpResponder.ProcessRequest(AContext: TIdContext; FKeyboardsCS.Leave; end; + FModelsCS.Enter; + try + for model in FModels.Keys do + begin + id := ChangeFileExt(model, ''); + response := response + Format('registerModel("%s", "%s");', [id, model]); + end; + finally + FModelsCS.Leave; + end; + response := response + '})();'; AResponseInfo.CharSet := 'UTF-8'; @@ -605,6 +647,26 @@ begin Exit; end; end + else if Copy(doc, 1, 6) = 'model/' then + begin + Delete(doc, 1, 6); + + // Models always expire immediately + AResponseInfo.Expires := EncodeDate(1990, 1, 1); // I4037 + AResponseInfo.CacheControl := 'no-cache, no-store'; // I4037 + AResponseInfo.LastModified := Now; // I4037 + if not FFileRegExp.Exec(doc) then + begin + Respond404; + Exit; + end; + doc := GetModelStoredFileName(doc); + if doc = '' then + begin + Respond404; + Exit; + end; + end else begin if not FFileRegExp.Exec(doc) then @@ -650,6 +712,19 @@ begin end; end; +procedure TDebuggerHttpResponder.RegisterModel(const Filename: string); +var + m: TWebDebugModelInfo; +begin + m := TWebDebugModelInfo.Create(Filename); + FModelsCS.Enter; + try + FModels.AddOrSetValue(ExtractFileName(Filename), m); + finally + FModelsCS.Leave; + end; +end; + procedure TDebuggerHttpResponder.RegisterPackage(const Filename, Name: string); var p: TWebDebugPackageInfo; @@ -673,6 +748,16 @@ begin end; end; +procedure TDebuggerHttpResponder.UnregisterModel(const Filename: string); +begin + FModelsCS.Enter; // I4036 + try + FModels.Remove(ExtractFileName(Filename)); + finally + FModelsCS.Leave; + end; +end; + procedure TDebuggerHttpResponder.UnregisterPackage(const Filename: string); begin FPackagesCS.Enter; // I4036 @@ -798,4 +883,12 @@ begin FName := AName; end; +{ TWebDebugModelInfo } + +constructor TWebDebugModelInfo.Create(const AFilename: string); +begin + inherited Create; + FFilename := AFilename; +end; + end. diff --git a/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas new file mode 100644 index 0000000000..c21d4b5e6f --- /dev/null +++ b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas @@ -0,0 +1,234 @@ +unit Keyman.Developer.System.LexicalModelParser; + +interface + +uses + System.Classes, + System.RegularExpressions, + + Keyman.Developer.System.LexicalModelParserTypes; + +type + TLexicalModelParser = class + private + FText: TStrings; + FModified: Boolean; + FComment: string; + FFormat: TLexicalModelFormat; + FWordlists: TStrings; + FWordBreaker: TLexicalModelWordBreaker; + FIsEditable: Boolean; + function GetText: string; + procedure PrepareText; + procedure SetComment(const Value: string); + procedure SetFormat(const Value: TLexicalModelFormat); + procedure SetWordBreaker(const Value: TLexicalModelWordBreaker); + procedure Parse; + procedure Modify; + function ReplaceFormat(const m: TMatch): string; + function ReplaceWordBreaker(const m: TMatch): string; + function ReplaceSources(const m: TMatch): string; + function ReplaceComment(const m: TMatch): string; + procedure WordlistsChange(Sender: TObject); + public + constructor Create(Source: string); + destructor Destroy; override; + property Text: string read GetText; + property Format: TLexicalModelFormat read FFormat write SetFormat; + property WordBreaker: TLexicalModelWordBreaker read FWordBreaker write SetWordBreaker; + property Wordlists: TStrings read FWordlists; + property Comment: string read FComment write SetComment; + property IsEditable: Boolean read FIsEditable; + end; + +implementation + +uses + System.SysUtils; + +{ TLexicalModelParser } + +const + SComment = '^\s*\/\*(.+?)\*\/'; + SFormat = 'format\s*:\s*([''"])(.+?)(\1)'; + SWordBreaker = 'wordBreaker\s*:\s*([''"])(.+?)(\1)'; + SSources = 'sources\s*:\s*\[(.+?)\]'; + SSource = '\s*([''"])(.+?)(\1)\s*(,?)'; + +constructor TLexicalModelParser.Create(Source: string); +begin + inherited Create; + FWordlists := TStringList.Create; + (FWordlists as TStringList).OnChange := WordlistsChange; + FText := TStringList.Create; + FText.Text := Source; + Parse; + FModified := False; +end; + +destructor TLexicalModelParser.Destroy; +begin + FreeAndNil(FWordlists); + FreeAndNil(FText); + inherited Destroy; +end; + +function TLexicalModelParser.GetText: string; +begin + PrepareText; + Result := FText.Text; +end; + +procedure TLexicalModelParser.Modify; +begin + FModified := True; +end; + +procedure TLexicalModelParser.Parse; +var + re: TRegEx; + m: TMatch; + s: string; +begin + // Long term, we should probably consider using the TypeScript compiler + // to real-time generate an AST and work from that. But that introduces a + // layer of glue to the Delphi code that I don't want to get into just now. + FIsEditable := True; + FFormat := lmfUnknown; + FWordBreaker := lmwbUnknown; + FComment := ''; + + s := FText.Text; + + // /* Comment */ + m := TRegEx.Match(s, SComment, [roSingleLine]); + if m.Success then + FComment := m.Groups[1].Value; + + // format: + m := TRegEx.Match(s, SFormat, [roMultiLine]); + if m.Success then + FFormat := TLexicalModelParserTypes.FormatFromText(m.Groups[2].Value); + + // wordBreaker: + m := TRegEx.Match(s, SWordBreaker, [roMultiLine]); + if m.Success then + FWordBreaker := TLexicalModelParserTypes.WordBreakerFromText(m.Groups[2].Value); + + // sources: + FWordlists.Clear; + m := TRegEx.Match(s, SSources, [roMultiLine]); + if m.Success then + begin + re := TRegEx.Create(SSource, [roMultiLine]); + m := re.Match(m.Groups[1].Value); + while m.Success do + begin + FWordlists.Add(m.Groups[2].Value); + m := m.NextMatch; + end; + end + else + FIsEditable := False; + + FIsEditable := FIsEditable and (FFormat <> lmfUnknown) and (FWordBreaker <> lmwbUnknown); +end; + +function TLexicalModelParser.ReplaceComment(const m: TMatch): string; +begin + if FComment = '' + then Result := '' + else Result := '/*'+FComment+'*/'; +end; + +function TLexicalModelParser.ReplaceFormat(const m: TMatch): string; +begin + Result := m.Value.Substring(0, m.Groups[2].Index - m.Index) + + TLexicalModelParserTypes.FormatToText(FFormat) + + m.Value.Substring(m.Groups[2].Index - m.Index + m.Groups[2].Length); +end; + +function TLexicalModelParser.ReplaceWordBreaker(const m: TMatch): string; +begin + Result := m.Value.Substring(0, m.Groups[2].Index - m.Index) + + TLexicalModelParserTypes.WordBreakerToText(FWordBreaker) + + m.Value.Substring(m.Groups[2].Index - m.Index + m.Groups[2].Length); +end; + +function TLexicalModelParser.ReplaceSources(const m: TMatch): string; +var + sources: string; + source: string; +begin + sources := ''; + for source in FWordlists do + begin + if sources <> '' then + sources := sources + ', '; + sources := sources + '''' + source + ''''; // assuming no quote characters + end; + + Result := m.Value.Substring(0, m.Groups[1].Index - m.Index) + + sources + + m.Value.Substring(m.Groups[1].Index - m.Index + m.Groups[1].Length); +end; + +procedure TLexicalModelParser.PrepareText; +var + s: string; +begin + if not FModified then Exit; + + s := FText.Text; + + // Replace tokens + s := TRegEx.Replace(s, SFormat, ReplaceFormat, [roMultiLine]); + s := TRegEx.Replace(s, SWordBreaker, ReplaceWordBreaker, [roMultiLine]); + s := TRegEx.Replace(s, SSources, ReplaceSources, [roMultiLine]); + + // Replace, remove or add comment + if TRegEx.IsMatch(s, SComment, [roSingleLine]) then + s := TRegEx.Replace(s, SComment, ReplaceComment, [roSingleLine]) + else if FComment <> '' then + s := '/*'+FComment+'*/'#13#10+s; + + + FText.Text := s; +end; + +procedure TLexicalModelParser.SetComment(const Value: string); +begin + Assert(FIsEditable); + if FComment <> Value then + begin + FComment := Value; + Modify; + end; +end; + +procedure TLexicalModelParser.SetFormat(const Value: TLexicalModelFormat); +begin + Assert(FIsEditable); + if FFormat <> Value then + begin + FFormat := Value; + Modify; + end; +end; + +procedure TLexicalModelParser.SetWordBreaker(const Value: TLexicalModelWordBreaker); +begin + Assert(FIsEditable); + if FWordBreaker <> Value then + begin + FWordBreaker := Value; + Modify; + end; +end; + +procedure TLexicalModelParser.WordlistsChange(Sender: TObject); +begin + Modify; +end; + +end. diff --git a/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParserTypes.pas b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParserTypes.pas new file mode 100644 index 0000000000..07539f72f7 --- /dev/null +++ b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParserTypes.pas @@ -0,0 +1,52 @@ +unit Keyman.Developer.System.LexicalModelParserTypes; + +interface + +type + TLexicalModelFormat = (lmfUnknown, lmfTrie10, lmfCustom10); + TLexicalModelWordBreaker = (lmwbUnknown, lmwbDefault, lmwbAscii, lmwbCustom); + + TLexicalModelParserTypes = class sealed + private + const + SLexicalModelFormats: array[TLexicalModelFormat] of string = ('?', 'trie-1.0', 'custom-1.0'); + SLexicalModelWordBreakers: array[TLexicalModelWordBreaker] of string = ('?', 'default', 'ascii', 'custom'); + LexicalModelFormats: set of TLexicalModelFormat = [lmfUnknown..lmfCustom10]; + LexicalModelWordBreakers: set of TLexicalModelWordBreaker = [lmwbUnknown..lmwbCustom]; + public + class function FormatFromText(const Value: string): TLexicalModelFormat; static; + class function FormatToText(const Value: TLexicalModelFormat): string; static; + class function WordBreakerFromText(const Value: string): TLexicalModelWordBreaker; static; + class function WordBreakerToText(const Value: TLexicalModelWordBreaker): string; static; + end; + +implementation + +uses + System.SysUtils; + +class function TLexicalModelParserTypes.FormatFromText(const Value: string): TLexicalModelFormat; +begin + for Result in LexicalModelFormats do + if SLexicalModelFormats[Result].Equals(Value) then Exit; + Result := lmfUnknown; +end; + +class function TLexicalModelParserTypes.FormatToText(const Value: TLexicalModelFormat): string; +begin + Result := SLexicalModelFormats[Value]; +end; + +class function TLexicalModelParserTypes.WordBreakerFromText(const Value: string): TLexicalModelWordBreaker; +begin + for Result in LexicalModelWordBreakers do + if SLexicalModelWordBreakers[Result].Equals(Value) then Exit; + Result := lmwbUnknown; +end; + +class function TLexicalModelParserTypes.WordBreakerToText(const Value: TLexicalModelWordBreaker): string; +begin + Result := SLexicalModelWordBreakers[Value]; +end; + +end. diff --git a/windows/src/developer/TIKE/main/KeymanDeveloperOptions.pas b/windows/src/developer/TIKE/main/KeymanDeveloperOptions.pas index 3b34ab248f..6cbc7a8efb 100644 --- a/windows/src/developer/TIKE/main/KeymanDeveloperOptions.pas +++ b/windows/src/developer/TIKE/main/KeymanDeveloperOptions.pas @@ -58,6 +58,7 @@ type FDisplayTheme: string; FEditorTheme: string; FFix183_LadderLength: Integer; + FDefaultProjectPath: string; procedure CloseRegistry; procedure OpenRegistry; function regReadString(const nm, def: string): string; @@ -93,6 +94,7 @@ type property AllowMultipleInstances: Boolean read FAllowMultipleInstances write FAllowMultipleInstances; property OpenKeyboardFilesInSourceView: Boolean read FOpenKeyboardFilesInSourceView write FOpenKeyboardFilesInSourceView; // I4751 + property DefaultProjectPath: string read FDefaultProjectPath write FDefaultProjectPath; property DisplayTheme: string read FDisplayTheme write FDisplayTheme; // I4796 @@ -117,9 +119,14 @@ const (Name: 'hc-black'; Desc: 'High Constrast (hc-black)') ); +const + CDefaultProjectPath = 'Keyman Developer\Projects\'; + implementation uses + Winapi.ShlObj, + utilsystem, OnlineConstants, GetOSVersion; @@ -195,6 +202,8 @@ begin FTestEmailAddresses := regReadString(SRegValue_IDEOptTestEmailAddresses, ''); // I4506 FFix183_LadderLength := regReadInt(SRegValue_IDEOpt_WebLadderLength, CRegValue_IDEOpt_WebLadderLength_Default); + + FDefaultProjectPath := IncludeTrailingPathDelimiter(regReadString(SRegValue_IDEOpt_DefaultProjectPath, GetFolderPath(CSIDL_PERSONAL) + CDefaultProjectPath)); finally CloseRegistry; end; @@ -231,6 +240,8 @@ begin regWriteString(SRegValue_IDEOptTestEmailAddresses, FTestEmailAddresses); // I4506 regWriteInt(SRegValue_IDEOpt_WebLadderLength, FFix183_LadderLength); + + regWriteString(SRegValue_IDEOpt_DefaultProjectPath, FDefaultProjectPath); finally CloseRegistry; end; diff --git a/windows/src/developer/TIKE/main/UframeTextEditor.pas b/windows/src/developer/TIKE/main/UframeTextEditor.pas index fca4d88760..e4b5b1dc8d 100644 --- a/windows/src/developer/TIKE/main/UframeTextEditor.pas +++ b/windows/src/developer/TIKE/main/UframeTextEditor.pas @@ -1116,7 +1116,15 @@ begin try j.AddPair('x', TJSONNumber.Create(X)); j.AddPair('y', TJSONNumber.Create(Y)); - j.AddPair('text', cdo.Text[cdo.InsertType]); + + // The character map insert format actually only + // makes sense for the .kmn source editor. For all + // other contexts, we currently only support cmimCharacter. + // It would be possible to do \uxxxx for JS/JSON etc but + // for now that is low priority. + if FEditorFormat = efKMN + then j.AddPair('text', cdo.Text[cdo.InsertType]) + else j.AddPair('text', cdo.Text[cmimCharacter]); ExecuteCommand('charmapDragDrop', j); finally j.Free; diff --git a/windows/src/developer/TIKE/main/UfrmMain.dfm b/windows/src/developer/TIKE/main/UfrmMain.dfm index 80a7409c84..fb3c8d4764 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.dfm +++ b/windows/src/developer/TIKE/main/UfrmMain.dfm @@ -2950,7 +2950,19 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper Action = modActionsMain.actProjectSettings end end - object Keyboards1: TMenuItem + object mnuModel: TMenuItem + Caption = '&Model' + object CompileModel1: TMenuItem + Action = modActionsModelEditor.actModelCompile + end + object N2: TMenuItem + Caption = '-' + end + object estLexicalModel1: TMenuItem + Action = modActionsModelEditor.actModelTest + end + end + object mnuKeyboard: TMenuItem Caption = '&Keyboard' object CompileKeyboard1: TMenuItem Action = modActionsKeyboardEditor.actKeyboardCompile @@ -2989,7 +3001,7 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper Action = modActionsKeyboardEditor.actKeyboardFontHelper end end - object Debug2: TMenuItem + object mnuDebug: TMenuItem Caption = '&Debug' object SetBreakpoint1: TMenuItem Action = modActionsKeyboardEditor.actDebugSetClearBreakpoint diff --git a/windows/src/developer/TIKE/main/UfrmMain.pas b/windows/src/developer/TIKE/main/UfrmMain.pas index 8cc494d7ec..10275ad8b3 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.pas +++ b/windows/src/developer/TIKE/main/UfrmMain.pas @@ -97,6 +97,7 @@ uses mrulist, UfrmUnicodeDataStatus, CharacterDragObject, + Keyman.Developer.UI.dmActionsModelEditor, dmActionsMain, UnicodeData, UserMessages, webhelp, dmActionsKeyboardEditor, Dialogs, UfrmTike, AppEvnts, DropTarget, @@ -156,8 +157,8 @@ type Edit1: TMenuItem; View1: TMenuItem; Project1: TMenuItem; - Keyboards1: TMenuItem; - Debug2: TMenuItem; + mnuKeyboard: TMenuItem; + mnuDebug: TMenuItem; ools1: TMenuItem; Help1: TMenuItem; New1: TMenuItem; @@ -269,6 +270,10 @@ type DebugTests1: TMenuItem; CrashTest1: TMenuItem; CloseProject1: TMenuItem; + mnuModel: TMenuItem; + CompileModel1: TMenuItem; + N2: TMenuItem; + estLexicalModel1: TMenuItem; procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); procedure mnuFileClick(Sender: TObject); @@ -341,6 +346,7 @@ type procedure SaveDockLayout; procedure CEFShutdownComplete(Sender: TObject); procedure ActivateActiveChild; + function OpenModelEditor(FFileName: string): TfrmTikeEditor; protected procedure WndProc(var Message: TMessage); override; @@ -410,6 +416,7 @@ implementation uses System.Math, Winapi.WinInet, + Winapi.ShlObj, Winapi.Urlmon, Winapi.UxTheme, System.Win.ComObj, @@ -432,6 +439,7 @@ uses Keyman.Developer.UI.Project.ProjectFileUI, Keyman.Developer.UI.Project.ProjectUI, Keyman.Developer.UI.UfrmWordlistEditor, + Keyman.Developer.UI.UfrmModelEditor, TextFileFormat, RedistFiles, ErrorControlledRegistry, @@ -472,6 +480,13 @@ var begin inherited; + if not ForceDirectories(FKeymanDeveloperOptions.DefaultProjectPath) then + begin + // Fall back to Documents folder if we cannot create the default project path + // Documents folder should always exist + FKeymanDeveloperOptions.DefaultProjectPath := GetFolderPath(CSIDL_PERSONAL); + end; + FFirstShow := True; hInputLangChangeHook := SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc_InputLangChange, 0, GetCurrentThreadId); @@ -479,6 +494,7 @@ begin modActionsTextEditor := TmodActionsTextEditor.Create(Self); modActionsKeyboardEditor := TmodActionsKeyboardEditor.Create(Self); modActionsMain := TmodActionsMain.Create(Self); + modActionsModelEditor := TmodActionsModelEditor.Create(Self); FProjectMRU := TMRUList.Create('Project'); FProjectMRU.OnChange := ProjectMRUChange; @@ -1104,6 +1120,11 @@ begin end; function TfrmKeymanDeveloper.OpenFile(FFileName: string; FCloseNewFile: Boolean): TfrmTikeChild; + function FileHasModelTsExt(Filename: string): Boolean; + begin + // We cannot use ExtractFileExt because of the two-part extension + Result := Filename.ToLower.EndsWith('.model.ts'); + end; var ext: string; begin @@ -1137,6 +1158,7 @@ begin else if ext = '.kvks' then Result := OpenKVKEditor(FFileName) else if ext = '.bmp' then Result := OpenEditor(FFileName, TfrmBitmapEditor) else if ext = '.tsv' then Result := OpenTSVEditor(FFileName) + else if FileHasModelTsExt(FFileName) then Result := OpenModelEditor(FFileName) else Result := OpenEditor(FFileName, TfrmEditor); end; @@ -1166,6 +1188,11 @@ begin //else Result := OpenEditor(FFileName, TfrmEditor); end; +function TfrmKeymanDeveloper.OpenModelEditor(FFileName: string): TfrmTikeEditor; +begin + Result := OpenEditor(FFileName, TfrmModelEditor); +end; + function TfrmKeymanDeveloper.OpenKPSEditor(FFileName: string): TfrmTikeEditor; begin Result := OpenEditor(FFileName, TfrmPackageEditor); @@ -1414,6 +1441,9 @@ begin if PrevFocus.Visible then PrevFocus.SetFocus; end; + + modActionsKeyboardEditor.actKeyboardCompile.Update; + modActionsModelEditor.actModelCompile.Update; end; procedure TfrmKeymanDeveloper.UDUI_Error(Sender: TUnicodeData; @@ -1460,8 +1490,6 @@ begin else if FGlobalProject.Untitled then Caption := '(Untitled project) - Keyman Developer' else Caption := ChangeFileExt(ExtractFileName(FGlobalProject.FileName), '') + ' - Keyman Developer'; - - Application.Title := Caption; end; procedure TfrmKeymanDeveloper.UpdateChildCaption(Window: TfrmTikeChild); @@ -1522,14 +1550,6 @@ procedure InitClasses; // I3350 const CUserAgent: AnsiString = 'Mozilla/5.0 (compatible; MSIE 11.0; Windows NT 6.1; WOW64; Trident/5.0; TIKE/'+SKeymanVersion+')'; // I4045 begin - with TRegistryErrorControlled.Create do // I3887 - try - if OpenKey(SRegKey_InternetExplorerFeatureBrowserEmulation_CU, True) then // I4436 - WriteInteger('tike.exe', 9000); // I4874 - finally - Free; - end; - OleCheck(UrlMkSetSessionOption(URLMON_OPTION_USERAGENT, PAnsiChar(CUserAgent), Length(CUserAgent), 0)); // I4045 end; diff --git a/windows/src/developer/TIKE/oskbuilder/UframeTouchLayoutBuilder.pas b/windows/src/developer/TIKE/oskbuilder/UframeTouchLayoutBuilder.pas index 8d93a2564b..8c56439806 100644 --- a/windows/src/developer/TIKE/oskbuilder/UframeTouchLayoutBuilder.pas +++ b/windows/src/developer/TIKE/oskbuilder/UframeTouchLayoutBuilder.pas @@ -677,7 +677,7 @@ begin try j.AddPair('x', TJSONNumber.Create(X)); j.AddPair('y', TJSONNumber.Create(Y)); - j.AddPair('text', cdo.Text[cdo.InsertType]); + j.AddPair('text', cdo.Text[cmimCharacter]); // It never makes sense to drop anything other than char BuilderCommand('charmapDragDrop', j); finally j.Free; diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.System.Project.modelTsProjectFile.pas b/windows/src/developer/TIKE/project/Keyman.Developer.System.Project.modelTsProjectFile.pas index fc011d57a0..822f920ee6 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.System.Project.modelTsProjectFile.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.System.Project.modelTsProjectFile.pas @@ -34,7 +34,8 @@ type TmodelTsProjectFile = class(TOpenableProjectFile) private FDebug: Boolean; - FWarnAsError: Boolean; // I4706 + FWarnAsError: Boolean; + FTestKeyboard: string; // I4706 function GetTargetFilename: string; protected @@ -49,10 +50,10 @@ type procedure SaveState(node: IXMLNode); override; // I4698 property Debug: Boolean read FDebug write FDebug; + property TestKeyboard: string read FTestKeyboard write FTestKeyboard; property WarnAsError: Boolean read FWarnAsError write FWarnAsError; // I4706 -// property OutputFilename: string read GetOutputFilename; property TargetFilename: string read GetTargetFilename; end; @@ -80,6 +81,7 @@ procedure TmodelTsProjectFile.SaveState(node: IXMLNode); // I4698 begin inherited SaveState(node); node.AddChild('Debug').NodeValue := FDebug; + node.AddChild('TestKeyboard').NodeValue := FTestKeyboard; end; procedure TmodelTsProjectFile.Load(node: IXMLNode; LoadState: Boolean); // I4698 @@ -95,8 +97,12 @@ begin inherited LoadState(node); try if node.ChildNodes.IndexOf('Debug') >= 0 then FDebug := node.ChildValues['Debug']; + if node.ChildNodes.IndexOf('TestKeyboard') >= 0 + then FTestKeyboard := node.ChildValues['TestKeyboard'] + else FTestKeyboard := ''; except FDebug := False; + FTestKeyboard := ''; end; end; diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas index 2010b129eb..f1a24edd5b 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas @@ -117,14 +117,12 @@ function ShowNewModelProjectParameters(Owner: TComponent): Boolean; implementation uses - Winapi.ShlObj, - Keyman.System.LanguageCodeUtils, Keyman.System.LexicalModelUtils, BCP47Tag, utilstr, - utilsystem, dmActionsMain, + KeymanDeveloperOptions, Keyman.Developer.System.HelpTopics, Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectFile, @@ -179,7 +177,7 @@ end; procedure TfrmNewModelProjectParameters.FormCreate(Sender: TObject); begin inherited; - editPath.Text := GetFolderPath(CSIDL_PERSONAL); + editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath; pack := TKPSFile.Create; pack.LexicalModels.Add(TPackageLexicalModel.Create(pack)); diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas index 226a1644cd..973117f11b 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas @@ -105,13 +105,11 @@ function ShowNewProjectParameters(Owner: TComponent): Boolean; implementation uses - Winapi.ShlObj, - Keyman.System.LanguageCodeUtils, BCP47Tag, utilstr, - utilsystem, dmActionsMain, + KeymanDeveloperOptions, Keyman.Developer.System.HelpTopics, Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectFile, @@ -178,7 +176,7 @@ var i: TKeymanTarget; begin inherited; - editPath.Text := GetFolderPath(CSIDL_PERSONAL); + editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath; pack := TKPSFile.Create; pack.Keyboards.Add(TPackageKeyboard.Create(pack)); diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmProject.pas b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmProject.pas index 82c76dafb3..ab7f2f3bd6 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmProject.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmProject.pas @@ -93,6 +93,7 @@ type procedure SetFocus; override; procedure SetGlobalProject; procedure StartClose; override; + procedure CompileAll; end; implementation @@ -265,6 +266,26 @@ begin frmMessages.Clear; end; +procedure TfrmProject.CompileAll; +var + i: Integer; +begin + ClearMessages; + for i := 0 to FGlobalProject.Files.Count - 1 do + begin + if (FGlobalProject.Files[i] is TkmnProjectFile) or + (FGlobalProject.Files[i] is TmodelTsProjectFile) then + begin + if not (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaCompile, False) then Exit; // I4687 + end; + end; + for i := 0 to FGlobalProject.Files.Count - 1 do + begin + if FGlobalProject.Files[i] is TkpsProjectFile then + if not (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaCompile, False) then Exit; // I4687 + end; +end; + function TfrmProject.ShouldHandleNavigation(URL: string): Boolean; begin Result := False; @@ -489,20 +510,7 @@ begin end else if Command = 'compileall' then // I4686 begin - ClearMessages; - for i := 0 to FGlobalProject.Files.Count - 1 do - begin - if (FGlobalProject.Files[i] is TkmnProjectFile) or - (FGlobalProject.Files[i] is TmodelTsProjectFile) then - begin - if not (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaCompile, False) then Exit; // I4687 - end; - end; - for i := 0 to FGlobalProject.Files.Count - 1 do - begin - if FGlobalProject.Files[i] is TkpsProjectFile then - if not (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaCompile, False) then Exit; // I4687 - end; + CompileAll; end else if Command = 'cleanall' then // I4692 begin diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas index b288af843e..937ed59e42 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.modelTsProjectFileUI.pas @@ -37,6 +37,8 @@ type function GetDebug: Boolean; procedure SetDebug(const Value: Boolean); function CompileModel(FSilent: Boolean): Boolean; + function TestKeymanWeb(FSilent: Boolean): Boolean; + function TestModelState(FCompiledName: string; FSilent: Boolean): Boolean; public function DoAction(action: TProjectFileAction; FSilent: Boolean): Boolean; override; property Debug: Boolean read GetDebug write SetDebug; @@ -51,10 +53,13 @@ uses Vcl.Dialogs, Vcl.Graphics, Vcl.Controls, + dmActionsMain, + KeyboardFonts, + UmodWebHttpServer, UfrmMain, UfrmMDIEditor, - KeyboardFonts, + Keyman.Developer.UI.UfrmModelEditor, KeymanDeveloperUtils, KeymanDeveloperOptions, System.Classes, @@ -64,8 +69,9 @@ uses function TmodelTsProjectFileUI.DoAction(action: TProjectFileAction; FSilent: Boolean): Boolean; begin case action of - pfaCompile: Result := CompileModel(FSilent); - pfaClean: Result := ProjectFile.Clean; + pfaCompile: Result := CompileModel(FSilent); + pfaClean: Result := ProjectFile.Clean; + pfaTestKeymanWeb: Result := TestKeymanWeb(FSilent); else Result := False; end; @@ -108,6 +114,73 @@ begin ProjectFile.Debug := Value; end; +function TmodelTsProjectFileUI.TestKeymanWeb(FSilent: Boolean): Boolean; +var + FCompiledName: string; + editor: TfrmTikeEditor; + wizard: TfrmModelEditor; + FontNames: TKeyboardFontArray; + i: TKeyboardFont; +begin + editor := frmKeymanDeveloper.FindEditorByFileName(ProjectFile.FileName); // I4021 + if not Assigned(editor) or not (editor is TfrmModelEditor) then + Exit(False); + wizard := editor as TfrmModelEditor; + + FCompiledName := ProjectFile.TargetFilename; + if not TestModelState(FCompiledName, FSilent) then + Exit(False); + + for i := Low(TKeyboardFont) to High(TKeyboardFont) do + FontNames[i] := ''; + + if FileExists(ProjectFile.TestKeyboard) then + modWebHttpServer.Debugger.RegisterKeyboard(ProjectFile.TestKeyboard, '1.0', FontNames); + modWebHttpServer.Debugger.RegisterModel(FCompiledName); + + wizard.NotifyStartedWebDebug; // I4021 + + Result := True; +end; + +function TmodelTsProjectFileUI.TestModelState(FCompiledName: string; + FSilent: Boolean): Boolean; +var + ftts, ftjs: TDateTime; +begin + Result := False; + + if not FileExists(FCompiledName) then + if FSilent then + begin + if not CompileModel(FSilent) then Exit; + end + else + case MessageDlg('You need to compile the model before you can continue. Compile now?', + mtConfirmation, mbOkCancel, 0) of + mrOk: if not CompileModel(FSilent) then Exit; + mrCancel: Exit; + end; + + FileAge(ProjectFile.FileName, ftts); + FileAge(FCompiledName, ftjs); + + if ProjectFile.Modified or (ftts > ftjs) then + if FSilent then + begin + if not CompileModel(FSilent) then Exit; + end + else + case MessageDlg('The source file has changed. Recompile before continuing?', + mtConfirmation, mbYesNoCancel, 0) of + mrYes: if not CompileModel(FSilent) then Exit; + mrNo: ; + mrCancel: Exit; + end; + + Result := True; +end; + initialization RegisterProjectFileUIType(TmodelTsProjectFileAction, TmodelTsProjectFileUI); end. diff --git a/windows/src/developer/TIKE/version.in b/windows/src/developer/TIKE/version.in index 24362452fd..17b4e66411 100644 --- a/windows/src/developer/TIKE/version.in +++ b/windows/src/developer/TIKE/version.in @@ -12,9 +12,9 @@ BLOCK "0C0904E4" BEGIN VALUE "CompanyName", "SIL International\0" - VALUE "FileDescription", "Integrated Keyboard Editor\0" + VALUE "FileDescription", "Keyman Developer\0" VALUE "FileVersion", "$VERSION\0" - VALUE "InternalName", "TIKE\0" + VALUE "InternalName", "Keyman Developer\0" VALUE "LegalCopyright", "© SIL International\0" VALUE "LegalTrademarks", "\0" VALUE "OriginalFilename", "TIKE.EXE\0" diff --git a/windows/src/developer/TIKE/xml/help/contexthelp.xml b/windows/src/developer/TIKE/xml/help/contexthelp.xml index 477b03aeaa..ab85f76965 100644 --- a/windows/src/developer/TIKE/xml/help/contexthelp.xml +++ b/windows/src/developer/TIKE/xml/help/contexthelp.xml @@ -239,8 +239,8 @@
- -

TIKE Debugger

+ +

Keyman Developer Debugger

Debug information must be included in the compile keyboard before the debugger process can be started. From the Keyboard menu select Include Debug Information. The debug information will display the error sources when compiling.

The debugger can be used without the debug information by clicking on Test without debugger.

diff --git a/windows/src/developer/TIKE/xml/kmw/index.html b/windows/src/developer/TIKE/xml/kmw/index.html index ec5007592b..7d48be0920 100644 --- a/windows/src/developer/TIKE/xml/kmw/index.html +++ b/windows/src/developer/TIKE/xml/kmw/index.html @@ -59,6 +59,7 @@ .touch-device #install-link, .touch-device #install-link-packages, + .touch-device #model-link, .touch-device-ios #install-link-ios, .touch-device-android #install-link-android { display: inline-block; @@ -132,10 +133,6 @@ - - - - @@ -156,7 +153,15 @@

Keyboard Test

- +
+ Models +
+
+ No models are currently loaded. +
+
+ Install packages +
@@ -322,7 +327,62 @@ //document.getElementById('ta1').focus(); keyman.moveToElement('ta1'); }, 10); + + document.getElementById('model-link').addEventListener('click', modelLinkClick); } + + /* Lexical models */ + + var firstModel = true; + + /** + * Register a model for debugging. Called by keyboards.js. The + * first model registered will be activated automatically. + */ + function registerModel(model, src) { + var list = document.getElementById('model-list-inner'); + var a = document.createElement('a'); + a.href = '#'; + a.innerText = model; + a.addEventListener('click', function(ev) { + ev.returnValue = false; + + var lastModel = keyman.modelManager.activeModel; + if(lastModel) + keyman.modelManager.deregister(lastModel.id); + keyman.modelManager.register({ + id: model, + languages: ['en'], + path: 'http://'+location.host+'/model/'+src + }); + }); + list.appendChild(a); + + if(firstModel) { + keyman.modelManager.register({ + id: model, + languages: ['en'], + path: 'http://'+location.host+'/model/'+src + }); + document.getElementById('model-list-empty').style.display = 'none'; + firstModel = false; + } + } + + /** + * Toggle visibility of the model menu + */ + function modelLinkClick(e) { + var elem = document.getElementById('model-list'); + elem.className = elem.className == '' ? 'model-list-visible' : ''; + e.returnValue = false; + return false; + } + + + + + diff --git a/windows/src/developer/TIKE/xml/kmw/test.css b/windows/src/developer/TIKE/xml/kmw/test.css index 7feae12984..47c98987cf 100644 --- a/windows/src/developer/TIKE/xml/kmw/test.css +++ b/windows/src/developer/TIKE/xml/kmw/test.css @@ -1,55 +1,81 @@ - html, body { - margin: 0; - padding: 0; - } +html, body { + margin: 0; + padding: 0; +} - h2 { - font-family: sans-serif - } +h2 { + font-family: sans-serif +} - .install-link { - background: none repeat scroll 0 0 #CCCCCC; - border: 1px solid #444444; - border-radius: 4px; - box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.25); - display: inline-block; - color: #444444; - font-family: Sans-serif; - font-size: 10pt; - padding: 6px; - margin: 12px 2px 0 2px; - text-decoration: none; - } +.install-link { + background: none repeat scroll 0 0 #CCCCCC; + border: 1px solid #444444; + border-radius: 4px; + box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.25); + display: inline-block; + color: #444444; + font-family: Sans-serif; + font-size: 10pt; + padding: 6px; + margin: 12px 2px 0 2px; + text-decoration: none; +} - .header { - padding: 4px 8px; - background: #424248; - color: #f0f0ff; - } +.header { + padding: 4px 8px; + background: #424248; + color: #f0f0ff; +} - .header div { - display: inline-block; - } +.header div { + display: inline-block; +} - .header div h2 { - margin: 4px; - } +.header div h2 { + margin: 4px; +} - .header div.float-right { - display: block; - position: absolute; - right: 0; - top: 0; - } +.header div.float-right { + display: block; + position: absolute; + right: 0; + top: 0; +} - .float-right a.install-link { - margin: 8px 8px 0 0 ; - } +.float-right a.install-link { + margin: 8px 8px 0 0 ; +} - .clear { - clear: both; - } +.clear { + clear: both; +} - #content { - margin: 8px 16px; - } +#content { + margin: 8px 16px; +} + +/* Lexical model menu */ + +.model-list-visible#model-list { + display: block; +} + +#model-list { + display: none; + position: absolute; + z-index: 3; + margin-top: 4px; + padding: 5px 5px 5px 5px; + background: #424248; + border-left: solid 1px #442244; + border-right: solid 1px #442244; + border-bottom: solid 1px #442244; + border-radius: 0 0 4px 4px; + box-shadow: 4px 4px 4px rgba(64,64,64,0.5); +} + +#model-list a { + display: block; + color: white; + padding: 6px; +} \ No newline at end of file diff --git a/windows/src/developer/TIKE/xml/project/common.js b/windows/src/developer/TIKE/xml/project/common.js index 263d3ba322..dc07ad0555 100644 --- a/windows/src/developer/TIKE/xml/project/common.js +++ b/windows/src/developer/TIKE/xml/project/common.js @@ -204,7 +204,12 @@ function ShowMenu(name,align) { menudiv.style.visibility='visible'; if(align=='right') menudiv.style.left = (pb.x+button.offsetWidth-menudiv.offsetWidth) + 'px'; else menudiv.style.left = pb.x + 'px'; - menudiv.style.top = (pb.y+button.offsetHeight) + 'px'; + + if(pb.y + button.offsetHeight + menudiv.offsetHeight > window.innerHeight - 4) { + menudiv.style.top = (pb.y - menudiv.offsetHeight) + 'px'; + } else { + menudiv.style.top = (pb.y + button.offsetHeight) + 'px'; + } document.getElementById('menubackground').className = 'show'; } diff --git a/windows/src/developer/history.md b/windows/src/developer/history.md index 234b453403..6fda4ef033 100644 --- a/windows/src/developer/history.md +++ b/windows/src/developer/history.md @@ -1,7 +1,23 @@ # Keyman Developer Version History ## 13.0 alpha -* Start version 13.0 +* Feature: Add unsupported kmdecomp decompiler utility (#2419) +* Feature: Add x64 version of kmcomp for macOS (#2352) +* Feature: Hotkeys defined in .kmn no longer need to be quoted (#2432) +* Feature: Show QRCode for web debugger URLs in Keyboard and Package editors (#2433) +* Feature: F7 in Project window compiles the whole project (#2442) +* Feature: Default Project Path is now Documents\Keyman Developer\Projects (#2449) +* Bug Fix: Remove version info from .kps when follow-keyboard-version is set (#2417) +* Bug Fix: Make sure popup menus are never obscured in Project view (#2415) +* Bug Fix: 'Show Help' was not working on Distribution tab in Project view (#2397) +* Bug Fix: Missing author email was causing lexical model compiler to fail (#2388) +* Bug Fix: Keyboard ID was not clean by default with Import Windows Keyboard (#2431) +* Bug Fix: Zero-length store names are no longer permitted (#2443) +* Change: Rename UI references of 'TIKE' to 'Keyman Developer' (#2439) +* Change: Application title no longer shows active project, only main window (#2441) + +## 2019-12-06 12.0.62 stable +* Bug Fix: Shortcuts in text editors were not working after #2331 (#2424) ## 2019-11-18 12.0.55 stable * Bug Fix: Some keyboards were incorrectly marked as mobile-capable (#2334) diff --git a/windows/src/developer/inst/Makefile b/windows/src/developer/inst/Makefile index 9b274f1e89..53a189e9b6 100644 --- a/windows/src/developer/inst/Makefile +++ b/windows/src/developer/inst/Makefile @@ -23,6 +23,7 @@ setup: $(MAKE) -fcopydev.mak candle $(WIXLIGHT) -sice:ICE91 -sice:ICE60 -dWixUILicenseRtf=License.rtf -out keymandeveloper.msi -ext WixUIExtension $(DEVELOPER_FILES) + $(MAKE) -fcopydev.mak clean-heat # # Sign the installation archive diff --git a/windows/src/developer/inst/copydev.in b/windows/src/developer/inst/copydev.in index 2270783b07..560721d977 100644 --- a/windows/src/developer/inst/copydev.in +++ b/windows/src/developer/inst/copydev.in @@ -33,7 +33,9 @@ candle: heat-cef heat-xml heat-templates heat-model-compiler $(WIXCANDLE) -dVERSION=$VERSION -dRELEASE=$RELEASE -dXmlSourceDir=$(ROOT)\src\developer\TIKE\xml xml.wxs $(WIXCANDLE) -dVERSION=$VERSION -dRELEASE=$RELEASE -dCefSourceDir=$(KEYMAN_CEF4DELPHI_ROOT) cef.wxs $(WIXCANDLE) -dVERSION=$VERSION -dRELEASE=$RELEASE -dTemplatesSourceDir=$(KEYMAN_DEVELOPER_TEMPLATES_ROOT) templates.wxs - $(WIXCANDLE) -dVERSION=$VERSION -dRELEASE=$RELEASE -dModelCompilerSourceDir=$(KEYMAN_MODELCOMPILER_ROOT) kmlmc.wxs + $(WIXCANDLE) -dVERSION=$VERSION -dRELEASE=$RELEASE -dModelCompilerSourceDir=$(KEYMAN_WIX_TEMP_MODELCOMPILER) kmlmc.wxs + +clean-heat: clean-heat-model-compiler heat-xml: # We copy the files to a temp folder in order to exclude thumbs.db, .vs, etc from harvesting @@ -103,6 +105,8 @@ heat-model-compiler: cd $(ROOT)\src\developer\inst $(WIXHEAT) dir $(KEYMAN_WIX_TEMP_MODELCOMPILER) -o kmlmc.wxs -ag -cg ModelCompiler -dr INSTALLDIR -var var.ModelCompilerSourceDir -wx -nologo +clean-heat-model-compiler: + # the production build generates files that are not in source, e.g. .ps1 scripts # When we candle/light build, we can grab the source files from the proper root so go ahead and delete the temp folder again -rmdir /s/q $(KEYMAN_WIX_TEMP_MODELCOMPILER) diff --git a/windows/src/developer/inst/kmdev.wxs b/windows/src/developer/inst/kmdev.wxs index bb285d5237..823460b18e 100644 --- a/windows/src/developer/inst/kmdev.wxs +++ b/windows/src/developer/inst/kmdev.wxs @@ -166,6 +166,14 @@ + + + + + + + + diff --git a/windows/src/developer/kmcmpdll/Compiler.cpp b/windows/src/developer/kmcmpdll/Compiler.cpp index a4c8623d6b..c8ce439669 100644 --- a/windows/src/developer/kmcmpdll/Compiler.cpp +++ b/windows/src/developer/kmcmpdll/Compiler.cpp @@ -1922,21 +1922,21 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma case 0: if(_wcsnicmp(p, L"deadkey", z = 7) == 0 || _wcsnicmp(p, L"dk", z = 2) == 0 ) - { + { p += z; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidDeadkey; + if(!q || !*q) return CERR_InvalidDeadkey; DWORD n = fk->cxDeadKeyArray; - tstr[mx++] = UC_SENTINEL; + tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_DEADKEY; if(!strvalidchrs(q, DeadKeyChars)) return CERR_InvalidDeadkey; tstr[mx++] = GetDeadKey(fk, q); //atoiW(q); 7-5-01: named deadkeys tstr[mx] = 0; } else - { + { n = xatoi(&p); if(*p != '\0' && !iswspace(*p)) return CERR_InvalidValue; if((err = UTF32ToUTF16(n, &n1, &n2)) != CERR_None) return err; @@ -1970,7 +1970,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_AnyInVirtualKeySection; p += 3; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidAny; + if(!q || !*q) return CERR_InvalidAny; for(i = 0; i < fk->cxStoreArray; i++) { @@ -2001,6 +2001,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_InvalidInVirtualKeySection; p += 10; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); + if (!q || !*q) return CERR_InvalidToken; err = process_baselayout(fk, q, tstr, &mx); if(err != CERR_None) return err; } @@ -2015,7 +2016,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_InvalidInVirtualKeySection; p += 2; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidIf; + if(!q || !*q) return CERR_InvalidIf; err = process_if(fk, q, tstr, &mx); if(err != CERR_None) return err; @@ -2027,7 +2028,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma p += 5; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidIndex; + if(!q || !*q) return CERR_InvalidIndex; { wchar_t *context = NULL; @@ -2058,7 +2059,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_OutsInVirtualKeySection; p += 4; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidOuts; + if(!q || !*q) return CERR_InvalidOuts; for(i = 0; i < fk->cxStoreArray; i++) { @@ -2083,7 +2084,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma p += 7; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(q) + if(q && *q) { VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_Contextn); int n1; @@ -2114,7 +2115,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_CallInVirtualKeySection; p += 4; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidCall; + if(!q || !*q) return CERR_InvalidCall; for(i = 0; i < fk->cxStoreArray; i++) { @@ -2142,7 +2143,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_AnyInVirtualKeySection; p += 6; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidAny; + if(!q || !*q) return CERR_InvalidAny; for(i = 0; i < fk->cxStoreArray; i++) { @@ -2182,7 +2183,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma p += 3; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidUse; + if(!q || !*q) return CERR_InvalidUse; tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_USE; tstr[mx] = GetGroupNum(fk, q); @@ -2196,7 +2197,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_InvalidInVirtualKeySection; p += 5; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidReset; + if(!q || !*q) return CERR_InvalidReset; err = process_reset(fk, q, tstr, &mx); if(err != CERR_None) return err; @@ -2366,7 +2367,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma VERIFY_KEYBOARD_VERSION(fk, VERSION_80, CERR_80FeatureOnly); p += 3; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidSet; + if(!q || !*q) return CERR_InvalidSet; err = process_set(fk, q, tstr, &mx); if(err != CERR_None) return err; @@ -2376,7 +2377,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma VERIFY_KEYBOARD_VERSION(fk, VERSION_80, CERR_80FeatureOnly); p += 4; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidSave; + if(!q || !*q) return CERR_InvalidSave; err = process_save(fk, q, tstr, &mx); if(err != CERR_None) return err; @@ -2386,7 +2387,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(_wcsnicmp(p, L"switch", 6) != 0) return CERR_InvalidToken; p += 6; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); - if(!q) return CERR_InvalidSwitch; + if(!q || !*q) return CERR_InvalidSwitch; tstr[mx++] = UC_SENTINEL; tstr[mx++] = CODE_SWITCH; tstr[mx++] = atoiW(q); @@ -2431,6 +2432,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_InvalidInVirtualKeySection; p += 8; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); + if (!q || !*q) return CERR_InvalidToken; err = process_platform(fk, q, tstr, &mx); if(err != CERR_None) return err; continue; @@ -2440,6 +2442,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma if(sFlag) return CERR_InvalidInVirtualKeySection; p += 5; q = GetDelimitedString(&p, L"()", GDS_CUTLEAD | GDS_CUTFOLL); + if (!q || !*q) return CERR_InvalidToken; err = process_set_synonym(TSS_LAYER, fk, q, tstr, &mx); if(err != CERR_None) return err; continue; @@ -2792,6 +2795,8 @@ DWORD ProcessEthnologueStore(PWSTR p) // I2646 return res; } +#define K_HOTKEYSHIFTFLAGS (K_SHIFTFLAG | K_CTRLFLAG | K_ALTFLAG | ISVIRTUALKEY) + DWORD ProcessHotKey(PWSTR p, DWORD *hk) { PWSTR q, r; @@ -2800,6 +2805,25 @@ DWORD ProcessHotKey(PWSTR p, DWORD *hk) *hk = 0; + if(*p == UC_SENTINEL && *(p+1) == CODE_EXTENDED) { + WORD Key = *(p + 3); + WORD ShiftFlags = *(p + 2); + + // Convert virtual key to hotkey (different bitflags) + + if (ShiftFlags & ~K_HOTKEYSHIFTFLAGS) { + AddWarning(CWARN_HotkeyHasInvalidModifier); + } + + if (ShiftFlags & K_SHIFTFLAG) *hk |= HK_SHIFT; + if (ShiftFlags & K_CTRLFLAG) *hk |= HK_CTRL; + if (ShiftFlags & K_ALTFLAG) *hk |= HK_ALT; + + *hk |= Key; + + return CERR_None; + } + q = wcschr(p, '['); if(q) { diff --git a/windows/src/developer/kmcmpdll/Compiler.rc b/windows/src/developer/kmcmpdll/Compiler.rc index 33325d1a84..888c924bac 100644 --- a/windows/src/developer/kmcmpdll/Compiler.rc +++ b/windows/src/developer/kmcmpdll/Compiler.rc @@ -222,5 +222,6 @@ END STRINGTABLE BEGIN CWARN_LanguageHeadersDeprecatedInKeyman10 "This language header has been deprecated in Keyman 10. Instead, add language metadata in the package file" + CWARN_HotkeyHasInvalidModifier "Hotkey has modifiers that are not supported. Use only SHIFT, CTRL and ALT" CINFO_NonUnicodeFile "Keyman Developer has detected that the file has ANSI encoding. Consider converting this file to UTF-8" END diff --git a/windows/src/developer/kmcmpdll/kcframe.cpp b/windows/src/developer/kmcmpdll/kcframe.cpp index 2ec24edc2a..2d41947135 100644 --- a/windows/src/developer/kmcmpdll/kcframe.cpp +++ b/windows/src/developer/kmcmpdll/kcframe.cpp @@ -37,9 +37,8 @@ int main(int argc, char *argv[]) if(argc < 3) { puts("Usage: kcframe infile.kmn outfile.kmx"); - return 0; + return 1; } - CompileKeyboardFile(argv[1], argv[2], TRUE, FALSE, TRUE, msgproc); // I4865 // I4866 - return 1; + return CompileKeyboardFile(argv[1], argv[2], TRUE, FALSE, TRUE, msgproc) ? 0 : 1; // I4865 // I4866 } diff --git a/windows/src/developer/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas b/windows/src/developer/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas index 98c0650429..81ecdeb749 100644 --- a/windows/src/developer/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas +++ b/windows/src/developer/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas @@ -73,6 +73,7 @@ uses Keyman.Developer.System.ImportKeyboardDLL, Keyman.Developer.System.TouchLayoutToVisualKeyboardConverter, Keyman.System.Util.RenderLanguageIcon, + Keyman.System.KeyboardUtils, KeymanVersion, KeyboardParser, kmxfileconsts, @@ -107,7 +108,7 @@ begin r.ValueExists(SRegValue_KeyboardLayoutText) then Exit(False); - KeyboardID := ChangeFileExt(r.ReadString(SRegValue_KeyboardLayoutFile), ''); + KeyboardID := TKeyboardUtils.CleanKeyboardID(ChangeFileExt(r.ReadString(SRegValue_KeyboardLayoutFile), '')); Name := r.ReadString(SRegValue_KeyboardLayoutText); finally r.Free; diff --git a/windows/src/developer/kmdecomp/Makefile b/windows/src/developer/kmdecomp/Makefile new file mode 100644 index 0000000000..4fa5b3ef8e --- /dev/null +++ b/windows/src/developer/kmdecomp/Makefile @@ -0,0 +1,28 @@ +# +# kmdecomp Makefile +# + +!include ..\..\Defines.mak + + +build: version.res dirs + $(MSBUILD) kmdecomp.sln $(MSBUILD_BUILD) /p:Platform=Win32 + $(COPY) $(WIN32_TARGET_PATH)\kmdecomp.exe $(PROGRAM)\developer + $(COPY) $(WIN32_TARGET_PATH)\kmdecomp.pdb $(DEBUGPATH)\developer + +clean: def-clean + $(MSBUILD) kmdecomp.sln $(MSBUILD_CLEAN) + +signcode: + $(SIGNCODE) /d "Keyman Developer Decompiler" $(PROGRAM)\developer\kmdecomp.exe + +backup: + $(WZZIP) $(BUILD)\developer\kmdecomp.zip $(BACKUPDEFAULTS) $(PROGRAM)\developer\kmdecomp.exe + +test-manifest: + @rem This target needed as dependency for KMDECOMP + +install: + $(COPY) $(PROGRAM)\developer\kmdecomp.exe "$(INSTALLPATH_KEYMANDEVELOPER)\kmdecomp.exe" + +!include ..\..\Target.mak diff --git a/windows/src/support/kmdecomp/crc32.cpp b/windows/src/developer/kmdecomp/crc32.cpp similarity index 100% rename from windows/src/support/kmdecomp/crc32.cpp rename to windows/src/developer/kmdecomp/crc32.cpp diff --git a/windows/src/support/kmdecomp/kmdecomp.cpp b/windows/src/developer/kmdecomp/kmdecomp.cpp similarity index 95% rename from windows/src/support/kmdecomp/kmdecomp.cpp rename to windows/src/developer/kmdecomp/kmdecomp.cpp index 4d9d5a46db..b7fa693687 100644 --- a/windows/src/support/kmdecomp/kmdecomp.cpp +++ b/windows/src/developer/kmdecomp/kmdecomp.cpp @@ -57,9 +57,9 @@ int main(int argc, char *argv[]) if(!LoadKeyboard(argv[1], &kbd, &lpBitmap, &cbBitmap)) return 2; - _splitpath(argv[1], drive, dir, filename, NULL); - _makepath(buf, drive, dir, filename, ".kmn"); - _makepath(buf2, drive, dir, filename, ".bmp"); + _splitpath_s(argv[1], drive, _MAX_DRIVE, dir, _MAX_DIR, filename, _MAX_FNAME, NULL, 0); + _makepath_s(buf, _MAX_PATH, drive, dir, filename, ".kmn"); + _makepath_s(buf2, _MAX_PATH, drive, dir, filename, ".bmp"); int n = SaveKeyboardSource(kbd, lpBitmap, cbBitmap, buf, buf2); diff --git a/windows/src/developer/kmdecomp/kmdecomp.md b/windows/src/developer/kmdecomp/kmdecomp.md new file mode 100644 index 0000000000..7a7bbc3ca6 --- /dev/null +++ b/windows/src/developer/kmdecomp/kmdecomp.md @@ -0,0 +1,9 @@ +# kmdecomp Keyman Decompiler + +This unsupported utility decompiles a Keyman .kmx keyboard. +It will produce a .kmn source file and optionally a .ico or +.bmp image. The source file may not contain symbols and will +include optimisations that were made by the compiler such as +expanding `any()` rules in key components into multiple rules. + +Usage: kmdecomp diff --git a/windows/src/support/kmdecomp/kmdecomp.sln b/windows/src/developer/kmdecomp/kmdecomp.sln similarity index 100% rename from windows/src/support/kmdecomp/kmdecomp.sln rename to windows/src/developer/kmdecomp/kmdecomp.sln diff --git a/windows/src/support/kmdecomp/kmdecomp.vcxproj b/windows/src/developer/kmdecomp/kmdecomp.vcxproj similarity index 87% rename from windows/src/support/kmdecomp/kmdecomp.vcxproj rename to windows/src/developer/kmdecomp/kmdecomp.vcxproj index efea591be5..11433424c0 100644 --- a/windows/src/support/kmdecomp/kmdecomp.vcxproj +++ b/windows/src/developer/kmdecomp/kmdecomp.vcxproj @@ -41,11 +41,8 @@ <_ProjectFileVersion>10.0.30319.1 - .\Debug\ - .\Debug\ + $(Platform)\$(Configuration)\ true - .\Release\ - .\Release\ false AllRules.ruleset @@ -54,6 +51,13 @@ + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + + + $(SolutionDir)$(Platform)\$(Configuration)\ + .\Debug/kmdecomp.tlb @@ -64,13 +68,8 @@ Disabled ..\..\global\inc;..\..\resellers;%(AdditionalIncludeDirectories) WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true EnableFastChecks MultiThreadedDebug - .\Debug/kmdecomp.pch - .\Debug/ - .\Debug/ - .\Debug/ Level3 true EditAndContinue @@ -80,10 +79,8 @@ 0x0c09 - .\Debug/kmdecomp.exe true true - .\Debug/kmdecomp.pdb Console MachineX86 true @@ -107,10 +104,6 @@ true MultiThreaded true - .\Release/kmdecomp.pch - .\Release/ - .\Release/ - .\Release/ Level3 true true @@ -120,9 +113,7 @@ 0x0c09 - .\Release/kmdecomp.exe true - .\Release/kmdecomp.pdb Console MachineX86 true @@ -152,6 +143,9 @@ %(PreprocessorDefinitions) + + + diff --git a/windows/src/support/kmdecomp/kmdecomp.vcxproj.filters b/windows/src/developer/kmdecomp/kmdecomp.vcxproj.filters similarity index 89% rename from windows/src/support/kmdecomp/kmdecomp.vcxproj.filters rename to windows/src/developer/kmdecomp/kmdecomp.vcxproj.filters index ed410a7c7b..20bb3504a7 100644 --- a/windows/src/support/kmdecomp/kmdecomp.vcxproj.filters +++ b/windows/src/developer/kmdecomp/kmdecomp.vcxproj.filters @@ -25,4 +25,9 @@ Source Files + + + Source Files + + \ No newline at end of file diff --git a/windows/src/support/kmdecomp/savekeyboard.cpp b/windows/src/developer/kmdecomp/savekeyboard.cpp similarity index 83% rename from windows/src/support/kmdecomp/savekeyboard.cpp rename to windows/src/developer/kmdecomp/savekeyboard.cpp index 9465269500..74bc135147 100644 --- a/windows/src/support/kmdecomp/savekeyboard.cpp +++ b/windows/src/developer/kmdecomp/savekeyboard.cpp @@ -43,9 +43,9 @@ void SaveBitmapFile(LPBYTE lpBitmap, DWORD cbBitmap, char *bmpfile); LPKEYBOARD g_kbd; -PWCHAR wcscat2(PWCHAR c1, const PWCHAR c2) +PWCHAR wcscat2(PWCHAR c1, size_t sz, const PWCHAR c2) { - wcscat(c1, c2); + wcscat_s(c1, sz, c2); return wcschr(c1, 0); } @@ -134,23 +134,23 @@ PWCHAR flagstr(int flag) { static WCHAR buf[256]; *buf = 0; - if(flag & LCTRLFLAG) wcscat2(buf, L"LCTRL "); - if(flag & RCTRLFLAG) wcscat2(buf, L"RCTRL "); - if(flag & LALTFLAG) wcscat2(buf, L"LALT "); - if(flag & RALTFLAG) wcscat2(buf, L"RALT "); - if(flag & K_SHIFTFLAG) wcscat2(buf, L"SHIFT "); - if(flag & K_CTRLFLAG) wcscat2(buf, L"CTRL "); - if(flag & K_ALTFLAG) wcscat2(buf, L"ALT "); - if(flag & CAPITALFLAG) wcscat2(buf, L"CAPS "); - if(flag & NOTCAPITALFLAG) wcscat2(buf, L"NCAPS "); - if(flag & NUMLOCKFLAG) wcscat2(buf, L"NUM "); - if(flag & NOTNUMLOCKFLAG) wcscat2(buf, L"NNUM "); - if(flag & SCROLLFLAG) wcscat2(buf, L"SCROLL "); - if(flag & NOTSCROLLFLAG) wcscat2(buf, L"NSCROLL "); + if(flag & LCTRLFLAG) wcscat2(buf, _countof(buf), L"LCTRL "); + if(flag & RCTRLFLAG) wcscat2(buf, _countof(buf), L"RCTRL "); + if(flag & LALTFLAG) wcscat2(buf, _countof(buf), L"LALT "); + if(flag & RALTFLAG) wcscat2(buf, _countof(buf), L"RALT "); + if(flag & K_SHIFTFLAG) wcscat2(buf, _countof(buf), L"SHIFT "); + if(flag & K_CTRLFLAG) wcscat2(buf, _countof(buf), L"CTRL "); + if(flag & K_ALTFLAG) wcscat2(buf, _countof(buf), L"ALT "); + if(flag & CAPITALFLAG) wcscat2(buf, _countof(buf), L"CAPS "); + if(flag & NOTCAPITALFLAG) wcscat2(buf, _countof(buf), L"NCAPS "); + if(flag & NUMLOCKFLAG) wcscat2(buf, _countof(buf), L"NUM "); + if(flag & NOTNUMLOCKFLAG) wcscat2(buf, _countof(buf), L"NNUM "); + if(flag & SCROLLFLAG) wcscat2(buf, _countof(buf), L"SCROLL "); + if(flag & NOTSCROLLFLAG) wcscat2(buf, _countof(buf), L"NSCROLL "); return buf; } -PWCHAR GetVKeyName(LPKEY key) // I3438 +PCWCHAR GetVKeyName(LPKEY key) // I3438 { static WCHAR buf[100]; if(key->Key <= VK__MAX) @@ -187,9 +187,10 @@ PWCHAR ifvalue(WCHAR ch) return L"="; } +#define BUFSIZE 512 PWCHAR ExtString(PWCHAR str) { - static WCHAR buf[2][512], bufpointer = 0; // allows for multiple strings in one printf + static WCHAR buf[2][BUFSIZE], bufpointer = 0; // allows for multiple strings in one printf // dodgy hack? PWCHAR p; @@ -203,7 +204,7 @@ PWCHAR ExtString(PWCHAR str) if(*str == UC_SENTINEL) { str++; - if(inquotes) p = wcscat2(p, L"\" "); + if(inquotes) p = wcscat2(p, BUFSIZE, L"\" "); inquotes = 0; switch(*str) { @@ -219,10 +220,10 @@ PWCHAR ExtString(PWCHAR str) p = wcschr(p, 0); break; case CODE_CONTEXT: - p = wcscat2(p, L"context "); + p = wcscat2(p, BUFSIZE, L"context "); break; case CODE_NUL: - p = wcscat2(p, L"nul "); + p = wcscat2(p, BUFSIZE, L"nul "); break; case CODE_USE: str++; @@ -230,10 +231,10 @@ PWCHAR ExtString(PWCHAR str) p = wcschr(p, 0); break; case CODE_RETURN: - p = wcscat2(p, L"return "); + p = wcscat2(p, BUFSIZE, L"return "); break; case CODE_BEEP: - p = wcscat2(p, L"beep "); + p = wcscat2(p, BUFSIZE, L"beep "); break; case CODE_DEADKEY: str++; @@ -257,13 +258,13 @@ PWCHAR ExtString(PWCHAR str) p = wcschr(p, 0); break; case CODE_SWITCH: - p = wcscat2(p, L"switch "); + p = wcscat2(p, BUFSIZE, L"switch "); break; case CODE_KEY: - p = wcscat2(p, L"key "); + p = wcscat2(p, BUFSIZE, L"key "); break; case CODE_CLEARCONTEXT: - p = wcscat2(p, L"clearcontext "); + p = wcscat2(p, BUFSIZE, L"clearcontext "); break; case CODE_CALL: str++; @@ -321,7 +322,7 @@ PWCHAR ExtString(PWCHAR str) break; default: - p = wcscat2(p, L"unknown() "); + p = wcscat2(p, BUFSIZE, L"unknown() "); break; } } @@ -329,13 +330,13 @@ PWCHAR ExtString(PWCHAR str) { if(*str == L'"') { - if(inquotes) p = wcscat2(p, L"\" "); + if(inquotes) p = wcscat2(p, BUFSIZE, L"\" "); inquotes = 0; - p = wcscat2(p, L"'\"' "); + p = wcscat2(p, BUFSIZE, L"'\"' "); } else if(*str < 32) { - if(inquotes) p = wcscat2(p, L"\" "); + if(inquotes) p = wcscat2(p, BUFSIZE, L"\" "); inquotes = 0; wsprintfW(p, L"x%x ", *str); p = wcschr(p, 0); @@ -345,7 +346,7 @@ PWCHAR ExtString(PWCHAR str) if(!inquotes) { inquotes = 1; - p = wcscat2(p, L"\""); + p = wcscat2(p, BUFSIZE, L"\""); } *p++ = *str; *p = 0; @@ -353,7 +354,7 @@ PWCHAR ExtString(PWCHAR str) } } - if(inquotes) p = wcscat2(p, L"\" "); + if(inquotes) p = wcscat2(p, BUFSIZE, L"\" "); return buf[bufpointer]; } @@ -376,10 +377,12 @@ int SaveKeyboardSource(LPKEYBOARD kbd, LPBYTE lpBitmap, DWORD cbBitmap, char *fi g_kbd = kbd; - fp = fopen(filename, "wb"); - fwrite(UTF16Sig, 2, 1, fp); + if (fopen_s(&fp, filename, "wb") != 0) { + Err("Unable to create output file."); + return 3; + } - if(!fp) { Err("Unable to create output file."); return 3; } + fwrite(UTF16Sig, 2, 1, fp); wsprintfW(buf, L"c Keyboard created by KMDECOMP\n"); wr(fp, buf); wsprintfW(buf, L"c\n"); wr(fp, buf); @@ -450,7 +453,9 @@ int SaveKeyboardSource(LPKEYBOARD kbd, LPBYTE lpBitmap, DWORD cbBitmap, char *fi void SaveBitmapFile(LPBYTE lpBitmap, DWORD cbBitmap, char *bmpfile) { - FILE * fp = fopen(bmpfile, "wb"); - fwrite(lpBitmap, cbBitmap, 1, fp); - fclose(fp); + FILE *fp; + if (fopen_s(&fp, bmpfile, "wb") == 0) { + fwrite(lpBitmap, cbBitmap, 1, fp); + fclose(fp); + } } diff --git a/windows/src/developer/kmdecomp/version.in b/windows/src/developer/kmdecomp/version.in new file mode 100644 index 0000000000..5ed1ddfa85 --- /dev/null +++ b/windows/src/developer/kmdecomp/version.in @@ -0,0 +1,30 @@ +1 VERSIONINFO + FILEVERSION $VERSIONNUM + PRODUCTVERSION $VERSIONNUM + FILEFLAGSMASK 0x3fL + FILEFLAGS 0x0L + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "0C0904E4" + BEGIN + VALUE "CompanyName", "SIL International\0" + VALUE "FileDescription", "Keyman decompiler\0" + VALUE "FileVersion", "$VERSION\0" + VALUE "InternalName", "KMDECOMP\0" + VALUE "LegalCopyright", "� SIL International\0" + VALUE "LegalTrademarks", "\0" + VALUE "OriginalFilename", "KMDECOMP.EXE\0" + VALUE "ProductName", "Keyman Developer\0" + VALUE "ProductVersion", "$VERSION\0" + VALUE "Comments", "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0xc09, 1252 + END + END diff --git a/windows/src/ext/zxingqrcode/LICENSE.md b/windows/src/ext/zxingqrcode/LICENSE.md new file mode 100644 index 0000000000..e06d208186 --- /dev/null +++ b/windows/src/ext/zxingqrcode/LICENSE.md @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/windows/src/ext/zxingqrcode/README.md b/windows/src/ext/zxingqrcode/README.md new file mode 100644 index 0000000000..a3bd674683 --- /dev/null +++ b/windows/src/ext/zxingqrcode/README.md @@ -0,0 +1,21 @@ +DelphiZXingQRCode +================= + +DelphiZXingQRCode is a Delphi port of the QR Code functionality from ZXing, an open source +barcode image processing library. The code was ported to Delphi by Senior Debenu Developer, +Kevin Newman. The port retains the original Apache License (v2.0). + +DelphiZXingQRCode Project + +http://www.debenu.com/open-source/delphizxingqrcode-open-source-delphi-qr-code-generator/ + +ZXing + +https://github.com/zxing/zxing + +# Getting Started # + +A sample Delphi project is provided in the TestApp folder to demonstrate how to use DelphiZXingQRCode. +Simply add the DelphiZXIngQRCode.pas to the DelphiZXingQRCodeTestApp Delphi project and compile. + +[Provided by Debenu] diff --git a/windows/src/ext/zxingqrcode/Source/DelphiZXIngQRCode.pas b/windows/src/ext/zxingqrcode/Source/DelphiZXIngQRCode.pas new file mode 100644 index 0000000000..6f54de94c6 --- /dev/null +++ b/windows/src/ext/zxingqrcode/Source/DelphiZXIngQRCode.pas @@ -0,0 +1,3573 @@ +unit DelphiZXingQRCode; + +// ZXing QRCode port to Delphi, by Debenu Pty Ltd (www.debenu.com) + +// Original copyright notice +(* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *) + +interface + +type + TQRCodeEncoding = (qrAuto, qrNumeric, qrAlphanumeric, qrISO88591, qrUTF8NoBOM, qrUTF8BOM); + T2DBooleanArray = array of array of Boolean; + + TDelphiZXingQRCode = class + protected + FData: WideString; + FRows: Integer; + FColumns: Integer; + FEncoding: TQRCodeEncoding; + FQuietZone: Integer; + FElements: T2DBooleanArray; + procedure SetEncoding(NewEncoding: TQRCodeEncoding); + procedure SetData(const NewData: WideString); + procedure SetQuietZone(NewQuietZone: Integer); + function GetIsBlack(Row, Column: Integer): Boolean; + procedure Update; + public + constructor Create; + property Data: WideString read FData write SetData; + property Encoding: TQRCodeEncoding read FEncoding write SetEncoding; + property QuietZone: Integer read FQuietZone write SetQuietZone; + property Rows: Integer read FRows; + property Columns: Integer read FColumns; + property IsBlack[Row, Column: Integer]: Boolean read GetIsBlack; + end; + +implementation + +uses + contnrs, Math, Classes; + +type + TByteArray = array of Byte; + T2DByteArray = array of array of Byte; + TIntegerArray = array of Integer; + +const + NUM_MASK_PATTERNS = 8; + + QUIET_ZONE_SIZE = 4; + + ALPHANUMERIC_TABLE: array[0..95] of Integer = ( + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1 // 0x50-0x5f + ); + + DEFAULT_BYTE_MODE_ENCODING = 'ISO-8859-1'; + + POSITION_DETECTION_PATTERN: array[0..6, 0..6] of Integer = ( + (1, 1, 1, 1, 1, 1, 1), + (1, 0, 0, 0, 0, 0, 1), + (1, 0, 1, 1, 1, 0, 1), + (1, 0, 1, 1, 1, 0, 1), + (1, 0, 1, 1, 1, 0, 1), + (1, 0, 0, 0, 0, 0, 1), + (1, 1, 1, 1, 1, 1, 1)); + + HORIZONTAL_SEPARATION_PATTERN: array[0..0, 0..7] of Integer = ( + (0, 0, 0, 0, 0, 0, 0, 0)); + + VERTICAL_SEPARATION_PATTERN: array[0..6, 0..0] of Integer = ( + (0), (0), (0), (0), (0), (0), (0)); + + POSITION_ADJUSTMENT_PATTERN: array[0..4, 0..4] of Integer = ( + (1, 1, 1, 1, 1), + (1, 0, 0, 0, 1), + (1, 0, 1, 0, 1), + (1, 0, 0, 0, 1), + (1, 1, 1, 1, 1)); + + // From Appendix E. Table 1, JIS0510X:2004 (p 71). The table was double-checked by komatsu. + POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE: array[0..39, 0..6] of Integer = ( + (-1, -1, -1, -1, -1, -1, -1), // Version 1 + ( 6, 18, -1, -1, -1, -1, -1), // Version 2 + ( 6, 22, -1, -1, -1, -1, -1), // Version 3 + ( 6, 26, -1, -1, -1, -1, -1), // Version 4 + ( 6, 30, -1, -1, -1, -1, -1), // Version 5 + ( 6, 34, -1, -1, -1, -1, -1), // Version 6 + ( 6, 22, 38, -1, -1, -1, -1), // Version 7 + ( 6, 24, 42, -1, -1, -1, -1), // Version 8 + ( 6, 26, 46, -1, -1, -1, -1), // Version 9 + ( 6, 28, 50, -1, -1, -1, -1), // Version 10 + ( 6, 30, 54, -1, -1, -1, -1), // Version 11 + ( 6, 32, 58, -1, -1, -1, -1), // Version 12 + ( 6, 34, 62, -1, -1, -1, -1), // Version 13 + ( 6, 26, 46, 66, -1, -1, -1), // Version 14 + ( 6, 26, 48, 70, -1, -1, -1), // Version 15 + ( 6, 26, 50, 74, -1, -1, -1), // Version 16 + ( 6, 30, 54, 78, -1, -1, -1), // Version 17 + ( 6, 30, 56, 82, -1, -1, -1), // Version 18 + ( 6, 30, 58, 86, -1, -1, -1), // Version 19 + ( 6, 34, 62, 90, -1, -1, -1), // Version 20 + ( 6, 28, 50, 72, 94, -1, -1), // Version 21 + ( 6, 26, 50, 74, 98, -1, -1), // Version 22 + ( 6, 30, 54, 78, 102, -1, -1), // Version 23 + ( 6, 28, 54, 80, 106, -1, -1), // Version 24 + ( 6, 32, 58, 84, 110, -1, -1), // Version 25 + ( 6, 30, 58, 86, 114, -1, -1), // Version 26 + ( 6, 34, 62, 90, 118, -1, -1), // Version 27 + ( 6, 26, 50, 74, 98, 122, -1), // Version 28 + ( 6, 30, 54, 78, 102, 126, -1), // Version 29 + ( 6, 26, 52, 78, 104, 130, -1), // Version 30 + ( 6, 30, 56, 82, 108, 134, -1), // Version 31 + ( 6, 34, 60, 86, 112, 138, -1), // Version 32 + ( 6, 30, 58, 86, 114, 142, -1), // Version 33 + ( 6, 34, 62, 90, 118, 146, -1), // Version 34 + ( 6, 30, 54, 78, 102, 126, 150), // Version 35 + ( 6, 24, 50, 76, 102, 128, 154), // Version 36 + ( 6, 28, 54, 80, 106, 132, 158), // Version 37 + ( 6, 32, 58, 84, 110, 136, 162), // Version 38 + ( 6, 26, 54, 82, 110, 138, 166), // Version 39 + ( 6, 30, 58, 86, 114, 142, 170) // Version 40 + ); + + // Type info cells at the left top corner. + TYPE_INFO_COORDINATES: array[0..14, 0..1] of Integer = ( + (8, 0), + (8, 1), + (8, 2), + (8, 3), + (8, 4), + (8, 5), + (8, 7), + (8, 8), + (7, 8), + (5, 8), + (4, 8), + (3, 8), + (2, 8), + (1, 8), + (0, 8) + ); + + // From Appendix D in JISX0510:2004 (p. 67) + VERSION_INFO_POLY = $1f25; // 1 1111 0010 0101 + + // From Appendix C in JISX0510:2004 (p.65). + TYPE_INFO_POLY = $537; + TYPE_INFO_MASK_PATTERN = $5412; + + + VERSION_DECODE_INFO: array[0..33] of Integer = ( + + $07C94, $085BC, $09A99, $0A4D3, $0BBF6, + $0C762, $0D847, $0E60D, $0F928, $10B78, + $1145D, $12A17, $13532, $149A6, $15683, + $168C9, $177EC, $18EC4, $191E1, $1AFAB, + $1B08E, $1CC1A, $1D33F, $1ED75, $1F250, + $209D5, $216F0, $228BA, $2379F, $24B0B, + $2542E, $26A64, $27541, $28C69); + +type + TMode = (qmTerminator, qmNumeric, qmAlphanumeric, qmStructuredAppend, + qmByte, qmECI, qmKanji, qmFNC1FirstPosition, qmFNC1SecondPosition, + qmHanzi); + +const + ModeCharacterCountBits: array[TMode] of array[0..2] of Integer = ( + (0, 0, 0), (10, 12, 14), (9, 11, 13), (0, 0, 0), (8, 16, 16), + (0, 0, 0), (8, 10, 12), (0, 0, 0), (0, 0, 0), (8, 10, 12)); + + ModeBits: array[TMode] of Integer = (0, 1, 2, 3, 4, 7, 8, 5, 9, 13); + +type + TErrorCorrectionLevel = class + private + FBits: Integer; + public + procedure Assign(Source: TErrorCorrectionLevel); + function Ordinal: Integer; + property Bits: Integer read FBits; + end; + + TECB = class + private + Count: Integer; + DataCodewords: Integer; + public + constructor Create(Count, DataCodewords: Integer); + function GetCount: Integer; + function GetDataCodewords: Integer; + end; + + TECBArray = array of TECB; + + TECBlocks = class + private + ECCodewordsPerBlock: Integer; + ECBlocks: TECBArray; + public + constructor Create(ECCodewordsPerBlock: Integer; ECBlocks: TECB); overload; + constructor Create(ECCodewordsPerBlock: Integer; ECBlocks1, ECBlocks2: TECB); overload; + destructor Destroy; override; + function GetTotalECCodewords: Integer; + function GetNumBlocks: Integer; + function GetECCodewordsPerBlock: Integer; + function GetECBlocks: TECBArray; + end; + + TByteMatrix = class + protected + Bytes: T2DByteArray; + FWidth: Integer; + FHeight: Integer; + public + constructor Create(Width, Height: Integer); + function Get(X, Y: Integer): Integer; + procedure SetBoolean(X, Y: Integer; Value: Boolean); + procedure SetInteger(X, Y: Integer; Value: Integer); + function GetArray: T2DByteArray; + procedure Assign(Source: TByteMatrix); + procedure Clear(Value: Byte); + function Hash: AnsiString; + property Width: Integer read FWidth; + property Height: Integer read FHeight; + end; + + TBitArray = class + private + Bits: array of Integer; + Size: Integer; + procedure EnsureCapacity(Size: Integer); + public + constructor Create; overload; + constructor Create(Size: Integer); overload; + function GetSizeInBytes: Integer; + function GetSize: Integer; + function Get(I: Integer): Boolean; + procedure SetBit(Index: Integer); + procedure AppendBit(Bit: Boolean); + procedure AppendBits(Value, NumBits: Integer); + procedure AppendBitArray(NewBitArray: TBitArray); + procedure ToBytes(BitOffset: Integer; Source: TByteArray; Offset, + NumBytes: Integer); + procedure XorOperation(Other: TBitArray); + end; + + TCharacterSetECI = class + + end; + + TVersion = class + private + VersionNumber: Integer; + AlignmentPatternCenters: array of Integer; + ECBlocks: array of TECBlocks; + TotalCodewords: Integer; + ECCodewords: Integer; + public + constructor Create(VersionNumber: Integer; AlignmentPatternCenters: array of Integer; ECBlocks1, ECBlocks2, ECBlocks3, ECBlocks4: TECBlocks); + destructor Destroy; override; + class function GetVersionForNumber(VersionNum: Integer): TVersion; + class function ChooseVersion(NumInputBits: Integer; ecLevel: TErrorCorrectionLevel): TVersion; + function GetTotalCodewords: Integer; + function GetECBlocksForLevel(ECLevel: TErrorCorrectionLevel): TECBlocks; + function GetDimensionForVersion: Integer; + end; + + TMaskUtil = class + public + function GetDataMaskBit(MaskPattern, X, Y: Integer): Boolean; + end; + + TQRCode = class + private + FMode: TMode; + FECLevel: TErrorCorrectionLevel; + FVersion: Integer; + FMatrixWidth: Integer; + FMaskPattern: Integer; + FNumTotalBytes: Integer; + FNumDataBytes: Integer; + FNumECBytes: Integer; + FNumRSBlocks: Integer; + FMatrix: TByteMatrix; + FQRCodeError: Boolean; + public + constructor Create; + destructor Destroy; override; + function At(X, Y: Integer): Integer; + function IsValid: Boolean; + function IsValidMaskPattern(MaskPattern: Integer): Boolean; + procedure SetMatrix(NewMatrix: TByteMatrix); + procedure SetECLevel(NewECLevel: TErrorCorrectionLevel); + procedure SetAll(VersionNum, NumBytes, NumDataBytes, NumRSBlocks, NumECBytes, MatrixWidth: Integer); + property QRCodeError: Boolean read FQRCodeError; + property Mode: TMode read FMode write FMode; + property Version: Integer read FVersion write FVersion; + property NumDataBytes: Integer read FNumDataBytes; + property NumTotalBytes: Integer read FNumTotalBytes; + property NumRSBlocks: Integer read FNumRSBlocks; + property MatrixWidth: Integer read FMatrixWidth; + property MaskPattern: Integer read FMaskPattern write FMaskPattern; + property ECLevel: TErrorCorrectionLevel read FECLevel; + end; + + TMatrixUtil = class + + private + FMatrixUtilError: Boolean; + procedure ClearMatrix(Matrix: TByteMatrix); + + procedure EmbedBasicPatterns(Version: Integer; Matrix: TByteMatrix); + procedure EmbedTypeInfo(ECLevel: TErrorCorrectionLevel; MaskPattern: Integer; Matrix: TByteMatrix); + procedure MaybeEmbedVersionInfo(Version: Integer; Matrix: TByteMatrix); + procedure EmbedDataBits(DataBits: TBitArray; MaskPattern: Integer; Matrix: TByteMatrix); + function FindMSBSet(Value: Integer): Integer; + function CalculateBCHCode(Value, Poly: Integer): Integer; + procedure MakeTypeInfoBits(ECLevel: TErrorCorrectionLevel; MaskPattern: Integer; Bits: TBitArray); + procedure MakeVersionInfoBits(Version: Integer; Bits: TBitArray); + function IsEmpty(Value: Integer): Boolean; + procedure EmbedTimingPatterns(Matrix: TByteMatrix); + procedure EmbedDarkDotAtLeftBottomCorner(Matrix: TByteMatrix); + procedure EmbedHorizontalSeparationPattern(XStart, YStart: Integer; Matrix: TByteMatrix); + procedure EmbedVerticalSeparationPattern(XStart, YStart: Integer; Matrix: TByteMatrix); + procedure EmbedPositionAdjustmentPattern(XStart, YStart: Integer; Matrix: TByteMatrix); + procedure EmbedPositionDetectionPattern(XStart, YStart: Integer; Matrix: TByteMatrix); + procedure EmbedPositionDetectionPatternsAndSeparators(Matrix: TByteMatrix); + procedure MaybeEmbedPositionAdjustmentPatterns(Version: Integer; Matrix: TByteMatrix); + public + constructor Create; + property MatrixUtilError: Boolean read FMatrixUtilError; + procedure BuildMatrix(DataBits: TBitArray; ECLevel: TErrorCorrectionLevel; Version, MaskPattern: Integer; Matrix: TByteMatrix); + end; + +function GetModeBits(Mode: TMode): Integer; +begin + Result := ModeBits[Mode]; +end; + +function GetModeCharacterCountBits(Mode: TMode; Version: TVersion): Integer; +var + Number: Integer; + Offset: Integer; +begin + Number := Version.VersionNumber; + + if (Number <= 9) then + begin + Offset := 0; + end else + if (number <= 26) then + begin + Offset := 1; + end else + begin + Offset := 2; + end; + Result := ModeCharacterCountBits[Mode][Offset]; +end; + +type + TBlockPair = class + private + FDataBytes: TByteArray; + FErrorCorrectionBytes: TByteArray; + public + constructor Create(BA1, BA2: TByteArray); + function GetDataBytes: TByteArray; + function GetErrorCorrectionBytes: TByteArray; + end; + + TGenericGFPoly = class; + + TGenericGF = class + private + FExpTable: TIntegerArray; + FLogTable: TIntegerArray; + FZero: TGenericGFPoly; + FOne: TGenericGFPoly; + FSize: Integer; + FPrimitive: Integer; + FGeneratorBase: Integer; + FInitialized: Boolean; + FPolyList: array of TGenericGFPoly; + + procedure CheckInit; + procedure Initialize; + public + class function CreateQRCodeField256: TGenericGF; + class function AddOrSubtract(A, B: Integer): Integer; + constructor Create(Primitive, Size, B: Integer); + destructor Destroy; override; + function GetZero: TGenericGFPoly; + function Exp(A: Integer): Integer; + function GetGeneratorBase: Integer; + function Inverse(A: Integer): Integer; + function Multiply(A, B: Integer): Integer; + function BuildMonomial(Degree, Coefficient: Integer): TGenericGFPoly; + end; + + TGenericGFPolyArray = array of TGenericGFPoly; + TGenericGFPoly = class + private + FField: TGenericGF; + FCoefficients: TIntegerArray; + public + constructor Create(AField: TGenericGF; ACoefficients: TIntegerArray); + destructor Destroy; override; + function Coefficients: TIntegerArray; + function Multiply(Other: TGenericGFPoly): TGenericGFPoly; + function MultiplyByMonomial(Degree, Coefficient: Integer): TGenericGFPoly; + function Divide(Other: TGenericGFPoly): TGenericGFPolyArray; + function GetCoefficients: TIntegerArray; + function IsZero: Boolean; + function GetCoefficient(Degree: Integer): Integer; + function GetDegree: Integer; + function AddOrSubtract(Other: TGenericGFPoly): TGenericGFPoly; + end; + + TReedSolomonEncoder = class + private + FField: TGenericGF; + FCachedGenerators: TObjectList; + public + constructor Create(AField: TGenericGF); + destructor Destroy; override; + procedure Encode(ToEncode: TIntegerArray; ECBytes: Integer); + function BuildGenerator(Degree: Integer): TGenericGFPoly; + end; + + TEncoder = class + private + FEncoderError: Boolean; + + function ApplyMaskPenaltyRule1Internal(Matrix: TByteMatrix; + IsHorizontal: Boolean): Integer; + function ChooseMode(const Content: WideString; var EncodeOptions: Integer): TMode; overload; + function FilterContent(const Content: WideString; Mode: TMode; EncodeOptions: Integer): WideString; + procedure Append8BitBytes(const Content: WideString; Bits: TBitArray; EncodeOptions: Integer); + + procedure AppendAlphanumericBytes(const Content: WideString; + Bits: TBitArray); + procedure AppendBytes(const Content: WideString; Mode: TMode; + Bits: TBitArray; EncodeOptions: Integer); + procedure AppendKanjiBytes(const Content: WideString; Bits: TBitArray); + procedure AppendLengthInfo(NumLetters, VersionNum: Integer; Mode: TMode; + Bits: TBitArray); + procedure AppendModeInfo(Mode: TMode; Bits: TBitArray); + procedure AppendNumericBytes(const Content: WideString; Bits: TBitArray); + function ChooseMaskPattern(Bits: TBitArray; ECLevel: TErrorCorrectionLevel; + Version: Integer; Matrix: TByteMatrix): Integer; + function GenerateECBytes(DataBytes: TByteArray; + + NumECBytesInBlock: Integer): TByteArray; + function GetAlphanumericCode(Code: Integer): Integer; + procedure GetNumDataBytesAndNumECBytesForBlockID(NumTotalBytes, + NumDataBytes, NumRSBlocks, BlockID: Integer; var NumDataBytesInBlock: TIntegerArray; + var NumECBytesInBlock: TIntegerArray); + procedure InterleaveWithECBytes(Bits: TBitArray; NumTotalBytes, + NumDataBytes, NumRSBlocks: Integer; var Result: TBitArray); + //function IsOnlyDoubleByteKanji(const Content: WideString): Boolean; + procedure TerminateBits(NumDataBytes: Integer; var Bits: TBitArray); + function CalculateMaskPenalty(Matrix: TByteMatrix): Integer; + function ApplyMaskPenaltyRule1(Matrix: TByteMatrix): Integer; + function ApplyMaskPenaltyRule2(Matrix: TByteMatrix): Integer; + function ApplyMaskPenaltyRule3(Matrix: TByteMatrix): Integer; + function ApplyMaskPenaltyRule4(Matrix: TByteMatrix): Integer; + //procedure Encode(const Content: WideString; ECLevel: TErrorCorrectionLevel; QRCode: TQRCode); overload; + procedure Encode(const Content: WideString; EncodeOptions: Integer; ECLevel: TErrorCorrectionLevel; QRCode: TQRCode); + public + constructor Create; + property EncoderError: Boolean read FEncoderError; + end; + +function TEncoder.ApplyMaskPenaltyRule1(Matrix: TByteMatrix): Integer; +begin + Result := ApplyMaskPenaltyRule1Internal(Matrix, True) + + ApplyMaskPenaltyRule1Internal(Matrix, False); +end; + +// Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give +// penalty to them. +function TEncoder.ApplyMaskPenaltyRule2(Matrix: TByteMatrix): Integer; +var + Penalty: Integer; + TheArray: T2DByteArray; + Width: Integer; + Height: Integer; + X: Integer; + Y: Integer; + Value: Integer; +begin + Penalty := 0; + TheArray := Matrix.GetArray; + Width := Matrix.Width; + Height := Matrix.Height; + for Y := 0 to Height - 2 do + begin + for X := 0 to Width - 2 do + begin + Value := TheArray[Y][X]; + if ((Value = TheArray[Y][X + 1]) and (Value = TheArray[Y + 1][X]) and + (Value = TheArray[Y + 1][X + 1])) then + begin + Inc(Penalty, 3); + end; + end; + end; + Result := Penalty; +end; + +// Apply mask penalty rule 3 and return the penalty. Find consecutive cells of 00001011101 or +// 10111010000, and give penalty to them. If we find patterns like 000010111010000, we give +// penalties twice (i.e. 40 * 2). +function TEncoder.ApplyMaskPenaltyRule3(Matrix: TByteMatrix): Integer; +var + Penalty: Integer; + TheArray: T2DByteArray; + Width: Integer; + Height: Integer; + X: Integer; + Y: Integer; +begin + Penalty := 0; + TheArray := Matrix.GetArray; + Width := Matrix.Width; + Height := Matrix.Height; + for Y := 0 to Height - 1 do + begin + for X := 0 to Width - 1 do + begin + if ((X + 6 < Width) and + (TheArray[Y][X] = 1) and + (TheArray[Y][X + 1] = 0) and + (TheArray[Y][X + 2] = 1) and + (TheArray[Y][X + 3] = 1) and + (TheArray[Y][X + 4] = 1) and + (TheArray[Y][X + 5] = 0) and + (TheArray[Y][X + 6] = 1) and + (((X + 10 < Width) and + (TheArray[Y][X + 7] = 0) and + (TheArray[Y][X + 8] = 0) and + (TheArray[Y][X + 9] = 0) and + (TheArray[Y][X + 10] = 0)) or + ((x - 4 >= 0) and + (TheArray[Y][X - 1] = 0) and + (TheArray[Y][X - 2] = 0) and + (TheArray[Y][X - 3] = 0) and + (TheArray[Y][X - 4] = 0)))) then + begin + Inc(Penalty, 40); + end; + if ((Y + 6 < Height) and + (TheArray[Y][X] = 1) and + (TheArray[Y + 1][X] = 0) and + (TheArray[Y + 2][X] = 1) and + (TheArray[Y + 3][X] = 1) and + (TheArray[Y + 4][X] = 1) and + (TheArray[Y + 5][X] = 0) and + (TheArray[Y + 6][X] = 1) and + (((Y + 10 < Height) and + (TheArray[Y + 7][X] = 0) and + (TheArray[Y + 8][X] = 0) and + (TheArray[Y + 9][X] = 0) and + (TheArray[Y + 10][X] = 0)) or + ((Y - 4 >= 0) and + (TheArray[Y - 1][X] = 0) and + (TheArray[Y - 2][X] = 0) and + (TheArray[Y - 3][X] = 0) and + (TheArray[Y - 4][X] = 0)))) then + begin + Inc(Penalty, 40); + end; + end; + end; + Result := Penalty; +end; + +// Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give +// penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance. Examples: +// - 0% => 100 +// - 40% => 20 +// - 45% => 10 +// - 50% => 0 +// - 55% => 10 +// - 55% => 20 +// - 100% => 100 +function TEncoder.ApplyMaskPenaltyRule4(Matrix: TByteMatrix): Integer; +var + NumDarkCells: Integer; + TheArray: T2DByteArray; + Width: Integer; + Height: Integer; + NumTotalCells: Integer; + DarkRatio: Double; + X: Integer; + Y: Integer; +begin + NumDarkCells := 0; + TheArray := Matrix.GetArray; + Width := Matrix.Width; + Height := matrix.Height; + for Y := 0 to Height - 1 do + begin + for X := 0 to Width - 1 do + begin + if (TheArray[Y][X] = 1) then + begin + Inc(NumDarkCells); + end; + end; + end; + numTotalCells := matrix.Height * Matrix.Width; + DarkRatio := NumDarkCells / NumTotalCells; + Result := Round(Abs((DarkRatio * 100 - 50)) / 50); +end; + +// Helper function for applyMaskPenaltyRule1. We need this for doing this calculation in both +// vertical and horizontal orders respectively. +function TEncoder.ApplyMaskPenaltyRule1Internal(Matrix: TByteMatrix; IsHorizontal: Boolean): Integer; +var + Penalty: Integer; + NumSameBitCells: Integer; + PrevBit: Integer; + TheArray: T2DByteArray; + I: Integer; + J: Integer; + Bit: Integer; + ILimit: Integer; + JLimit: Integer; +begin + Penalty := 0; + NumSameBitCells := 0; + PrevBit := -1; + // Horizontal mode: + // for (int i = 0; i < matrix.height(); ++i) { + // for (int j = 0; j < matrix.width(); ++j) { + // int bit = matrix.get(i, j); + // Vertical mode: + // for (int i = 0; i < matrix.width(); ++i) { + // for (int j = 0; j < matrix.height(); ++j) { + // int bit = matrix.get(j, i); + if (IsHorizontal) then + begin + ILimit := Matrix.Height; + JLimit := Matrix.Width; + end else + begin + ILimit := Matrix.Width; + JLimit := Matrix.Height; + end; + TheArray := Matrix.GetArray; + + for I := 0 to ILimit - 1 do + begin + for J := 0 to JLimit - 1 do + begin + if (IsHorizontal) then + begin + Bit := TheArray[I][J]; + end else + begin + Bit := TheArray[J][I]; + end; + if (Bit = PrevBit) then + begin + Inc(NumSameBitCells); + // Found five repetitive cells with the same color (bit). + // We'll give penalty of 3. + if (NumSameBitCells = 5) then + begin + Inc(Penalty, 3); + end else if (NumSameBitCells > 5) then + begin + // After five repetitive cells, we'll add the penalty one + // by one. + Inc(Penalty, 1);; + end; + end else + begin + NumSameBitCells := 1; // Include the cell itself. + PrevBit := bit; + end; + end; + NumSameBitCells := 0; // Clear at each row/column. + end; + Result := Penalty; +end; + +{ TQRCode } + +constructor TQRCode.Create; +begin + FMode := qmTerminator; + FQRCodeError := False; + FECLevel := nil; + FVersion := -1; + FMatrixWidth := -1; + FMaskPattern := -1; + FNumTotalBytes := -1; + FNumDataBytes := -1; + FNumECBytes := -1; + FNumRSBlocks := -1; + FMatrix := nil; +end; + +destructor TQRCode.Destroy; +begin + if (Assigned(FECLevel)) then + begin + FECLevel.Free; + end; + if (Assigned(FMatrix)) then + begin + FMatrix.Free; + end; + inherited; +end; + +function TQRCode.At(X, Y: Integer): Integer; +var + Value: Integer; +begin + // The value must be zero or one. + Value := FMatrix.Get(X, Y); + if (not ((Value = 0) or (Value = 1))) then + begin + FQRCodeError := True; + end; + Result := Value; +end; + +function TQRCode.IsValid: Boolean; +begin + Result := + // First check if all version are not uninitialized. + ((FECLevel <> nil) and + (FVersion <> -1) and + (FMatrixWidth <> -1) and + (FMaskPattern <> -1) and + (FNumTotalBytes <> -1) and + (FNumDataBytes <> -1) and + (FNumECBytes <> -1) and + (FNumRSBlocks <> -1) and + // Then check them in other ways.. + IsValidMaskPattern(FMaskPattern) and + (FNumTotalBytes = FNumDataBytes + FNumECBytes) and + // ByteMatrix stuff. + (Assigned(FMatrix)) and + (FMatrixWidth = FMatrix.Width) and + // See 7.3.1 of JISX0510:2004 (Fp.5). + (FMatrix.Width = FMatrix.Height)); // Must be square. +end; + +function TQRCode.IsValidMaskPattern(MaskPattern: Integer): Boolean; +begin + Result := (MaskPattern >= 0) and (MaskPattern < NUM_MASK_PATTERNS); +end; + +procedure TQRCode.SetMatrix(NewMatrix: TByteMatrix); +begin + if (Assigned(FMatrix)) then + begin + FMatrix.Free; + FMatrix := nil; + end; + FMatrix := NewMatrix; +end; + +procedure TQRCode.SetAll(VersionNum, NumBytes, NumDataBytes, NumRSBlocks, + NumECBytes, MatrixWidth: Integer); +begin + FVersion := VersionNum; + FNumTotalBytes := NumBytes; + FNumDataBytes := NumDataBytes; + FNumRSBlocks := NumRSBlocks; + FNumECBytes := NumECBytes; + FMatrixWidth := MatrixWidth; +end; + +procedure TQRCode.SetECLevel(NewECLevel: TErrorCorrectionLevel); +begin + if (Assigned(FECLevel)) then + begin + FECLevel.Free; + end; + FECLevel := TErrorCorrectionLevel.Create; + FECLevel.Assign(NewECLevel); +end; + +{ TByteMatrix } + +procedure TByteMatrix.Clear(Value: Byte); +var + X, Y: Integer; +begin + for Y := 0 to FHeight - 1 do + begin + for X := 0 to FWidth - 1 do + begin + Bytes[Y][X] := Value; + end; + end; +end; + +constructor TByteMatrix.Create(Width, Height: Integer); +var + Y: Integer; + X: Integer; +begin + FWidth := Width; + FHeight := Height; + SetLength(Bytes, Height); + for Y := 0 to Height - 1 do + begin + SetLength(Bytes[Y], Width); + for X := 0 to Width - 1 do + begin + Bytes[Y][X] := 0; + end; + end; +end; + +function TByteMatrix.Get(X, Y: Integer): Integer; +begin + if (Bytes[Y][X] = 255) then Result := -1 else Result := Bytes[Y][X]; +end; + +function TByteMatrix.GetArray: T2DByteArray; +begin + Result := Bytes; +end; + +function TByteMatrix.Hash: AnsiString; +var + X, Y: Integer; + Counter: Integer; + CC: Integer; +begin + Result := ''; + for Y := 0 to FHeight - 1 do + begin + Counter := 0; + for X := 0 to FWidth - 1 do + begin + CC := Get(X, Y); + if (CC = -1) then CC := 255; + Counter := Counter + CC; + end; + Result := Result + AnsiChar((Counter mod 26) + 65); + end; +end; + +procedure TByteMatrix.SetBoolean(X, Y: Integer; Value: Boolean); +begin + Bytes[Y][X] := Byte(Value) and $FF; +end; + +procedure TByteMatrix.SetInteger(X, Y, Value: Integer); +begin + Bytes[Y][X] := Value and $FF; +end; + +procedure TByteMatrix.Assign(Source: TByteMatrix); +var + SourceLength: Integer; +begin + SourceLength := Length(Source.Bytes); + SetLength(Bytes, SourceLength); + if (SourceLength > 0) then + begin + Move(Source.Bytes[0], Bytes[0], SourceLength); + end; + FWidth := Source.Width; + FHeight := Source.Height; +end; + +{ TEncoder } + +function TEncoder.CalculateMaskPenalty(Matrix: TByteMatrix): Integer; +var + Penalty: Integer; +begin + Penalty := 0; + Inc(Penalty, ApplyMaskPenaltyRule1(Matrix)); + Inc(Penalty, ApplyMaskPenaltyRule2(Matrix)); + Inc(Penalty, ApplyMaskPenaltyRule3(Matrix)); + Inc(Penalty, ApplyMaskPenaltyRule4(Matrix)); + Result := Penalty; +end; + +{procedure TEncoder.Encode(const Content: WideString; ECLevel: TErrorCorrectionLevel; QRCode: TQRCode); +begin + Encode(Content, ECLevel, nil, QRCode); +end;} + +procedure TEncoder.Encode(const Content: WideString; EncodeOptions: Integer; ECLevel: TErrorCorrectionLevel; QRCode: TQRCode); +var + Mode: TMode; + DataBits: TBitArray; + FinalBits: TBitArray; + HeaderBits: TBitArray; + HeaderAndDataBits: TBitArray; + Matrix: TByteMatrix; + NumLetters: Integer; + MatrixUtil: TMatrixUtil; + BitsNeeded: Integer; + ProvisionalBitsNeeded: Integer; + ProvisionalVersion: TVersion; + Version: TVersion; + ECBlocks: TECBlocks; + NumDataBytes: Integer; + Dimension: Integer; + FilteredContent: WideString; +begin + DataBits := TBitArray.Create; + HeaderBits := TBitArray.Create; + + // Pick an encoding mode appropriate for the content. Note that this will not attempt to use + // multiple modes / segments even if that were more efficient. Twould be nice. + // Collect data within the main segment, separately, to count its size if needed. Don't add it to + // main payload yet. + + Mode := ChooseMode(Content, EncodeOptions); + FilteredContent := FilterContent(Content, Mode, EncodeOptions); + AppendBytes(FilteredContent, Mode, DataBits, EncodeOptions); + + // (With ECI in place,) Write the mode marker + AppendModeInfo(Mode, HeaderBits); + + // Hard part: need to know version to know how many bits length takes. But need to know how many + // bits it takes to know version. First we take a guess at version by assuming version will be + // the minimum, 1: + ProvisionalVersion := TVersion.GetVersionForNumber(1); + try + ProvisionalBitsNeeded := HeaderBits.GetSize + + GetModeCharacterCountBits(Mode, ProvisionalVersion) + + DataBits.GetSize; + finally + ProvisionalVersion.Free; + end; + + ProvisionalVersion := TVersion.ChooseVersion(ProvisionalBitsNeeded, ECLevel); + try + // Use that guess to calculate the right version. I am still not sure this works in 100% of cases. + BitsNeeded := HeaderBits.GetSize + + GetModeCharacterCountBits(Mode, ProvisionalVersion) + + DataBits.GetSize; + Version := TVersion.ChooseVersion(BitsNeeded, ECLevel); + finally + ProvisionalVersion.Free; + end; + + HeaderAndDataBits := TBitArray.Create; + FinalBits := TBitArray.Create; + try + HeaderAndDataBits.AppendBitArray(HeaderBits); + + // Find "length" of main segment and write it + if (Mode = qmByte) then + begin + NumLetters := DataBits.GetSizeInBytes; + end else + begin + NumLetters := Length(FilteredContent); + end; + AppendLengthInfo(NumLetters, Version.VersionNumber, Mode, HeaderAndDataBits); + // Put data together into the overall payload + HeaderAndDataBits.AppendBitArray(DataBits); + + ECBlocks := Version.GetECBlocksForLevel(ECLevel); + NumDataBytes := Version.GetTotalCodewords - ECBlocks.GetTotalECCodewords; + + // Terminate the bits properly. + TerminateBits(NumDataBytes, HeaderAndDataBits); + + // Interleave data bits with error correction code. + InterleaveWithECBytes(HeaderAndDataBits, Version.GetTotalCodewords, + NumDataBytes, ECBlocks.GetNumBlocks, FinalBits); + + // QRCode qrCode = new QRCode(); // This is passed in + + + QRCode.SetECLevel(ECLevel); + QRCode.Mode := Mode; + QRCode.Version := Version.VersionNumber; + + // Choose the mask pattern and set to "qrCode". + Dimension := Version.GetDimensionForVersion; + Matrix := TByteMatrix.Create(Dimension, Dimension); + + QRCode.MaskPattern := ChooseMaskPattern(FinalBits, ECLevel, Version.VersionNumber, Matrix); + + Matrix.Free; + Matrix := TByteMatrix.Create(Dimension, Dimension); + + // Build the matrix and set it to "qrCode". + MatrixUtil := TMatrixUtil.Create; + try + MatrixUtil.BuildMatrix(FinalBits, QRCode.ECLevel, QRCode.Version, + QRCode.MaskPattern, Matrix); + finally + MatrixUtil.Free; + end; + + QRCode.SetMatrix(Matrix); // QRCode will free the matrix + finally + DataBits.Free; + HeaderAndDataBits.Free; + FinalBits.Free; + HeaderBits.Free; + Version.Free; + end; +end; + +function TEncoder.FilterContent(const Content: WideString; Mode: TMode; + EncodeOptions: Integer): WideString; +var + X: Integer; + CanAdd: Boolean; +begin + Result := ''; + for X := 1 to Length(Content) do + begin + CanAdd := False; + if (Mode = qmNumeric) then + begin + CanAdd := (Content[X] >= '0') and (Content[X] <= '9'); + end else + if (Mode = qmAlphanumeric) then + begin + CanAdd := GetAlphanumericCode(Ord(Content[X])) > 0; + end else + if (Mode = qmByte) then + begin + if (EncodeOptions = 3) then + begin + CanAdd := Ord(Content[X]) <= $FF; + end else + if ((EncodeOptions = 4) or (EncodeOptions = 5)) then + begin + CanAdd := True; + end; + end; + if (CanAdd) then + begin + Result := Result + Content[X]; + end; + end; +end; + +// Return the code point of the table used in alphanumeric mode or +// -1 if there is no corresponding code in the table. +function TEncoder.GetAlphanumericCode(Code: Integer): Integer; +begin + if (Code < Length(ALPHANUMERIC_TABLE)) then + begin + Result := ALPHANUMERIC_TABLE[Code]; + end else + begin + Result := -1; + end; +end; + +// Choose the mode based on the content +function TEncoder.ChooseMode(const Content: WideString; var EncodeOptions: Integer): TMode; +var + AllNumeric: Boolean; + AllAlphanumeric: Boolean; + AllISO: Boolean; + I: Integer; + C: WideChar; +begin + if (EncodeOptions = 0) then + begin + AllNumeric := Length(Content) > 0; + I := 1; + while (I <= Length(Content)) and (AllNumeric) do + begin + C := Content[I]; + if ((C < '0') or (C > '9')) then + begin + AllNumeric := False; + end else + begin + Inc(I); + end; + end; + + if (not AllNumeric) then + begin + AllAlphanumeric := Length(Content) > 0; + I := 1; + while (I <= Length(Content)) and (AllAlphanumeric) do + begin + C := Content[I]; + if (GetAlphanumericCode(Ord(C)) < 0) then + begin + AllAlphanumeric := False; + end else + begin + Inc(I); + end; + end; + end else + begin + AllAlphanumeric := False; + end; + + if (not AllAlphanumeric) then + begin + AllISO := Length(Content) > 0; + I := 1; + while (I <= Length(Content)) and (AllISO) do + begin + C := Content[I]; + if (Ord(C) > $FF) then + begin + AllISO := False; + end else + begin + Inc(I); + end; + end; + end else + begin + AllISO := False; + end; + + if (AllNumeric) then + begin + Result := qmNumeric; + end else + if (AllAlphanumeric) then + begin + Result := qmAlphanumeric; + end else + if (AllISO) then + begin + Result := qmByte; + EncodeOptions := 3; + end else + begin + Result := qmByte; + EncodeOptions := 4; + end; + end else + if (EncodeOptions = 1) then + begin + Result := qmNumeric; + end else + if (EncodeOptions = 2) then + begin + Result := qmAlphanumeric; + end else + begin + Result := qmByte; + end; +end; + +constructor TEncoder.Create; +begin + FEncoderError := False; +end; + +{function TEncoder.IsOnlyDoubleByteKanji(const Content: WideString): Boolean; +var + I: Integer; + Char1: Integer; +begin + Result := True; + I := 0; + while ((I < Length(Content)) and Result) do + begin + Char1 := Ord(Content[I + 1]); + if (((Char1 < $81) or (Char1 > $9F)) and ((Char1 < $E0) or (Char1 > $EB))) then + begin + Result := False; + end; + end; +end;} + +function TEncoder.ChooseMaskPattern(Bits: TBitArray; ECLevel: TErrorCorrectionLevel; Version: Integer; Matrix: TByteMatrix): Integer; +var + MinPenalty: Integer; + BestMaskPattern: Integer; + MaskPattern: Integer; + MatrixUtil: TMatrixUtil; + Penalty: Integer; +begin + MinPenalty := MaxInt; + BestMaskPattern := -1; + + // We try all mask patterns to choose the best one. + for MaskPattern := 0 to NUM_MASK_PATTERNS - 1 do + begin + MatrixUtil := TMatrixUtil.Create; + try + MatrixUtil.BuildMatrix(Bits, ECLevel, Version, MaskPattern, Matrix); + finally + MatrixUtil.Free; + end; + Penalty := CalculateMaskPenalty(Matrix); + if (Penalty < MinPenalty) then + begin + MinPenalty := Penalty; + BestMaskPattern := MaskPattern; + end; + end; + + Result := BestMaskPattern; +end; + +// Terminate bits as described in 8.4.8 and 8.4.9 of JISX0510:2004 (p.24). +procedure TEncoder.TerminateBits(NumDataBytes: Integer; var Bits: TBitArray); +var + Capacity: Integer; + I: Integer; + NumBitsInLastByte: Integer; + NumPaddingBytes: Integer; +begin + Capacity := NumDataBytes shl 3; + if (Bits.GetSize > Capacity) then + begin + FEncoderError := True; + Exit; + end; + I := 0; + while ((I < 4) and (Bits.GetSize < capacity)) do + begin + Bits.AppendBit(False); + Inc(I); + end; + + // Append termination bits. See 8.4.8 of JISX0510:2004 (p.24) for details. + // If the last byte isn't 8-bit aligned, we'll add padding bits. + NumBitsInLastByte := Bits.GetSize and $07; + if (NumBitsInLastByte > 0) then + begin + for I := numBitsInLastByte to 7 do + begin + Bits.AppendBit(False); + end; + end; + + // If we have more space, we'll fill the space with padding patterns defined in 8.4.9 (p.24). + NumPaddingBytes := NumDataBytes - Bits.GetSizeInBytes; + for I := 0 to NumPaddingBytes - 1 do + begin + if ((I and $01) = 0) then + begin + Bits.AppendBits($EC, 8); + end else + begin + Bits.AppendBits($11, 8); + end; + end; + if (Bits.GetSize <> Capacity) then + begin + FEncoderError := True; + end; +end; + +// Get number of data bytes and number of error correction bytes for block id "blockID". Store +// the result in "numDataBytesInBlock", and "numECBytesInBlock". See table 12 in 8.5.1 of +// JISX0510:2004 (p.30) +procedure TEncoder.GetNumDataBytesAndNumECBytesForBlockID(NumTotalBytes, NumDataBytes, + NumRSBlocks, BlockID: Integer; var NumDataBytesInBlock: TIntegerArray; + var NumECBytesInBlock: TIntegerArray); +var + NumRSBlocksInGroup1: Integer; + NumRSBlocksInGroup2: Integer; + NumTotalBytesInGroup1: Integer; + NumTotalBytesInGroup2: Integer; + NumDataBytesInGroup1: Integer; + NumDataBytesInGroup2: Integer; + NumECBytesInGroup1: Integer; + NumECBytesInGroup2: Integer; +begin + if (BlockID >= NumRSBlocks) then + begin + FEncoderError := True; + Exit; + end; + // numRsBlocksInGroup2 = 196 % 5 = 1 + NumRSBlocksInGroup2 := NumTotalBytes mod NumRSBlocks; + // numRsBlocksInGroup1 = 5 - 1 = 4 + NumRSBlocksInGroup1 := NumRSBlocks - NumRSBlocksInGroup2; + // numTotalBytesInGroup1 = 196 / 5 = 39 + NumTotalBytesInGroup1 := NumTotalBytes div NumRSBlocks; + // numTotalBytesInGroup2 = 39 + 1 = 40 + NumTotalBytesInGroup2 := NumTotalBytesInGroup1 + 1; + // numDataBytesInGroup1 = 66 / 5 = 13 + NumDataBytesInGroup1 := NumDataBytes div NumRSBlocks; + // numDataBytesInGroup2 = 13 + 1 = 14 + NumDataBytesInGroup2 := NumDataBytesInGroup1 + 1; + // numEcBytesInGroup1 = 39 - 13 = 26 + NumECBytesInGroup1 := NumTotalBytesInGroup1 - NumDataBytesInGroup1; + // numEcBytesInGroup2 = 40 - 14 = 26 + NumECBytesInGroup2 := NumTotalBytesInGroup2 - NumDataBytesInGroup2; + // Sanity checks. + // 26 = 26 + if (NumECBytesInGroup1 <> NumECBytesInGroup2) then + begin + FEncoderError := True; + Exit; + end; + // 5 = 4 + 1. + if (NumRSBlocks <> (NumRSBlocksInGroup1 + NumRSBlocksInGroup2)) then + begin + FEncoderError := True; + Exit; + end; + // 196 = (13 + 26) * 4 + (14 + 26) * 1 + if (NumTotalBytes <> + ((NumDataBytesInGroup1 + NumECBytesInGroup1) * NumRsBlocksInGroup1) + + ((NumDataBytesInGroup2 + NumECBytesInGroup2) * NumRsBlocksInGroup2)) then + begin + FEncoderError := True; + Exit; + end; + + if (BlockID < NumRSBlocksInGroup1) then + begin + NumDataBytesInBlock[0] := NumDataBytesInGroup1; + NumECBytesInBlock[0] := numECBytesInGroup1; + end else + begin + NumDataBytesInBlock[0] := NumDataBytesInGroup2; + NumECBytesInBlock[0] := numEcBytesInGroup2; + end; +end; + +// Interleave "bits" with corresponding error correction bytes. On success, store the result in +// "result". The interleave rule is complicated. See 8.6 of JISX0510:2004 (p.37) for details. +procedure TEncoder.InterleaveWithECBytes(Bits: TBitArray; NumTotalBytes, + NumDataBytes, NumRSBlocks: Integer; var Result: TBitArray); +var + DataBytesOffset: Integer; + MaxNumDataBytes: Integer; + MaxNumECBytes: Integer; + Blocks: TObjectList; + NumDataBytesInBlock: TIntegerArray; + NumECBytesInBlock: TIntegerArray; + Size: Integer; + DataBytes: TByteArray; + ECBytes: TByteArray; + I, J: Integer; + BlockPair: TBlockPair; +begin + SetLength(ECBytes, 0); + + // "bits" must have "getNumDataBytes" bytes of data. + if (Bits.GetSizeInBytes <> NumDataBytes) then + begin + FEncoderError := True; + Exit; + end; + + // Step 1. Divide data bytes into blocks and generate error correction bytes for them. We'll + // store the divided data bytes blocks and error correction bytes blocks into "blocks". + DataBytesOffset := 0; + MaxNumDataBytes := 0; + MaxNumEcBytes := 0; + + // Since, we know the number of reedsolmon blocks, we can initialize the vector with the number. + Blocks := TObjectList.Create(True); + try + Blocks.Capacity := NumRSBlocks; + + for I := 0 to NumRSBlocks - 1 do + begin + SetLength(NumDataBytesInBlock, 1); + SetLength(NumECBytesInBlock, 1); + GetNumDataBytesAndNumECBytesForBlockID( + NumTotalBytes, NumDataBytes, NumRSBlocks, I, + NumDataBytesInBlock, NumEcBytesInBlock); + + Size := NumDataBytesInBlock[0]; + SetLength(DataBytes, Size); + Bits.ToBytes(8 * DataBytesOffset, DataBytes, 0, Size); + ECBytes := GenerateECBytes(DataBytes, NumEcBytesInBlock[0]); + BlockPair := TBlockPair.Create(DataBytes, ECBytes); + Blocks.Add(BlockPair); + + MaxNumDataBytes := Max(MaxNumDataBytes, Size); + MaxNumECBytes := Max(MaxNumECBytes, Length(ECBytes)); + Inc(DataBytesOffset, NumDataBytesInBlock[0]); + end; + if (NumDataBytes <> DataBytesOffset) then + begin + FEncoderError := True; + Exit; + end; + + // First, place data blocks. + for I := 0 to MaxNumDataBytes - 1 do + begin + for J := 0 to Blocks.Count - 1 do + begin + DataBytes := TBlockPair(Blocks.Items[J]).GetDataBytes; + if (I < Length(DataBytes)) then + begin + Result.AppendBits(DataBytes[I], 8); + end; + end; + end; + // Then, place error correction blocks. + for I := 0 to MaxNumECBytes - 1 do + begin + for J := 0 to Blocks.Count - 1 do + begin + ECBytes := TBlockPair(Blocks.Items[J]).GetErrorCorrectionBytes; + if (I < Length(ECBytes)) then + begin + Result.AppendBits(ECBytes[I], 8); + end; + end; + end; + finally + Blocks.Free; + end; + if (numTotalBytes <> Result.GetSizeInBytes) then // Should be same. + begin + FEncoderError := True; + Exit; + end; +end; + +function TEncoder.GenerateECBytes(DataBytes: TByteArray; NumECBytesInBlock: Integer): TByteArray; +var + NumDataBytes: Integer; + ToEncode: TIntegerArray; + ReedSolomonEncoder: TReedSolomonEncoder; + I: Integer; + ECBytes: TByteArray; + GenericGF: TGenericGF; +begin + NumDataBytes := Length(DataBytes); + SetLength(ToEncode, NumDataBytes + NumECBytesInBlock); + + for I := 0 to NumDataBytes - 1 do + begin + ToEncode[I] := DataBytes[I] and $FF; + end; + + GenericGF := TGenericGF.CreateQRCodeField256; + try + ReedSolomonEncoder := TReedSolomonEncoder.Create(GenericGF); + try + ReedSolomonEncoder.Encode(ToEncode, NumECBytesInBlock); + finally + ReedSolomonEncoder.Free; + end; + finally + GenericGF.Free; + end; + + SetLength(ECBytes, NumECBytesInBlock); + for I := 0 to NumECBytesInBlock - 1 do + begin + ECBytes[I] := ToEncode[NumDataBytes + I]; + end; + + Result := ECBytes; +end; + +// Append mode info. On success, store the result in "bits". +procedure TEncoder.AppendModeInfo(Mode: TMode; Bits: TBitArray); +begin + Bits.AppendBits(GetModeBits(Mode), 4); +end; + +// Append length info. On success, store the result in "bits". +procedure TEncoder.AppendLengthInfo(NumLetters, VersionNum: Integer; Mode: TMode; Bits: TBitArray); +var + NumBits: Integer; + Version: TVersion; +begin + Version := TVersion.GetVersionForNumber(VersionNum); + try + NumBits := GetModeCharacterCountBits(Mode, Version); + finally + Version.Free; + end; + + if (NumLetters > ((1 shl NumBits) - 1)) then + begin + FEncoderError := True; + Exit; + end; + + Bits.AppendBits(NumLetters, NumBits); +end; + +// Append "bytes" in "mode" mode (encoding) into "bits". On success, store the result in "bits". +procedure TEncoder.AppendBytes(const Content: WideString; Mode: TMode; Bits: TBitArray; EncodeOptions: Integer); +begin + if (Mode = qmNumeric) then + begin + AppendNumericBytes(Content, Bits); + end else + if (Mode = qmAlphanumeric) then + begin + AppendAlphanumericBytes(Content, Bits); + end else + if (Mode = qmByte) then + begin + Append8BitBytes(Content, Bits, EncodeOptions); + end else + if (Mode = qmKanji) then + begin + AppendKanjiBytes(Content, Bits); + end else + begin + FEncoderError := True; + Exit; + end; +end; + +procedure TEncoder.AppendNumericBytes(const Content: WideString; Bits: TBitArray); +var + ContentLength: Integer; + I: Integer; + Num1: Integer; + Num2: Integer; + Num3: Integer; +begin + ContentLength := Length(Content); + I := 0; + while (I < ContentLength) do + begin + Num1 := Ord(Content[I + 0 + 1]) - Ord('0'); + if (I + 2 < ContentLength) then + begin + // Encode three numeric letters in ten bits. + Num2 := Ord(Content[I + 1 + 1]) - Ord('0'); + Num3 := Ord(Content[I + 2 + 1]) - Ord('0'); + Bits.AppendBits(Num1 * 100 + Num2 * 10 + Num3, 10); + Inc(I, 3); + end else + if (I + 1 < ContentLength) then + begin + // Encode two numeric letters in seven bits. + Num2 := Ord(Content[I + 1 + 1]) - Ord('0'); + Bits.AppendBits(Num1 * 10 + Num2, 7); + Inc(I, 2); + end else + begin + // Encode one numeric letter in four bits. + Bits.AppendBits(Num1, 4); + Inc(I); + end; + end; +end; + +procedure TEncoder.AppendAlphanumericBytes(const Content: WideString; Bits: TBitArray); +var + ContentLength: Integer; + I: Integer; + Code1: Integer; + Code2: Integer; +begin + ContentLength := Length(Content); + I := 0; + while (I < ContentLength) do + begin + Code1 := GetAlphanumericCode(Ord(Content[I + 0 + 1])); + if (Code1 = -1) then + begin + FEncoderError := True; + Exit; + end; + if (I + 1 < ContentLength) then + begin + Code2 := GetAlphanumericCode(Ord(Content[I + 1 + 1])); + if (Code2 = -1) then + begin + FEncoderError := True; + Exit; + end; + // Encode two alphanumeric letters in 11 bits. + Bits.AppendBits(Code1 * 45 + Code2, 11); + Inc(I, 2); + end else + begin + // Encode one alphanumeric letter in six bits. + Bits.AppendBits(Code1, 6); + Inc(I); + end; + end; +end; + +procedure TEncoder.Append8BitBytes(const Content: WideString; Bits: TBitArray; EncodeOptions: Integer); +var + Bytes: TByteArray; + I: Integer; + UTF8Version: AnsiString; +begin + SetLength(Bytes, 0); + if (EncodeOptions = 3) then + begin + SetLength(Bytes, Length(Content)); + for I := 1 to Length(Content) do + begin + Bytes[I - 1] := Ord(Content[I]) and $FF; + end; + end else + if (EncodeOptions = 4) then + begin + // Add the UTF-8 BOM + UTF8Version := #$EF#$BB#$BF + UTF8Encode(Content); + SetLength(Bytes, Length(UTF8Version)); + if (Length(UTF8Version) > 0) then + begin + Move(UTF8Version[1], Bytes[0], Length(UTF8Version)); + end; + end else + if (EncodeOptions = 5) then + begin + // No BOM + UTF8Version := UTF8Encode(Content); + SetLength(Bytes, Length(UTF8Version)); + if (Length(UTF8Version) > 0) then + begin + Move(UTF8Version[1], Bytes[0], Length(UTF8Version)); + end; + end; + for I := 0 to Length(Bytes) - 1 do + begin + Bits.AppendBits(Bytes[I], 8); + end; +end; + +procedure TEncoder.AppendKanjiBytes(const Content: WideString; Bits: TBitArray); +var + Bytes: TByteArray; + ByteLength: Integer; + I: Integer; + Byte1: Integer; + Byte2: Integer; + Code: Integer; + Subtracted: Integer; + Encoded: Integer; +begin + SetLength(Bytes, 0); + try + + except + FEncoderError := True; + Exit; + end; + + ByteLength := Length(Bytes); + I := 0; + while (I < ByteLength) do + begin + Byte1 := Bytes[I] and $FF; + Byte2 := Bytes[I + 1] and $FF; + Code := (Byte1 shl 8) or Byte2; + Subtracted := -1; + if ((Code >= $8140) and (Code <= $9ffc)) then + begin + Subtracted := Code - $8140; + end else + if ((Code >= $e040) and (Code <= $ebbf)) then + begin + Subtracted := Code - $c140; + end; + if (Subtracted = -1) then + begin + FEncoderError := True; + Exit; + end; + Encoded := ((Subtracted shr 8) * $c0) + (Subtracted and $ff); + Bits.AppendBits(Encoded, 13); + Inc(I, 2); + end; +end; + +procedure TMatrixUtil.ClearMatrix(Matrix: TByteMatrix); +begin + Matrix.Clear(Byte(-1)); +end; + +constructor TMatrixUtil.Create; +begin + FMatrixUtilError := False; +end; + +// Build 2D matrix of QR Code from "dataBits" with "ecLevel", "version" and "getMaskPattern". On +// success, store the result in "matrix" and return true. +procedure TMatrixUtil.BuildMatrix(DataBits: TBitArray; ECLevel: TErrorCorrectionLevel; + Version, MaskPattern: Integer; Matrix: TByteMatrix); +begin + ClearMatrix(Matrix); + EmbedBasicPatterns(Version, Matrix); + + // Type information appear with any version. + EmbedTypeInfo(ECLevel, MaskPattern, Matrix); + + // Version info appear if version >= 7. + MaybeEmbedVersionInfo(Version, Matrix); + + // Data should be embedded at end. + EmbedDataBits(DataBits, MaskPattern, Matrix); +end; + +// Embed basic patterns. On success, modify the matrix and return true. +// The basic patterns are: +// - Position detection patterns +// - Timing patterns +// - Dark dot at the left bottom corner +// - Position adjustment patterns, if need be +procedure TMatrixUtil.EmbedBasicPatterns(Version: Integer; Matrix: TByteMatrix); +begin + // Let's get started with embedding big squares at corners. + EmbedPositionDetectionPatternsAndSeparators(Matrix); + + // Then, embed the dark dot at the left bottom corner. + EmbedDarkDotAtLeftBottomCorner(Matrix); + + // Position adjustment patterns appear if version >= 2. + MaybeEmbedPositionAdjustmentPatterns(Version, Matrix); + + // Timing patterns should be embedded after position adj. patterns. + EmbedTimingPatterns(Matrix); +end; + +// Embed type information. On success, modify the matrix. +procedure TMatrixUtil.EmbedTypeInfo(ECLevel: TErrorCorrectionLevel; MaskPattern: Integer; Matrix: TByteMatrix); +var + TypeInfoBits: TBitArray; + I: Integer; + Bit: Boolean; + X1, Y1: Integer; + X2, Y2: Integer; +begin + TypeInfoBits := TBitArray.Create; + try + MakeTypeInfoBits(ECLevel, MaskPattern, TypeInfoBits); + + for I := 0 to TypeInfoBits.GetSize - 1 do + begin + // Place bits in LSB to MSB order. LSB (least significant bit) is the last value in + // "typeInfoBits". + Bit := TypeInfoBits.Get(TypeInfoBits.GetSize - 1 - I); + + // Type info bits at the left top corner. See 8.9 of JISX0510:2004 (p.46). + X1 := TYPE_INFO_COORDINATES[I][0]; + Y1 := TYPE_INFO_COORDINATES[I][1]; + Matrix.SetBoolean(X1, Y1, Bit); + + if (I < 8) then + begin + // Right top corner. + X2 := Matrix.Width - I - 1; + Y2 := 8; + Matrix.SetBoolean(X2, Y2, Bit); + end else + begin + // Left bottom corner. + X2 := 8; + Y2 := Matrix.Height - 7 + (I - 8); + Matrix.SetBoolean(X2, Y2, Bit); + end; + end; + finally + TypeInfoBits.Free; + end; +end; + +// Embed version information if need be. On success, modify the matrix and return true. +// See 8.10 of JISX0510:2004 (p.47) for how to embed version information. +procedure TMatrixUtil.MaybeEmbedVersionInfo(Version: Integer; Matrix: TByteMatrix); +var + VersionInfoBits: TBitArray; + I, J: Integer; + BitIndex: Integer; + Bit: Boolean; +begin + if (Version < 7) then + begin + Exit; // Don't need version info. + end; + + VersionInfoBits := TBitArray.Create; + try + MakeVersionInfoBits(Version, VersionInfoBits); + + BitIndex := 6 * 3 - 1; // It will decrease from 17 to 0. + for I := 0 to 5 do + begin + for J := 0 to 2 do + begin + // Place bits in LSB (least significant bit) to MSB order. + Bit := VersionInfoBits.Get(BitIndex); + Dec(BitIndex); + // Left bottom corner. + Matrix.SetBoolean(I, Matrix.Height - 11 + J, Bit); + // Right bottom corner. + Matrix.SetBoolean(Matrix.Height - 11 + J, I, bit); + end; + end; + finally + VersionInfoBits.Free; + end; +end; + +// Embed "dataBits" using "getMaskPattern". On success, modify the matrix and return true. +// For debugging purposes, it skips masking process if "getMaskPattern" is -1. +// See 8.7 of JISX0510:2004 (p.38) for how to embed data bits. +procedure TMatrixUtil.EmbedDataBits(DataBits: TBitArray; MaskPattern: Integer; Matrix: TByteMatrix); +var + BitIndex: Integer; + Direction: Integer; + X, Y, I, XX: Integer; + Bit: Boolean; + MaskUtil: TMaskUtil; +begin + MaskUtil := TMaskUtil.Create; + try + bitIndex := 0; + direction := -1; + // Start from the right bottom cell. + X := Matrix.Width - 1; + Y := Matrix.Height - 1; + while (X > 0) do + begin + // Skip the vertical timing pattern. + if (X = 6) then + begin + Dec(X, 1); + end; + while ((Y >= 0) and (y < Matrix.Height)) do + begin + for I := 0 to 1 do + begin + XX := X - I; + // Skip the cell if it's not empty. + if (not IsEmpty(Matrix.Get(XX, Y))) then + begin + Continue; + end; + + if (BitIndex < DataBits.GetSize) then + begin + Bit := DataBits.Get(BitIndex); + Inc(BitIndex); + end else + begin + // Padding bit. If there is no bit left, we'll fill the left cells with 0, as described + // in 8.4.9 of JISX0510:2004 (p. 24). + Bit := False; + end; + + // Skip masking if mask_pattern is -1. + if (MaskPattern <> -1) then + begin + if (MaskUtil.GetDataMaskBit(MaskPattern, XX, Y)) then + begin + Bit := not Bit; + end; + end; + Matrix.SetBoolean(XX, Y, Bit); + end; + Inc(Y, Direction); + end; + Direction := -Direction; // Reverse the direction. + Inc(Y, Direction); + Dec(X, 2); // Move to the left. + end; + finally + MaskUtil.Free; + end; + + // All bits should be consumed. + if (BitIndex <> DataBits.GetSize()) then + begin + FMatrixUtilError := True; + Exit; + end; +end; + +// Return the position of the most significant bit set (to one) in the "value". The most +// significant bit is position 32. If there is no bit set, return 0. Examples: +// - findMSBSet(0) => 0 +// - findMSBSet(1) => 1 +// - findMSBSet(255) => 8 +function TMatrixUtil.FindMSBSet(Value: Integer): Integer; +var + NumDigits: Integer; +begin + NumDigits := 0; + while (Value <> 0) do + begin + Value := Value shr 1; + Inc(NumDigits); + end; + Result := NumDigits; +end; + +// Calculate BCH (Bose-Chaudhuri-Hocquenghem) code for "value" using polynomial "poly". The BCH +// code is used for encoding type information and version information. +// Example: Calculation of version information of 7. +// f(x) is created from 7. +// - 7 = 000111 in 6 bits +// - f(x) = x^2 + x^1 + x^0 +// g(x) is given by the standard (p. 67) +// - g(x) = x^12 + x^11 + x^10 + x^9 + x^8 + x^5 + x^2 + 1 +// Multiply f(x) by x^(18 - 6) +// - f'(x) = f(x) * x^(18 - 6) +// - f'(x) = x^14 + x^13 + x^12 +// Calculate the remainder of f'(x) / g(x) +// x^2 +// __________________________________________________ +// g(x) )x^14 + x^13 + x^12 +// x^14 + x^13 + x^12 + x^11 + x^10 + x^7 + x^4 + x^2 +// -------------------------------------------------- +// x^11 + x^10 + x^7 + x^4 + x^2 +// +// The remainder is x^11 + x^10 + x^7 + x^4 + x^2 +// Encode it in binary: 110010010100 +// The return value is 0xc94 (1100 1001 0100) +// +// Since all coefficients in the polynomials are 1 or 0, we can do the calculation by bit +// operations. We don't care if cofficients are positive or negative. +function TMatrixUtil.CalculateBCHCode(Value, Poly: Integer): Integer; +var + MSBSetInPoly: Integer; +begin + // If poly is "1 1111 0010 0101" (version info poly), msbSetInPoly is 13. We'll subtract 1 + // from 13 to make it 12. + MSBSetInPoly := FindMSBSet(Poly); + Value := Value shl (MSBSetInPoly - 1); + // Do the division business using exclusive-or operations. + while (FindMSBSet(Value) >= MSBSetInPoly) do + begin + Value := Value xor (Poly shl (FindMSBSet(Value) - MSBSetInPoly)); + end; + // Now the "value" is the remainder (i.e. the BCH code) + Result := Value; +end; + +// Make bit vector of type information. On success, store the result in "bits" and return true. +// Encode error correction level and mask pattern. See 8.9 of +// JISX0510:2004 (p.45) for details. +procedure TMatrixUtil.MakeTypeInfoBits(ECLevel: TErrorCorrectionLevel; MaskPattern: Integer; Bits: TBitArray); +var + TypeInfo: Integer; + BCHCode: Integer; + MaskBits: TBitArray; +begin + if ((MaskPattern >= 0) and (MaskPattern < NUM_MASK_PATTERNS)) then + begin + TypeInfo := (ECLevel.Bits shl 3) or MaskPattern; + Bits.AppendBits(TypeInfo, 5); + + BCHCode := CalculateBCHCode(TypeInfo, TYPE_INFO_POLY); + Bits.AppendBits(BCHCode, 10); + + MaskBits := TBitArray.Create; + try + MaskBits.AppendBits(TYPE_INFO_MASK_PATTERN, 15); + Bits.XorOperation(MaskBits); + finally + MaskBits.Free; + end; + + if (Bits.GetSize <> 15) then // Just in case. + begin + FMatrixUtilError := True; + Exit; + end; + end; +end; + +// Make bit vector of version information. On success, store the result in "bits" and return true. +// See 8.10 of JISX0510:2004 (p.45) for details. +procedure TMatrixUtil.MakeVersionInfoBits(Version: Integer; Bits: TBitArray); +var + BCHCode: Integer; +begin + Bits.AppendBits(Version, 6); + BCHCode := CalculateBCHCode(Version, VERSION_INFO_POLY); + Bits.AppendBits(BCHCode, 12); + + if (Bits.GetSize() <> 18) then + begin + FMatrixUtilError := True; + Exit; + end; +end; + +// Check if "value" is empty. +function TMatrixUtil.IsEmpty(Value: Integer): Boolean; +begin + Result := (Value = -1); +end; + +procedure TMatrixUtil.EmbedTimingPatterns(Matrix: TByteMatrix); +var + I: Integer; + Bit: Integer; +begin + // -8 is for skipping position detection patterns (size 7), and two horizontal/vertical + // separation patterns (size 1). Thus, 8 = 7 + 1. + for I := 8 to Matrix.Width - 9 do + begin + Bit := (I + 1) mod 2; + // Horizontal line. + if (IsEmpty(Matrix.Get(I, 6))) then + begin + Matrix.SetInteger(I, 6, Bit); + end; + // Vertical line. + if (IsEmpty(Matrix.Get(6, I))) then + begin + Matrix.SetInteger(6, I, Bit); + end; + end; +end; + +// Embed the lonely dark dot at left bottom corner. JISX0510:2004 (p.46) +procedure TMatrixUtil.EmbedDarkDotAtLeftBottomCorner(Matrix: TByteMatrix); +begin + if (Matrix.Get(8, Matrix.Height - 8) = 0) then + begin + FMatrixUtilError := True; + Exit; + end; + Matrix.SetInteger(8, Matrix.Height - 8, 1); +end; + +procedure TMatrixUtil.EmbedHorizontalSeparationPattern(XStart, YStart: Integer; Matrix: TByteMatrix); +var + X: Integer; +begin + // We know the width and height. + for X := 0 to 7 do + begin + if (not IsEmpty(Matrix.Get(XStart + X, YStart))) then + begin + FMatrixUtilError := True; + Exit; + end; + Matrix.SetInteger(XStart + X, YStart, HORIZONTAL_SEPARATION_PATTERN[0][X]); + end; +end; + +procedure TMatrixUtil.EmbedVerticalSeparationPattern(XStart, YStart: Integer; Matrix: TByteMatrix); +var + Y: Integer; +begin + // We know the width and height. + for Y := 0 to 6 do + begin + if (not IsEmpty(Matrix.Get(XStart, YStart + Y))) then + begin + FMatrixUtilError := True; + Exit; + end; + Matrix.SetInteger(XStart, YStart + Y, VERTICAL_SEPARATION_PATTERN[Y][0]); + end; +end; + +// Note that we cannot unify the function with embedPositionDetectionPattern() despite they are +// almost identical, since we cannot write a function that takes 2D arrays in different sizes in +// C/C++. We should live with the fact. +procedure TMatrixUtil.EmbedPositionAdjustmentPattern(XStart, YStart: Integer; Matrix: TByteMatrix); +var + X, Y: Integer; +begin + // We know the width and height. + for Y := 0 to 4 do + begin + for X := 0 to 4 do + begin + if (not IsEmpty(Matrix.Get(XStart + X, YStart + Y))) then + begin + FMatrixUtilError := True; + Exit; + end; + Matrix.SetInteger(XStart + X, YStart + Y, POSITION_ADJUSTMENT_PATTERN[Y][X]); + end; + end; +end; + +procedure TMatrixUtil.EmbedPositionDetectionPattern(XStart, YStart: Integer; Matrix: TByteMatrix); +var + X, Y: Integer; +begin + // We know the width and height. + for Y := 0 to 6 do + begin + for X := 0 to 6 do + begin + if (not IsEmpty(Matrix.Get(XStart + X, YStart + Y))) then + begin + FMatrixUtilError := True; + Exit; + end; + Matrix.SetInteger(XStart + X, YStart + Y, POSITION_DETECTION_PATTERN[Y][X]); + end; + end; +end; + +// Embed position detection patterns and surrounding vertical/horizontal separators. +procedure TMatrixUtil.EmbedPositionDetectionPatternsAndSeparators(Matrix: TByteMatrix); +var + PDPWidth: Integer; + HSPWidth: Integer; + VSPSize: Integer; +begin + // Embed three big squares at corners. + PDPWidth := Length(POSITION_DETECTION_PATTERN[0]); + // Left top corner. + EmbedPositionDetectionPattern(0, 0, Matrix); + // Right top corner. + EmbedPositionDetectionPattern(Matrix.Width - PDPWidth, 0, Matrix); + // Left bottom corner. + EmbedPositionDetectionPattern(0, Matrix.Width- PDPWidth, Matrix); + + // Embed horizontal separation patterns around the squares. + HSPWidth := Length(HORIZONTAL_SEPARATION_PATTERN[0]); + // Left top corner. + EmbedHorizontalSeparationPattern(0, HSPWidth - 1, Matrix); + // Right top corner. + EmbedHorizontalSeparationPattern(Matrix.Width - HSPWidth, + HSPWidth - 1, Matrix); + // Left bottom corner. + EmbedHorizontalSeparationPattern(0, Matrix.Width - HSPWidth, Matrix); + + // Embed vertical separation patterns around the squares. + VSPSize := Length(VERTICAL_SEPARATION_PATTERN); + // Left top corner. + EmbedVerticalSeparationPattern(VSPSize, 0, Matrix); + // Right top corner. + EmbedVerticalSeparationPattern(Matrix.Height - VSPSize - 1, 0, Matrix); + // Left bottom corner. + EmbedVerticalSeparationPattern(VSPSize, Matrix.Height - VSPSize, Matrix); +end; + +// Embed position adjustment patterns if need be. +procedure TMatrixUtil.MaybeEmbedPositionAdjustmentPatterns(Version: Integer; Matrix: TByteMatrix); +var + Index: Integer; + Coordinates: array of Integer; + NumCoordinates: Integer; + X, Y, I, J: Integer; +begin + if (Version >= 2) then + begin + Index := Version - 1; + NumCoordinates := Length(POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[Index]); + SetLength(Coordinates, NumCoordinates); + Move(POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[Index][0], Coordinates[0], NumCoordinates * SizeOf(Integer)); + for I := 0 to NumCoordinates - 1 do + begin + for J := 0 to NumCoordinates - 1 do + begin + Y := Coordinates[I]; + X := Coordinates[J]; + if ((X = -1) or (Y = -1)) then + begin + Continue; + end; + // If the cell is unset, we embed the position adjustment pattern here. + if (IsEmpty(Matrix.Get(X, Y))) then + begin + // -2 is necessary since the x/y coordinates point to the center of the pattern, not the + // left top corner. + EmbedPositionAdjustmentPattern(X - 2, Y - 2, Matrix); + end; + end; + end; + end; +end; + + +{ TBitArray } + + +procedure TBitArray.AppendBits(Value, NumBits: Integer); +var + NumBitsLeft: Integer; +begin + if ((NumBits < 0) or (NumBits > 32)) then + begin + + end; + EnsureCapacity(Size + NumBits); + for NumBitsLeft := NumBits downto 1 do + begin + AppendBit(((Value shr (NumBitsLeft - 1)) and $01) = 1); + end; +end; + +constructor TBitArray.Create(Size: Integer); + +begin + Size := Size; + SetLength(Bits, (Size + 31) shr 5); +end; + +constructor TBitArray.Create; +begin + Size := 0; + SetLength(Bits, 1); +end; + +function TBitArray.Get(I: Integer): Boolean; +begin + Result := (Bits[I shr 5] and (1 shl (I and $1F))) <> 0; +end; + +function TBitArray.GetSize: Integer; +begin + Result := Size; +end; + +function TBitArray.GetSizeInBytes: Integer; +begin + Result := (Size + 7) shr 3; +end; + +procedure TBitArray.SetBit(Index: Integer); +begin + Bits[Index shr 5] := Bits[Index shr 5] or (1 shl (Index and $1F)); +end; + +procedure TBitArray.AppendBit(Bit: Boolean); +begin + EnsureCapacity(Size + 1); + if (Bit) then + begin + Bits[Size shr 5] := Bits[Size shr 5] or (1 shl (Size and $1F)); + end; + Inc(Size); +end; + +procedure TBitArray.ToBytes(BitOffset: Integer; Source: TByteArray; Offset, + NumBytes: Integer); +var + I: Integer; + J: Integer; + TheByte: Integer; +begin + for I := 0 to NumBytes - 1 do + begin + TheByte := 0; + for J := 0 to 7 do + begin + if (Get(BitOffset)) then + begin + TheByte := TheByte or (1 shl (7 - J)); + end; + Inc(BitOffset); + end; + Source[Offset + I] := TheByte; + end; +end; + +procedure TBitArray.XorOperation(Other: TBitArray); +var + I: Integer; +begin + if (Length(Bits) = Length(Other.Bits)) then + begin + for I := 0 to Length(Bits) - 1 do + begin + // The last byte could be incomplete (i.e. not have 8 bits in + // it) but there is no problem since 0 XOR 0 == 0. + Bits[I] := Bits[I] xor Other.Bits[I]; + end; + end; +end; + +procedure TBitArray.AppendBitArray(NewBitArray: TBitArray); +var + OtherSize: Integer; + I: Integer; +begin + OtherSize := NewBitArray.GetSize; + EnsureCapacity(Size + OtherSize); + for I := 0 to OtherSize - 1 do + begin + AppendBit(NewBitArray.Get(I)); + end; +end; + +procedure TBitArray.EnsureCapacity(Size: Integer); +begin + if (Size > (Length(Bits) shl 5)) then + begin + SetLength(Bits, Size); + end; +end; + +{ TErrorCorrectionLevel } + +procedure TErrorCorrectionLevel.Assign(Source: TErrorCorrectionLevel); +begin + Self.FBits := Source.FBits; +end; + +function TErrorCorrectionLevel.Ordinal: Integer; +begin + Result := 0; +end; + +{ TVersion } + +class function TVersion.ChooseVersion(NumInputBits: Integer; + ECLevel: TErrorCorrectionLevel): TVersion; +var + VersionNum: Integer; + Version: TVersion; + NumBytes: Integer; + ECBlocks: TECBlocks; + NumECBytes: Integer; + NumDataBytes: Integer; + TotalInputBytes: Integer; +begin + Result := nil; + // In the following comments, we use numbers of Version 7-H. + for VersionNum := 1 to 40 do + begin + Version := TVersion.GetVersionForNumber(VersionNum); + + // numBytes = 196 + NumBytes := Version.GetTotalCodewords; + // getNumECBytes = 130 + ECBlocks := Version.GetECBlocksForLevel(ECLevel); + NumECBytes := ECBlocks.GetTotalECCodewords; + // getNumDataBytes = 196 - 130 = 66 + NumDataBytes := NumBytes - NumECBytes; + TotalInputBytes := (NumInputBits + 7) div 8; + + if (numDataBytes >= totalInputBytes) then + begin + Result := Version; + Exit; + end else + begin + Version.Free; + end; + end; +end; + +constructor TVersion.Create(VersionNumber: Integer; + AlignmentPatternCenters: array of Integer; ECBlocks1, ECBlocks2, ECBlocks3, + ECBlocks4: TECBlocks); +var + Total: Integer; + ECBlock: TECB; + ECBArray: TECBArray; + I: Integer; +begin + Self.VersionNumber := VersionNumber; + SetLength(Self.AlignmentPatternCenters, Length(AlignmentPatternCenters)); + if (Length(AlignmentPatternCenters) > 0) then + begin + Move(AlignmentPatternCenters[0], Self.AlignmentPatternCenters[0], + Length(AlignmentPatternCenters) * SizeOf(Integer)); + end; + SetLength(ECBlocks, 4); + ECBlocks[0] := ECBlocks1; + ECBlocks[1] := ECBlocks2; + ECBlocks[2] := ECBlocks3; + ECBlocks[3] := ECBlocks4; + Total := 0; + ECCodewords := ECBlocks1.GetECCodewordsPerBlock; + ECBArray := ECBlocks1.GetECBlocks; + for I := 0 to Length(ECBArray) - 1 do + begin + ECBlock := ECBArray[I]; + Inc(Total, ECBlock.GetCount * (ECBlock.GetDataCodewords + ECCodewords)); + end; + TotalCodewords := Total; +end; + +destructor TVersion.Destroy; +var + X: Integer; +begin + for X := 0 to Length(ECBlocks) - 1 do + begin + ECBlocks[X].Free; + end; + inherited; +end; + +function TVersion.GetDimensionForVersion: Integer; +begin + Result := 17 + 4 * VersionNumber; +end; + +function TVersion.GetECBlocksForLevel(ECLevel: TErrorCorrectionLevel): TECBlocks; +begin + Result := ECBlocks[ECLevel.Ordinal]; +end; + +function TVersion.GetTotalCodewords: Integer; +begin + Result := TotalCodewords; +end; + +class function TVersion.GetVersionForNumber(VersionNum: Integer): TVersion; +begin + if (VersionNum = 1) then + begin + Result := TVersion.Create(1, [], + TECBlocks.Create(7, TECB.Create(1, 19)), + TECBlocks.Create(10, TECB.Create(1, 16)), + TECBlocks.Create(13, TECB.Create(1, 13)), + TECBlocks.Create(17, TECB.Create(1, 9))); + end else + if (VersionNum = 2) then + begin + Result := TVersion.Create(2, [6, 18], + TECBlocks.Create(10, TECB.Create(1, 34)), + TECBlocks.Create(16, TECB.Create(1, 28)), + TECBlocks.Create(22, TECB.Create(1, 22)), + TECBlocks.Create(28, TECB.Create(1, 16))); + end else + if (VersionNum = 3) then + begin + Result := TVersion.Create(3, [6, 22], + TECBlocks.Create(15, TECB.Create(1, 55)), + TECBlocks.Create(26, TECB.Create(1, 44)), + TECBlocks.Create(18, TECB.Create(2, 17)), + TECBlocks.Create(22, TECB.Create(2, 13))); + end else + if (VersionNum = 4) then + begin + Result := TVersion.Create(4, [6, 26], + TECBlocks.Create(20, TECB.Create(1, 80)), + TECBlocks.Create(18, TECB.Create(2, 32)), + TECBlocks.Create(26, TECB.Create(2, 24)), + TECBlocks.Create(16, TECB.Create(4, 9))); + end else + if (VersionNum = 5) then + begin + Result := TVersion.Create(5, [6, 30], + TECBlocks.Create(26, TECB.Create(1, 108)), + TECBlocks.Create(24, TECB.Create(2, 43)), + TECBlocks.Create(18, TECB.Create(2, 15), + TECB.Create(2, 16)), + TECBlocks.Create(22, TECB.Create(2, 11), + TECB.Create(2, 12))); + end else + if (VersionNum = 6) then + begin + Result := TVersion.Create(6, [6, 34], + TECBlocks.Create(18, TECB.Create(2, 68)), + TECBlocks.Create(16, TECB.Create(4, 27)), + TECBlocks.Create(24, TECB.Create(4, 19)), + TECBlocks.Create(28, TECB.Create(4, 15))); + end else + if (VersionNum = 7) then + begin + Result := TVersion.Create(7, [6, 22, 38], + TECBlocks.Create(20, TECB.Create(2, 78)), + TECBlocks.Create(18, TECB.Create(4, 31)), + TECBlocks.Create(18, TECB.Create(2, 14), + TECB.Create(4, 15)), + TECBlocks.Create(26, TECB.Create(4, 13), + TECB.Create(1, 14))); + end else + if (VersionNum = 8) then + begin + Result := TVersion.Create(8, [6, 24, 42], + TECBlocks.Create(24, TECB.Create(2, 97)), + TECBlocks.Create(22, TECB.Create(2, 38), + TECB.Create(2, 39)), + TECBlocks.Create(22, TECB.Create(4, 18), + TECB.Create(2, 19)), + TECBlocks.Create(26, TECB.Create(4, 14), + TECB.Create(2, 15))); + end else + if (VersionNum = 9) then + begin + Result := TVersion.Create(9, [6, 26, 46], + TECBlocks.Create(30, TECB.Create(2, 116)), + TECBlocks.Create(22, TECB.Create(3, 36), + TECB.Create(2, 37)), + TECBlocks.Create(20, TECB.Create(4, 16), + TECB.Create(4, 17)), + TECBlocks.Create(24, TECB.Create(4, 12), + TECB.Create(4, 13))); + end else + if (VersionNum = 10) then + begin + Result := TVersion.Create(10, [6, 28, 50], + TECBlocks.Create(18, TECB.Create(2, 68), + TECB.Create(2, 69)), + TECBlocks.Create(26, TECB.Create(4, 43), + TECB.Create(1, 44)), + TECBlocks.Create(24, TECB.Create(6, 19), + TECB.Create(2, 20)), + TECBlocks.Create(28, TECB.Create(6, 15), + TECB.Create(2, 16))); + end else + if (VersionNum = 11) then + begin + Result := TVersion.Create(11, [6, 30, 54], + TECBlocks.Create(20, TECB.Create(4, 81)), + TECBlocks.Create(30, TECB.Create(1, 50), + TECB.Create(4, 51)), + TECBlocks.Create(28, TECB.Create(4, 22), + TECB.Create(4, 23)), + TECBlocks.Create(24, TECB.Create(3, 12), + TECB.Create(8, 13))); + end else + if (VersionNum = 12) then + begin + Result := TVersion.Create(12, [6, 32, 58], + TECBlocks.Create(24, TECB.Create(2, 92), + TECB.Create(2, 93)), + TECBlocks.Create(22, TECB.Create(6, 36), + TECB.Create(2, 37)), + TECBlocks.Create(26, TECB.Create(4, 20), + TECB.Create(6, 21)), + TECBlocks.Create(28, TECB.Create(7, 14), + TECB.Create(4, 15))); + end else + if (VersionNum = 13) then + begin + Result := TVersion.Create(13, [6, 34, 62], + TECBlocks.Create(26, TECB.Create(4, 107)), + TECBlocks.Create(22, TECB.Create(8, 37), + TECB.Create(1, 38)), + TECBlocks.Create(24, TECB.Create(8, 20), + TECB.Create(4, 21)), + TECBlocks.Create(22, TECB.Create(12, 11), + TECB.Create(4, 12))); + end else + if (VersionNum = 14) then + begin + Result := TVersion.Create(14, [6, 26, 46, 66], + TECBlocks.Create(30, TECB.Create(3, 115), + TECB.Create(1, 116)), + TECBlocks.Create(24, TECB.Create(4, 40), + TECB.Create(5, 41)), + TECBlocks.Create(20, TECB.Create(11, 16), + TECB.Create(5, 17)), + TECBlocks.Create(24, TECB.Create(11, 12), + TECB.Create(5, 13))); + end else + if (VersionNum = 15) then + begin + Result := TVersion.Create(15, [6, 26, 48, 70], + TECBlocks.Create(22, TECB.Create(5, 87), + TECB.Create(1, 88)), + TECBlocks.Create(24, TECB.Create(5, 41), + TECB.Create(5, 42)), + TECBlocks.Create(30, TECB.Create(5, 24), + TECB.Create(7, 25)), + TECBlocks.Create(24, TECB.Create(11, 12), + TECB.Create(7, 13))); + end else + if (VersionNum = 16) then + begin + Result := TVersion.Create(16, [6, 26, 50, 74], + TECBlocks.Create(24, TECB.Create(5, 98), + TECB.Create(1, 99)), + TECBlocks.Create(28, TECB.Create(7, 45), + TECB.Create(3, 46)), + TECBlocks.Create(24, TECB.Create(15, 19), + TECB.Create(2, 20)), + TECBlocks.Create(30, TECB.Create(3, 15), + TECB.Create(13, 16))); + end else + if (VersionNum = 17) then + begin + Result := TVersion.Create(17, [6, 30, 54, 78], + TECBlocks.Create(28, TECB.Create(1, 107), + TECB.Create(5, 108)), + TECBlocks.Create(28, TECB.Create(10, 46), + TECB.Create(1, 47)), + TECBlocks.Create(28, TECB.Create(1, 22), + TECB.Create(15, 23)), + TECBlocks.Create(28, TECB.Create(2, 14), + TECB.Create(17, 15))); + end else + if (VersionNum = 18) then + begin + Result := TVersion.Create(18, [6, 30, 56, 82], + TECBlocks.Create(30, TECB.Create(5, 120), + TECB.Create(1, 121)), + TECBlocks.Create(26, TECB.Create(9, 43), + TECB.Create(4, 44)), + TECBlocks.Create(28, TECB.Create(17, 22), + TECB.Create(1, 23)), + TECBlocks.Create(28, TECB.Create(2, 14), + TECB.Create(19, 15))); + end else + if (VersionNum = 19) then + begin + Result := TVersion.Create(19, [6, 30, 58, 86], + TECBlocks.Create(28, TECB.Create(3, 113), + TECB.Create(4, 114)), + TECBlocks.Create(26, TECB.Create(3, 44), + TECB.Create(11, 45)), + TECBlocks.Create(26, TECB.Create(17, 21), + TECB.Create(4, 22)), + TECBlocks.Create(26, TECB.Create(9, 13), + TECB.Create(16, 14))); + end else + if (VersionNum = 20) then + begin + Result := TVersion.Create(20, [6, 34, 62, 90], + TECBlocks.Create(28, TECB.Create(3, 107), + TECB.Create(5, 108)), + TECBlocks.Create(26, TECB.Create(3, 41), + TECB.Create(13, 42)), + TECBlocks.Create(30, TECB.Create(15, 24), + TECB.Create(5, 25)), + TECBlocks.Create(28, TECB.Create(15, 15), + TECB.Create(10, 16))); + end else + if (VersionNum = 21) then + begin + Result := TVersion.Create(21, [6, 28, 50, 72, 94], + TECBlocks.Create(28, TECB.Create(4, 116), + TECB.Create(4, 117)), + TECBlocks.Create(26, TECB.Create(17, 42)), + TECBlocks.Create(28, TECB.Create(17, 22), + TECB.Create(6, 23)), + TECBlocks.Create(30, TECB.Create(19, 16), + TECB.Create(6, 17))); + end else + if (VersionNum = 22) then + begin + Result := TVersion.Create(22, [6, 26, 50, 74, 98], + TECBlocks.Create(28, TECB.Create(2, 111), + TECB.Create(7, 112)), + TECBlocks.Create(28, TECB.Create(17, 46)), + TECBlocks.Create(30, TECB.Create(7, 24), + TECB.Create(16, 25)), + TECBlocks.Create(24, TECB.Create(34, 13))); + end else + if (VersionNum = 23) then + begin + Result := TVersion.Create(23, [6, 30, 54, 78, 102], + TECBlocks.Create(30, TECB.Create(4, 121), + TECB.Create(5, 122)), + TECBlocks.Create(28, TECB.Create(4, 47), + TECB.Create(14, 48)), + TECBlocks.Create(30, TECB.Create(11, 24), + TECB.Create(14, 25)), + TECBlocks.Create(30, TECB.Create(16, 15), + TECB.Create(14, 16))); + end else + if (VersionNum = 24) then + begin + Result := TVersion.Create(24, [6, 28, 54, 80, 106], + TECBlocks.Create(30, TECB.Create(6, 117), + TECB.Create(4, 118)), + TECBlocks.Create(28, TECB.Create(6, 45), + TECB.Create(14, 46)), + TECBlocks.Create(30, TECB.Create(11, 24), + TECB.Create(16, 25)), + TECBlocks.Create(30, TECB.Create(30, 16), + TECB.Create(2, 17))); + end else + if (VersionNum = 25) then + begin + Result := TVersion.Create(25, [6, 32, 58, 84, 110], + TECBlocks.Create(26, TECB.Create(8, 106), + TECB.Create(4, 107)), + TECBlocks.Create(28, TECB.Create(8, 47), + TECB.Create(13, 48)), + TECBlocks.Create(30, TECB.Create(7, 24), + TECB.Create(22, 25)), + TECBlocks.Create(30, TECB.Create(22, 15), + TECB.Create(13, 16))); + end else + if (VersionNum = 26) then + begin + Result := TVersion.Create(26, [6, 30, 58, 86, 114], + TECBlocks.Create(28, TECB.Create(10, 114), + TECB.Create(2, 115)), + TECBlocks.Create(28, TECB.Create(19, 46), + TECB.Create(4, 47)), + TECBlocks.Create(28, TECB.Create(28, 22), + TECB.Create(6, 23)), + TECBlocks.Create(30, TECB.Create(33, 16), + TECB.Create(4, 17))); + end else + if (VersionNum = 27) then + begin + Result := TVersion.Create(27, [6, 34, 62, 90, 118], + TECBlocks.Create(30, TECB.Create(8, 122), + TECB.Create(4, 123)), + TECBlocks.Create(28, TECB.Create(22, 45), + TECB.Create(3, 46)), + TECBlocks.Create(30, TECB.Create(8, 23), + TECB.Create(26, 24)), + TECBlocks.Create(30, TECB.Create(12, 15), + TECB.Create(28, 16))); + end else + if (VersionNum = 28) then + begin + Result := TVersion.Create(28, [6, 26, 50, 74, 98, 122], + TECBlocks.Create(30, TECB.Create(3, 117), + TECB.Create(10, 118)), + TECBlocks.Create(28, TECB.Create(3, 45), + TECB.Create(23, 46)), + TECBlocks.Create(30, TECB.Create(4, 24), + TECB.Create(31, 25)), + TECBlocks.Create(30, TECB.Create(11, 15), + TECB.Create(31, 16))); + end else + if (VersionNum = 29) then + begin + Result := TVersion.Create(29, [6, 30, 54, 78, 102, 126], + TECBlocks.Create(30, TECB.Create(7, 116), + TECB.Create(7, 117)), + TECBlocks.Create(28, TECB.Create(21, 45), + TECB.Create(7, 46)), + TECBlocks.Create(30, TECB.Create(1, 23), + TECB.Create(37, 24)), + TECBlocks.Create(30, TECB.Create(19, 15), + TECB.Create(26, 16))); + end else + if (VersionNum = 30) then + begin + Result := TVersion.Create(30, [6, 26, 52, 78, 104, 130], + TECBlocks.Create(30, TECB.Create(5, 115), + TECB.Create(10, 116)), + TECBlocks.Create(28, TECB.Create(19, 47), + TECB.Create(10, 48)), + TECBlocks.Create(30, TECB.Create(15, 24), + TECB.Create(25, 25)), + TECBlocks.Create(30, TECB.Create(23, 15), + TECB.Create(25, 16))); + end else + if (VersionNum = 31) then + begin + Result := TVersion.Create(31, [6, 30, 56, 82, 108, 134], + TECBlocks.Create(30, TECB.Create(13, 115), + TECB.Create(3, 116)), + TECBlocks.Create(28, TECB.Create(2, 46), + TECB.Create(29, 47)), + TECBlocks.Create(30, TECB.Create(42, 24), + TECB.Create(1, 25)), + TECBlocks.Create(30, TECB.Create(23, 15), + TECB.Create(28, 16))); + end else + if (VersionNum = 32) then + begin + Result := TVersion.Create(32, [6, 34, 60, 86, 112, 138], + TECBlocks.Create(30, TECB.Create(17, 115)), + TECBlocks.Create(28, TECB.Create(10, 46), + TECB.Create(23, 47)), + TECBlocks.Create(30, TECB.Create(10, 24), + TECB.Create(35, 25)), + TECBlocks.Create(30, TECB.Create(19, 15), + TECB.Create(35, 16))); + end else + if (VersionNum = 33) then + begin + Result := TVersion.Create(33, [6, 30, 58, 86, 114, 142], + TECBlocks.Create(30, TECB.Create(17, 115), + TECB.Create(1, 116)), + TECBlocks.Create(28, TECB.Create(14, 46), + TECB.Create(21, 47)), + TECBlocks.Create(30, TECB.Create(29, 24), + TECB.Create(19, 25)), + TECBlocks.Create(30, TECB.Create(11, 15), + TECB.Create(46, 16))); + end else + if (VersionNum = 34) then + begin + Result := TVersion.Create(34, [6, 34, 62, 90, 118, 146], + TECBlocks.Create(30, TECB.Create(13, 115), + TECB.Create(6, 116)), + TECBlocks.Create(28, TECB.Create(14, 46), + TECB.Create(23, 47)), + TECBlocks.Create(30, TECB.Create(44, 24), + TECB.Create(7, 25)), + TECBlocks.Create(30, TECB.Create(59, 16), + TECB.Create(1, 17))); + end else + if (VersionNum = 35) then + begin + Result := TVersion.Create(35, [6, 30, 54, 78, 102, 126, 150], + TECBlocks.Create(30, TECB.Create(12, 121), + TECB.Create(7, 122)), + TECBlocks.Create(28, TECB.Create(12, 47), + TECB.Create(26, 48)), + TECBlocks.Create(30, TECB.Create(39, 24), + TECB.Create(14, 25)), + TECBlocks.Create(30, TECB.Create(22, 15), + TECB.Create(41, 16))); + end else + if (VersionNum = 36) then + begin + Result := TVersion.Create(36, [6, 24, 50, 76, 102, 128, 154], + TECBlocks.Create(30, TECB.Create(6, 121), + TECB.Create(14, 122)), + TECBlocks.Create(28, TECB.Create(6, 47), + TECB.Create(34, 48)), + TECBlocks.Create(30, TECB.Create(46, 24), + TECB.Create(10, 25)), + TECBlocks.Create(30, TECB.Create(2, 15), + TECB.Create(64, 16))); + end else + if (VersionNum = 37) then + begin + Result := TVersion.Create(37, [6, 28, 54, 80, 106, 132, 158], + TECBlocks.Create(30, TECB.Create(17, 122), + TECB.Create(4, 123)), + TECBlocks.Create(28, TECB.Create(29, 46), + TECB.Create(14, 47)), + TECBlocks.Create(30, TECB.Create(49, 24), + TECB.Create(10, 25)), + TECBlocks.Create(30, TECB.Create(24, 15), + TECB.Create(46, 16))); + end else + if (VersionNum = 38) then + begin + Result := TVersion.Create(38, [6, 32, 58, 84, 110, 136, 162], + TECBlocks.Create(30, TECB.Create(4, 122), + TECB.Create(18, 123)), + TECBlocks.Create(28, TECB.Create(13, 46), + TECB.Create(32, 47)), + TECBlocks.Create(30, TECB.Create(48, 24), + TECB.Create(14, 25)), + TECBlocks.Create(30, TECB.Create(42, 15), + TECB.Create(32, 16))); + end else + if (VersionNum = 39) then + begin + Result := TVersion.Create(39, [6, 26, 54, 82, 110, 138, 166], + TECBlocks.Create(30, TECB.Create(20, 117), + TECB.Create(4, 118)), + TECBlocks.Create(28, TECB.Create(40, 47), + TECB.Create(7, 48)), + TECBlocks.Create(30, TECB.Create(43, 24), + TECB.Create(22, 25)), + TECBlocks.Create(30, TECB.Create(10, 15), + TECB.Create(67, 16))); + end else + if (VersionNum = 40) then + begin + Result := TVersion.Create(40, [6, 30, 58, 86, 114, 142, 170], + TECBlocks.Create(30, TECB.Create(19, 118), + TECB.Create(6, 119)), + TECBlocks.Create(28, TECB.Create(18, 47), + TECB.Create(31, 48)), + TECBlocks.Create(30, TECB.Create(34, 24), + TECB.Create(34, 25)), + TECBlocks.Create(30, TECB.Create(20, 15), + TECB.Create(61, 16))); + end else + begin + Result := nil; + end; +end; + +{ TMaskUtil } + +// Return the mask bit for "getMaskPattern" at "x" and "y". See 8.8 of JISX0510:2004 for mask +// pattern conditions. +function TMaskUtil.GetDataMaskBit(MaskPattern, X, Y: Integer): Boolean; +var + Intermediate: Integer; + Temp: Integer; +begin + Intermediate := 0; + if ((MaskPattern >= 0) and (MaskPattern < NUM_MASK_PATTERNS)) then + begin + case (maskPattern) of + 0: Intermediate := (Y + X) and 1; + 1: Intermediate := Y and 1; + 2: Intermediate := X mod 3; + 3: Intermediate := (Y + X) mod 3; + 4: Intermediate := ((y shr 1) + (X div 3)) and 1; + 5: + begin + Temp := Y * X; + Intermediate := (Temp and 1) + (Temp mod 3); + end; + 6: + begin + Temp := Y * X; + Intermediate := ((Temp and 1) + (Temp mod 3)) and 1; + end; + 7: + begin + Temp := Y * X; + Intermediate := ((temp mod 3) + ((Y + X) and 1)) and 1; + end; + end; + end; + Result := Intermediate = 0; +end; + +{ TECBlocks } + +constructor TECBlocks.Create(ECCodewordsPerBlock: Integer; ECBlocks: TECB); +begin + Self.ECCodewordsPerBlock := ECCodewordsPerBlock; + SetLength(Self.ECBlocks, 1); + Self.ECBlocks[0] := ECBlocks; +end; + +constructor TECBlocks.Create(ECCodewordsPerBlock: Integer; ECBlocks1, + ECBlocks2: TECB); +begin + Self.ECCodewordsPerBlock := ECCodewordsPerBlock; + SetLength(Self.ECBlocks, 2); + ECBlocks[0] := ECBlocks1; + ECBlocks[1] := ECBlocks2; +end; + +destructor TECBlocks.Destroy; +var + X: Integer; +begin + for X := 0 to Length(ECBlocks) - 1 do + begin + ECBlocks[X].Free; + end; + inherited; +end; + +function TECBlocks.GetECBlocks: TECBArray; +begin + Result := ECBlocks; +end; + +function TECBlocks.GetECCodewordsPerBlock: Integer; +begin + Result := ECCodewordsPerBlock; +end; + +function TECBlocks.GetNumBlocks: Integer; +var + Total: Integer; + I: Integer; +begin + Total := 0; + for I := 0 to Length(ECBlocks) - 1 do + begin + Inc(Total, ECBlocks[I].GetCount); + end; + Result := Total; +end; + +function TECBlocks.GetTotalECCodewords: Integer; +begin + Result := ECCodewordsPerBlock * GetNumBlocks; +end; + +{ TBlockPair } + +constructor TBlockPair.Create(BA1, BA2: TByteArray); +begin + FDataBytes := BA1; + FErrorCorrectionBytes := BA2; +end; + +function TBlockPair.GetDataBytes: TByteArray; +begin + Result := FDataBytes; +end; + +function TBlockPair.GetErrorCorrectionBytes: TByteArray; +begin + Result := FErrorCorrectionBytes; +end; + +{ TReedSolomonEncoder } + +function TReedSolomonEncoder.BuildGenerator(Degree: Integer): TGenericGFPoly; +var + LastGenerator: TGenericGFPoly; + NextGenerator: TGenericGFPoly; + Poly: TGenericGFPoly; + D: Integer; + CA: TIntegerArray; +begin + if (Degree >= FCachedGenerators.Count) then + begin + LastGenerator := TGenericGFPoly(FCachedGenerators[FCachedGenerators.Count - 1]); + + for D := FCachedGenerators.Count to Degree do + begin + SetLength(CA, 2); + CA[0] := 1; + CA[1] := FField.Exp(D - 1 + FField.GetGeneratorBase); + Poly := TGenericGFPoly.Create(FField, CA); + NextGenerator := LastGenerator.Multiply(Poly); + FCachedGenerators.Add(NextGenerator); + LastGenerator := NextGenerator; + end; + end; + Result := TGenericGFPoly(FCachedGenerators[Degree]); +end; + +constructor TReedSolomonEncoder.Create(AField: TGenericGF); +var + GenericGFPoly: TGenericGFPoly; + IntArray: TIntegerArray; +begin + FField := AField; + + // Contents of FCachedGenerators will be freed by FGenericGF.Destroy + FCachedGenerators := TObjectList.Create(False); + + SetLength(IntArray, 1); + IntArray[0] := 1; + GenericGFPoly := TGenericGFPoly.Create(AField, IntArray); + FCachedGenerators.Add(GenericGFPoly); +end; + +destructor TReedSolomonEncoder.Destroy; +begin + FCachedGenerators.Free; + inherited; +end; + +procedure TReedSolomonEncoder.Encode(ToEncode: TIntegerArray; ECBytes: Integer); +var + DataBytes: Integer; + Generator: TGenericGFPoly; + InfoCoefficients: TIntegerArray; + Info: TGenericGFPoly; + Remainder: TGenericGFPoly; + Coefficients: TIntegerArray; + NumZeroCoefficients: Integer; + I: Integer; +begin + SetLength(Coefficients, 0); + if (ECBytes > 0) then + begin + DataBytes := Length(ToEncode) - ECBytes; + if (DataBytes > 0) then + begin + Generator := BuildGenerator(ECBytes); + SetLength(InfoCoefficients, DataBytes); + InfoCoefficients := Copy(ToEncode, 0, DataBytes); + Info := TGenericGFPoly.Create(FField, InfoCoefficients); + Info := Info.MultiplyByMonomial(ECBytes, 1); + Remainder := Info.Divide(Generator)[1]; + Coefficients := Remainder.GetCoefficients; + NumZeroCoefficients := ECBytes - Length(Coefficients); + for I := 0 to NumZeroCoefficients - 1 do + begin + ToEncode[DataBytes + I] := 0; + end; + Move(Coefficients[0], ToEncode[DataBytes + NumZeroCoefficients], Length(Coefficients) * SizeOf(Integer)); + end; + end; +end; + +{ TECB } + +constructor TECB.Create(Count, DataCodewords: Integer); +begin + Self.Count := Count; + Self.DataCodewords := DataCodewords; +end; + +function TECB.GetCount: Integer; +begin + Result := Count; +end; + +function TECB.GetDataCodewords: Integer; +begin + Result := DataCodewords; +end; + +{ TGenericGFPoly } + +function TGenericGFPoly.AddOrSubtract(Other: TGenericGFPoly): TGenericGFPoly; +var + SmallerCoefficients: TIntegerArray; + LargerCoefficients: TIntegerArray; + Temp: TIntegerArray; + SumDiff: TIntegerArray; + LengthDiff: Integer; + I: Integer; +begin + SetLength(SmallerCoefficients, 0); + SetLength(LargerCoefficients, 0); + SetLength(Temp, 0); + SetLength(SumDiff, 0); + + Result := nil; + if (Assigned(Other)) then + begin + if (FField = Other.FField) then + begin + if (IsZero) then + begin + Result := Other; + Exit; + end; + + if (Other.IsZero) then + begin + Result := Self; + Exit; + end; + + SmallerCoefficients := FCoefficients; + LargerCoefficients := Other.Coefficients; + if (Length(SmallerCoefficients) > Length(LargerCoefficients)) then + begin + Temp := smallerCoefficients; + SmallerCoefficients := LargerCoefficients; + LargerCoefficients := temp; + end; + SetLength(SumDiff, Length(LargerCoefficients)); + LengthDiff := Length(LargerCoefficients) - Length(SmallerCoefficients); + + // Copy high-order terms only found in higher-degree polynomial's coefficients + if (LengthDiff > 0) then + begin + //SumDiff := Copy(LargerCoefficients, 0, LengthDiff); + Move(LargerCoefficients[0], SumDiff[0], LengthDiff * SizeOf(Integer)); + end; + + for I := LengthDiff to Length(LargerCoefficients) - 1 do + begin + SumDiff[I] := TGenericGF.AddOrSubtract(SmallerCoefficients[I - LengthDiff], LargerCoefficients[I]); + end; + + Result := TGenericGFPoly.Create(FField, SumDiff); + end; + end; +end; + +function TGenericGFPoly.Coefficients: TIntegerArray; +begin + Result := FCoefficients; +end; + +constructor TGenericGFPoly.Create(AField: TGenericGF; + ACoefficients: TIntegerArray); +var + CoefficientsLength: Integer; + FirstNonZero: Integer; +begin + FField := AField; + SetLength(FField.FPolyList, Length(FField.FPolyList) + 1); + FField.FPolyList[Length(FField.FPolyList) - 1] := Self; + CoefficientsLength := Length(ACoefficients); + if ((CoefficientsLength > 1) and (ACoefficients[0] = 0)) then + begin + // Leading term must be non-zero for anything except the constant polynomial "0" + FirstNonZero := 1; + while ((FirstNonZero < CoefficientsLength) and (ACoefficients[FirstNonZero] = 0)) do + begin + Inc(FirstNonZero); + end; + + if (FirstNonZero = CoefficientsLength) then + begin + FCoefficients := AField.GetZero.Coefficients; + end else + begin + SetLength(FCoefficients, CoefficientsLength - FirstNonZero); + FCoefficients := Copy(ACoefficients, FirstNonZero, Length(FCoefficients)); + end; + end else + begin + FCoefficients := ACoefficients; + end; +end; + +destructor TGenericGFPoly.Destroy; +begin + Self.FField := FField; + inherited; +end; + +function TGenericGFPoly.Divide(Other: TGenericGFPoly): TGenericGFPolyArray; +var + Quotient: TGenericGFPoly; + Remainder: TGenericGFPoly; + DenominatorLeadingTerm: Integer; + InverseDenominatorLeadingTerm: integer; + DegreeDifference: Integer; + Scale: Integer; + Term: TGenericGFPoly; + IterationQuotient: TGenericGFPoly; +begin + SetLength(Result, 0); + if ((FField = Other.FField) and (not Other.IsZero)) then + begin + + Quotient := FField.GetZero; + Remainder := Self; + + DenominatorLeadingTerm := Other.GetCoefficient(Other.GetDegree); + InverseDenominatorLeadingTerm := FField.Inverse(DenominatorLeadingTerm); + + while ((Remainder.GetDegree >= Other.GetDegree) and (not Remainder.IsZero)) do + begin + DegreeDifference := Remainder.GetDegree - Other.GetDegree; + Scale := FField.Multiply(Remainder.GetCoefficient(Remainder.GetDegree), InverseDenominatorLeadingTerm); + Term := Other.MultiplyByMonomial(DegreeDifference, Scale); + IterationQuotient := FField.BuildMonomial(degreeDifference, scale); + Quotient := Quotient.AddOrSubtract(IterationQuotient); + Remainder := Remainder.AddOrSubtract(Term); + end; + + SetLength(Result, 2); + Result[0] := Quotient; + Result[1] := Remainder; + end; +end; + +function TGenericGFPoly.GetCoefficient(Degree: Integer): Integer; +begin + Result := FCoefficients[Length(FCoefficients) - 1 - Degree]; +end; + +function TGenericGFPoly.GetCoefficients: TIntegerArray; +begin + Result := FCoefficients; +end; + +function TGenericGFPoly.GetDegree: Integer; +begin + Result := Length(FCoefficients) - 1; +end; + +function TGenericGFPoly.IsZero: Boolean; +begin + Result := FCoefficients[0] = 0; +end; + +function TGenericGFPoly.Multiply(Other: TGenericGFPoly): TGenericGFPoly; +var + ACoefficients: TIntegerArray; + BCoefficients: TIntegerArray; + Product: TIntegerArray; + ALength: Integer; + BLength: Integer; + I: Integer; + J: Integer; + ACoeff: Integer; +begin + SetLength(ACoefficients, 0); + SetLength(BCoefficients, 0); + Result := nil; + + if (FField = Other.FField) then + begin + if (IsZero or Other.IsZero) then + begin + Result := FField.GetZero; + Exit; + end; + + ACoefficients := FCoefficients; + ALength := Length(ACoefficients); + BCoefficients := Other.Coefficients; + BLength := Length(BCoefficients); + SetLength(Product, aLength + bLength - 1); + for I := 0 to ALength - 1 do + begin + ACoeff := ACoefficients[I]; + for J := 0 to BLength - 1 do + begin + Product[I + J] := TGenericGF.AddOrSubtract(Product[I + J], + FField.Multiply(ACoeff, BCoefficients[J])); + end; + end; + Result := TGenericGFPoly.Create(FField, Product); + end; +end; + +function TGenericGFPoly.MultiplyByMonomial(Degree, + Coefficient: Integer): TGenericGFPoly; +var + I: Integer; + Size: Integer; + Product: TIntegerArray; +begin + Result := nil; + if (Degree >= 0) then + begin + if (Coefficient = 0) then + begin + Result := FField.GetZero; + Exit; + end; + Size := Length(Coefficients); + SetLength(Product, Size + Degree); + for I := 0 to Size - 1 do + begin + Product[I] := FField.Multiply(FCoefficients[I], Coefficient); + end; + Result := TGenericGFPoly.Create(FField, Product); + end; +end; + +{ TGenericGF } + +class function TGenericGF.AddOrSubtract(A, B: Integer): Integer; +begin + Result := A xor B; +end; + +function TGenericGF.BuildMonomial(Degree, Coefficient: Integer): TGenericGFPoly; +var + Coefficients: TIntegerArray; +begin + CheckInit(); + + if (Degree >= 0) then + begin + if (Coefficient = 0) then + begin + Result := FZero; + Exit; + end; + SetLength(Coefficients, Degree + 1); + Coefficients[0] := Coefficient; + Result := TGenericGFPoly.Create(Self, Coefficients); + end else + begin + Result := nil; + end; +end; + +procedure TGenericGF.CheckInit; +begin + if (not FInitialized) then + begin + Initialize; + end; +end; + +constructor TGenericGF.Create(Primitive, Size, B: Integer); +begin + FInitialized := False; + FPrimitive := Primitive; + FSize := Size; + FGeneratorBase := B; + if (FSize < 0) then + begin + Initialize; + end; +end; + +class function TGenericGF.CreateQRCodeField256: TGenericGF; +begin + Result := TGenericGF.Create($011D, 256, 0); +end; + +destructor TGenericGF.Destroy; +var + X: Integer; + Y: Integer; +begin + for X := 0 to Length(FPolyList) - 1 do + begin + if (Assigned(FPolyList[X])) then + begin + for Y := X + 1 to Length(FPolyList) - 1 do + begin + if (FPolyList[Y] = FPolyList[X]) then + begin + FPolyList[Y] := nil; + end; + end; + FPolyList[X].Free; + end; + end; + inherited; +end; + +function TGenericGF.Exp(A: Integer): Integer; +begin + CheckInit; + Result := FExpTable[A]; +end; + +function TGenericGF.GetGeneratorBase: Integer; +begin + Result := FGeneratorBase; +end; + +function TGenericGF.GetZero: TGenericGFPoly; +begin + CheckInit; + Result := FZero; +end; + +procedure TGenericGF.Initialize; +var + X: Integer; + I: Integer; + CA: TIntegerArray; +begin + SetLength(FExpTable, FSize); + SetLength(FLogTable, FSize); + X := 1; + for I := 0 to FSize - 1 do + begin + FExpTable[I] := x; + X := X shl 1; // x = x * 2; we're assuming the generator alpha is 2 + if (X >= FSize) then + begin + X := X xor FPrimitive; + X := X and (FSize - 1); + end; + end; + + for I := 0 to FSize - 2 do + begin + FLogTable[FExpTable[I]] := I; + end; + + // logTable[0] == 0 but this should never be used + + SetLength(CA, 1); + CA[0] := 0; + FZero := TGenericGFPoly.Create(Self, CA); + + SetLength(CA, 1); + CA[0] := 1; + FOne := TGenericGFPoly.Create(Self, CA); + + FInitialized := True; +end; + +function TGenericGF.Inverse(A: Integer): Integer; +begin + CheckInit; + + if (a <> 0) then + begin + Result := FExpTable[FSize - FLogTable[A] - 1]; + end else + begin + Result := 0; + end; +end; + +function TGenericGF.Multiply(A, B: Integer): Integer; +begin + CheckInit; + if ((A <> 0) and (B <> 0)) then + begin + Result := FExpTable[(FLogTable[A] + FLogTable[B]) mod (FSize - 1)]; + end else + begin + Result := 0; + end; +end; + +function GenerateQRCode(const Input: WideString; EncodeOptions: Integer): T2DBooleanArray; +var + Encoder: TEncoder; + Level: TErrorCorrectionLevel; + QRCode: TQRCode; + X: Integer; + Y: Integer; +begin + Level := TErrorCorrectionLevel.Create; + Level.FBits := 1; + Encoder := TEncoder.Create; + QRCode := TQRCode.Create; + try + Encoder.Encode(Input, EncodeOptions, Level, QRCode); + if (Assigned(QRCode.FMatrix)) then + begin + SetLength(Result, QRCode.FMatrix.FHeight); + for Y := 0 to QRCode.FMatrix.FHeight - 1 do + begin + SetLength(Result[Y], QRCode.FMatrix.FWidth); + for X := 0 to QRCode.FMatrix.FWidth - 1 do + begin + Result[Y][X] := QRCode.FMatrix.Get(Y, X) = 1; + end; + end; + end; + finally + QRCode.Free; + Encoder.Free; + Level.Free; + end; +end; + +{ TDelphiZXingQRCode } + +constructor TDelphiZXingQRCode.Create; +begin + FData := ''; + FEncoding := qrAuto; + FQuietZone := 4; + FRows := 0; + FColumns := 0; +end; + +function TDelphiZXingQRCode.GetIsBlack(Row, Column: Integer): Boolean; +begin + Dec(Row, FQuietZone); + Dec(Column, FQuietZone); + if ((Row >= 0) and (Column >= 0) and (Row < (FRows - FQuietZone * 2)) and (Column < (FColumns - FQuietZone * 2))) then + begin + Result := FElements[Column, Row]; + end else + begin + Result := False; + end; +end; + +procedure TDelphiZXingQRCode.SetData(const NewData: WideString); +begin + if (FData <> NewData) then + begin + FData := NewData; + Update; + end; +end; + +procedure TDelphiZXingQRCode.SetEncoding(NewEncoding: TQRCodeEncoding); +begin + if (FEncoding <> NewEncoding) then + begin + FEncoding := NewEncoding; + Update; + end; +end; + +procedure TDelphiZXingQRCode.SetQuietZone(NewQuietZone: Integer); +begin + if ((FQuietZone <> NewQuietZone) and (NewQuietZone >= 0) and (NewQuietZone <= 100)) then + begin + FQuietZone := NewQuietZone; + Update; + end; +end; + +procedure TDelphiZXingQRCode.Update; +begin + FElements := GenerateQRCode(FData, Ord(FEncoding)); + FRows := Length(FElements) + FQuietZone * 2; + FColumns := FRows; +end; + +end. \ No newline at end of file diff --git a/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.dpr b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.dpr new file mode 100644 index 0000000000..8c121a5d98 --- /dev/null +++ b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.dpr @@ -0,0 +1,14 @@ +program DelphiZXingQRCodeTestApp; + +uses + Vcl.Forms, + DelphiZXingQRCodeTestAppMainForm in 'DelphiZXingQRCodeTestAppMainForm.pas' {Form1}; + +{$R *.res} + +begin + Application.Initialize; + Application.MainFormOnTaskbar := True; + Application.CreateForm(TForm1, Form1); + Application.Run; +end. diff --git a/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.dproj b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.dproj new file mode 100644 index 0000000000..f72f248de8 --- /dev/null +++ b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.dproj @@ -0,0 +1,156 @@ + + + {9B95C818-479B-45EB-917E-C5AC561D7C60} + 13.4 + VCL + DelphiZXingQRCodeTestApp.dpr + True + Debug + Win32 + 1 + Application + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + ..\Source\;$(DCC_UnitSearchPath) + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments= + None + 7177 + bindcompfmx;dsnap;fmx;rtl;dbrtl;fmxase;bindcomp;fmxobj;xmlrtl;fmxdae;bindengine;$(DCC_UsePackage) + $(BDS)\bin\delphi_PROJECTICON.ico + System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace) + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + + + bindcompvcl;vcltouch;VclSmp;vcl;dsnapcon;vclx;vclimg;vclactnband;vcldb;vcldsnap;$(DCC_UsePackage) + + + bindcompvcl;vcltouch;vcldbx;VclSmp;vcl;IndyCore;IndySystem;dsnapcon;DelphiAdobeReaderActiveX;vclx;svnui;svn;vclimg;fmi;IndyProtocols;bdertl;vclactnband;vcldb;vcldsnap;$(DCC_UsePackage) + true + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + 1033 + $(BDS)\bin\default_app.manifest + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments= + + + DEBUG;$(DCC_Define) + false + true + true + true + + + true + 1033 + false + + + false + RELEASE;$(DCC_Define) + 0 + false + + + + MainSource + + + Form1 + dfm + + + Cfg_2 + Base + + + Base + + + Cfg_1 + Base + + + + Delphi.Personality.12 + + + + + False + False + 1 + 0 + 0 + 0 + False + False + False + False + False + 7177 + 1252 + + + + + 1.0.0.0 + + + + + + 1.0.0.0 + + + + DelphiZXingQRCodeTestApp.dpr + + + CodeSite Express 5.1 + + + + + False + True + + + 12 + + + + diff --git a/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.res b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.res new file mode 100644 index 0000000000..c287ee9863 Binary files /dev/null and b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestApp.res differ diff --git a/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestAppMainForm.dfm b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestAppMainForm.dfm new file mode 100644 index 0000000000..51c5eb083a --- /dev/null +++ b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestAppMainForm.dfm @@ -0,0 +1,97 @@ +object Form1: TForm1 + Left = 0 + Top = 0 + Caption = 'Delphi port of ZXing QRCode' + ClientHeight = 282 + ClientWidth = 534 + Color = clBtnFace + Constraints.MinHeight = 320 + Constraints.MinWidth = 550 + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + OldCreateOrder = False + OnCreate = FormCreate + OnDestroy = FormDestroy + DesignSize = ( + 534 + 282) + PixelsPerInch = 96 + TextHeight = 13 + object Label1: TLabel + Left = 8 + Top = 13 + Width = 22 + Height = 13 + Caption = 'Text' + end + object Label2: TLabel + Left = 8 + Top = 69 + Width = 43 + Height = 13 + Caption = 'Encoding' + end + object Label3: TLabel + Left = 184 + Top = 69 + Width = 52 + Height = 13 + Caption = 'Quiet zone' + end + object Label4: TLabel + Left = 296 + Top = 13 + Width = 38 + Height = 13 + Caption = 'Preview' + end + object PaintBox1: TPaintBox + Left = 296 + Top = 32 + Width = 230 + Height = 242 + Anchors = [akLeft, akTop, akRight, akBottom] + OnPaint = PaintBox1Paint + ExplicitWidth = 331 + ExplicitHeight = 260 + end + object edtText: TEdit + Left = 8 + Top = 32 + Width = 265 + Height = 21 + TabOrder = 0 + Text = 'Hello world' + OnChange = edtTextChange + end + object cmbEncoding: TComboBox + Left = 8 + Top = 88 + Width = 145 + Height = 21 + Style = csDropDownList + ItemIndex = 0 + TabOrder = 1 + Text = 'Auto' + OnChange = cmbEncodingChange + Items.Strings = ( + 'Auto' + 'Numeric' + 'Alphanumeric' + 'ISO-8859-1' + 'UTF-8 without BOM' + 'UTF-8 with BOM') + end + object edtQuietZone: TEdit + Left = 184 + Top = 88 + Width = 89 + Height = 21 + TabOrder = 2 + Text = '4' + OnChange = edtQuietZoneChange + end +end diff --git a/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestAppMainForm.pas b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestAppMainForm.pas new file mode 100644 index 0000000000..e5be9ece87 --- /dev/null +++ b/windows/src/ext/zxingqrcode/TestApp/DelphiZXingQRCodeTestAppMainForm.pas @@ -0,0 +1,117 @@ +unit DelphiZXingQRCodeTestAppMainForm; + +// Demo app for ZXing QRCode port to Delphi, by Debenu Pty Ltd (www.debenu.com) +// Need a PDF SDK? Checkout Debenu Quick PDF Library: http://www.debenu.com/products/development/debenu-pdf-library/ + +interface + +uses + Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, + Vcl.Controls, Vcl.Forms, Vcl.Dialogs, DelphiZXingQRCode, Vcl.ExtCtrls, + Vcl.StdCtrls; + +type + TForm1 = class(TForm) + edtText: TEdit; + Label1: TLabel; + cmbEncoding: TComboBox; + Label2: TLabel; + Label3: TLabel; + edtQuietZone: TEdit; + Label4: TLabel; + PaintBox1: TPaintBox; + procedure FormDestroy(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure PaintBox1Paint(Sender: TObject); + procedure edtTextChange(Sender: TObject); + procedure cmbEncodingChange(Sender: TObject); + procedure edtQuietZoneChange(Sender: TObject); + private + QRCodeBitmap: TBitmap; + public + procedure Update; + end; + +var + Form1: TForm1; + +implementation + +{$R *.dfm} + +procedure TForm1.cmbEncodingChange(Sender: TObject); +begin + Update; +end; + +procedure TForm1.edtQuietZoneChange(Sender: TObject); +begin + Update; +end; + +procedure TForm1.edtTextChange(Sender: TObject); +begin + Update; +end; + +procedure TForm1.FormCreate(Sender: TObject); +begin + QRCodeBitmap := TBitmap.Create; + Update; +end; + +procedure TForm1.FormDestroy(Sender: TObject); +begin + QRCodeBitmap.Free; +end; + +procedure TForm1.PaintBox1Paint(Sender: TObject); +var + Scale: Double; +begin + PaintBox1.Canvas.Brush.Color := clWhite; + PaintBox1.Canvas.FillRect(Rect(0, 0, PaintBox1.Width, PaintBox1.Height)); + if ((QRCodeBitmap.Width > 0) and (QRCodeBitmap.Height > 0)) then + begin + if (PaintBox1.Width < PaintBox1.Height) then + begin + Scale := PaintBox1.Width / QRCodeBitmap.Width; + end else + begin + Scale := PaintBox1.Height / QRCodeBitmap.Height; + end; + PaintBox1.Canvas.StretchDraw(Rect(0, 0, Trunc(Scale * QRCodeBitmap.Width), Trunc(Scale * QRCodeBitmap.Height)), QRCodeBitmap); + end; +end; + +procedure TForm1.Update; +var + QRCode: TDelphiZXingQRCode; + Row, Column: Integer; +begin + QRCode := TDelphiZXingQRCode.Create; + try + QRCode.Data := edtText.Text; + QRCode.Encoding := TQRCodeEncoding(cmbEncoding.ItemIndex); + QRCode.QuietZone := StrToIntDef(edtQuietZone.Text, 4); + QRCodeBitmap.SetSize(QRCode.Rows, QRCode.Columns); + for Row := 0 to QRCode.Rows - 1 do + begin + for Column := 0 to QRCode.Columns - 1 do + begin + if (QRCode.IsBlack[Row, Column]) then + begin + QRCodeBitmap.Canvas.Pixels[Column, Row] := clBlack; + end else + begin + QRCodeBitmap.Canvas.Pixels[Column, Row] := clWhite; + end; + end; + end; + finally + QRCode.Free; + end; + PaintBox1.Repaint; +end; + +end. diff --git a/windows/src/global/delphi/charmap/UnicodeData.pas b/windows/src/global/delphi/charmap/UnicodeData.pas index 9d982f23fb..32a42fac7c 100644 --- a/windows/src/global/delphi/charmap/UnicodeData.pas +++ b/windows/src/global/delphi/charmap/UnicodeData.pas @@ -201,7 +201,7 @@ type end; const - SKRebuildDatabase = 'TIKE has a Unicode character database, for use in the Character Map, '+ + SKRebuildDatabase = 'Keyman Developer has a Unicode character database, for use in the Character Map, '+ 'that needs to be built. This may take up to a minute to complete.'#13#10#13#10+ 'Do you want to do this now?'; diff --git a/windows/src/global/delphi/comp/KMDActions.pas b/windows/src/global/delphi/comp/KMDActions.pas index 179a87091a..87aac61e51 100644 --- a/windows/src/global/delphi/comp/KMDActions.pas +++ b/windows/src/global/delphi/comp/KMDActions.pas @@ -228,11 +228,17 @@ function IsTextEditor(control: TObject): Boolean; begin Result := Assigned(control) and - (control is TWinControl) and + (control is TWinControl); + if not Result then + Exit; + + if control.ClassName <> 'TframeCEFHost' then + control := TWinControl(control).Owner; + + Result := Assigned(control) and + (control.ClassName = 'TframeCEFHost') and Assigned(TControl(control).Owner) and - (TControl(control).Owner.ClassName = 'TframeCEFHost') and - Assigned(TControl(control).Owner.Owner) and - Supports(TControl(control).Owner.Owner, IKMDTextEditorActions); + Supports(TControl(control).Owner, IKMDTextEditorActions); end; function GetTextEditorController(control: TObject): IKMDTextEditorActions; @@ -240,7 +246,21 @@ begin if not IsTextEditor(control) then Exit(nil); - Result := TControl(control).Owner.Owner as IKMDTextEditorActions; + if control.ClassName <> 'TframeCEFHost' then + control := TWinControl(control).Owner; + + Result := TControl(control).Owner as IKMDTextEditorActions; +end; + +function GetEditController(control: TObject): IKMDEditActions; +begin + if not IsTextEditor(control) then + Exit(nil); + + if control.ClassName <> 'TframeCEFHost' then + control := TWinControl(control).Owner; + + Result := TControl(control).Owner as IKMDEditActions; end; { TKMDEditAction } @@ -262,7 +282,7 @@ end; function TKMDEditAction.GetInterface(Target: TObject): IKMDEditActions; begin if IsTextEditor(Target) then - Result := TControl(Target).Owner.Owner as IKMDEditActions + Result := GetEditController(Target) else if not Supports(TWinControl(Target), IKMDEditActions, Result) then if not Supports(TWinControl(Target).Owner, IKMDEditActions, Result) then raise Exception.Create('Unable to get interface for edit actions'); diff --git a/windows/src/global/delphi/general/Keyman.System.QRCode.pas b/windows/src/global/delphi/general/Keyman.System.QRCode.pas new file mode 100644 index 0000000000..4c2b6cf1eb --- /dev/null +++ b/windows/src/global/delphi/general/Keyman.System.QRCode.pas @@ -0,0 +1,40 @@ +unit Keyman.System.QRCode; + +interface + +uses + DelphiZXingQRCode, + Vcl.Graphics; + +procedure DrawQRCode(const text: string; b: TBitmap); + +implementation + +procedure DrawQRCode(const text: string; b: TBitmap); +var + QRCode: TDelphiZXingQRCode; + Row, Column: Integer; +begin + QRCode := TDelphiZXingQRCode.Create; + try + QRCode.Data := text; + b.SetSize(QRCode.Rows, QRCode.Columns); + for Row := 0 to QRCode.Rows - 1 do + begin + for Column := 0 to QRCode.Columns - 1 do + begin + if (QRCode.IsBlack[Row, Column]) then + begin + b.Canvas.Pixels[Column, Row] := clBlack; + end else + begin + b.Canvas.Pixels[Column, Row] := clWhite; + end; + end; + end; + finally + QRCode.Free; + end; +end; + +end. diff --git a/windows/src/global/delphi/general/RegistryKeys.pas b/windows/src/global/delphi/general/RegistryKeys.pas index 1c9a4cf758..22bd7a5472 100644 --- a/windows/src/global/delphi/general/RegistryKeys.pas +++ b/windows/src/global/delphi/general/RegistryKeys.pas @@ -396,6 +396,8 @@ const SRegValue_IDEOpt_WebLadderLength = 'web ladder length'; // CU CRegValue_IDEOpt_WebLadderLength_Default = 100; + SRegValue_IDEOpt_DefaultProjectPath = 'default project path'; + { SRegKey_KCT values } // SRegValue_KCTTemplatePath = 'template path'; // LM diff --git a/windows/src/global/delphi/general/kpsfile.pas b/windows/src/global/delphi/general/kpsfile.pas index f6c107451c..d92c8b9ae7 100644 --- a/windows/src/global/delphi/general/kpsfile.pas +++ b/windows/src/global/delphi/general/kpsfile.pas @@ -217,6 +217,9 @@ var i: Integer; ANode: IXMLNode; begin + if (Options as TKPSOptions).FollowKeyboardVersion then + Info.Desc[PackageInfo_Version] := ''; + Options.FileVersion := SKeymanVersion70; if FileName <> '' then diff --git a/windows/src/global/inc/Comperr.h b/windows/src/global/inc/Comperr.h index 52bc24c554..b59687e378 100644 --- a/windows/src/global/inc/Comperr.h +++ b/windows/src/global/inc/Comperr.h @@ -189,6 +189,8 @@ #define CWARN_TooManyErrorsOrWarnings 0x000020A7 +#define CWARN_HotkeyHasInvalidModifier 0x000020A8 + #define CERR_BufferOverflow 0x000080C0 #define CERR_Break 0x000080C1 diff --git a/windows/src/test/regressiontest/UfrmRegressionTests.pas b/windows/src/test/regressiontest/UfrmRegressionTests.pas index c87214e572..43848fb0c8 100644 --- a/windows/src/test/regressiontest/UfrmRegressionTests.pas +++ b/windows/src/test/regressiontest/UfrmRegressionTests.pas @@ -754,7 +754,7 @@ begin if not TUtilExecute.Shell(Handle, t + 'tike.exe', FilePath, '"'+FilePath + s + '.kmn"') then begin - ShowMessage('Could not find TIKE to edit the keyboard.'); + ShowMessage('Could not find Keyman Developer to edit the keyboard.'); Exit; end; end; diff --git a/windows/src/test/unit-tests/kmcomp-empty-store-names/emptystorename.kmn b/windows/src/test/unit-tests/kmcomp-empty-store-names/emptystorename.kmn new file mode 100644 index 0000000000..4f3ce2b72e --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp-empty-store-names/emptystorename.kmn @@ -0,0 +1,8 @@ +store(&NAME) 'Test Empty Store Name' +begin Unicode > use(main) + +group(main) using keys + +store(a) 'abc' + +any(a) + 'a' > index(a,1) diff --git a/windows/src/test/unit-tests/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas b/windows/src/test/unit-tests/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas new file mode 100644 index 0000000000..cf11b8e600 --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/Keyman.System.Test.LexicalModelParserTest.pas @@ -0,0 +1,130 @@ +unit Keyman.System.Test.LexicalModelParserTest; + +interface + +uses + System.Classes, + System.SysUtils, + DUnitX.TestFramework; + +type + [TestFixture] + TLexicalModelParserTest = class(TObject) + private + m: TStringList; + AssetRootPath: string; + mcustom: TStringList; + mnocomment: TStrings; + public + [Setup] + procedure Setup; + [TearDown] + procedure TearDown; + + [Test] + procedure TestRead; + + [Test] + procedure TestWrite; + + [Test] + procedure TestWriteDeleteComment; + + [Test] + procedure TestWriteNewComment; + end; + +implementation + +uses + Keyman.Developer.System.LexicalModelParser, + Keyman.Developer.System.LexicalModelParserTypes; + +procedure TLexicalModelParserTest.Setup; +begin + // Running from platform/configuration/ folder has + // assets two levels up. + if FileExists('..\..\assets\nrc.en.mtnt.model.ts') + then AssetRootPath := '..\..\assets\' + else AssetRootPath := ''; + + m := TStringList.Create; + m.LoadFromFile(AssetRootPath + 'nrc.en.mtnt.model.ts'); + + mcustom := TStringList.Create; + mcustom.LoadFromFile(AssetRootPath + 'custom.model.ts'); + + mnocomment := TStringList.Create; + mnocomment.LoadFromFile(AssetRootPath + 'nocomment.model.ts'); +end; + +procedure TLexicalModelParserTest.TearDown; +begin + m.Free; + mcustom.Free; + mnocomment.Free; +end; + +procedure TLexicalModelParserTest.TestRead; +var + lm: TLexicalModelParser; +begin + lm := TLexicalModelParser.Create(m.Text); + try + Assert.IsTrue(lm.Format = lmfTrie10, 'Expected lmfTrie10'); + Assert.IsTrue(lm.WordBreaker = lmwbDefault, 'Expected lmwbDefault'); + Assert.AreEqual(1, lm.Wordlists.Count, 'Expected 1 wordlist'); + Assert.AreEqual('mtnt.tsv', lm.Wordlists[0], 'Wordlist name'); + Assert.AreEqual(#13#10' Sample model for testing parser'#13#10, lm.Comment); + finally + lm.Free; + end; +end; + +procedure TLexicalModelParserTest.TestWrite; +var + lm: TLexicalModelParser; +begin + lm := TLexicalModelParser.Create(m.Text); + try + lm.Comment := 'Testing'; + lm.Format := lmfCustom10; + lm.WordBreaker := lmwbAscii; + lm.Wordlists.Clear; + lm.Wordlists.Add('foo.tsv'); + lm.Wordlists.Add('bar.tsv'); + Assert.AreEqual(mcustom.Text.Trim, lm.Text.Trim); // Ignoring whitespace before/after + finally + lm.Free; + end; +end; + +procedure TLexicalModelParserTest.TestWriteNewComment; +var + lm: TLexicalModelParser; +begin + lm := TLexicalModelParser.Create(mnocomment.Text); + try + lm.Comment := #13#10' Sample model for testing parser'#13#10; + Assert.AreEqual(m.Text.Trim, lm.Text.Trim); // Ignoring whitespace before/after + finally + lm.Free; + end; +end; + +procedure TLexicalModelParserTest.TestWriteDeleteComment; +var + lm: TLexicalModelParser; +begin + lm := TLexicalModelParser.Create(m.Text); + try + lm.Comment := ''; + Assert.AreEqual(mnocomment.Text.Trim, lm.Text.Trim); // Ignoring whitespace before/after + finally + lm.Free; + end; +end; + +initialization + TDUnitX.RegisterTestFixture(TLexicalModelParserTest); +end. diff --git a/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dpr b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dpr new file mode 100644 index 0000000000..214fb595a2 --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dpr @@ -0,0 +1,61 @@ +program ModelTsParserTest; + +{$IFNDEF TESTINSIGHT} +{$APPTYPE CONSOLE} +{$ENDIF}{$STRONGLINKTYPES ON} +uses + System.SysUtils, + {$IFDEF TESTINSIGHT} + TestInsight.DUnitX, + {$ENDIF } + DUnitX.Loggers.Console, + DUnitX.Loggers.Xml.NUnit, + DUnitX.TestFramework, + Keyman.System.Test.LexicalModelParserTest in 'Keyman.System.Test.LexicalModelParserTest.pas', + Keyman.Developer.System.LexicalModelParser in '..\..\..\developer\TIKE\main\Keyman.Developer.System.LexicalModelParser.pas', + Keyman.Developer.System.LexicalModelParserTypes in '..\..\..\developer\TIKE\main\Keyman.Developer.System.LexicalModelParserTypes.pas'; + +var + runner : ITestRunner; + results : IRunResults; + logger : ITestLogger; + nunitLogger : ITestLogger; +begin +{$IFDEF TESTINSIGHT} + TestInsight.DUnitX.RunRegisteredTests; + exit; +{$ENDIF} + try + //Check command line options, will exit if invalid + TDUnitX.CheckCommandLine; + //Create the test runner + runner := TDUnitX.CreateRunner; + //Tell the runner to use RTTI to find Fixtures + runner.UseRTTI := True; + //tell the runner how we will log things + //Log to the console window + logger := TDUnitXConsoleLogger.Create(true); + runner.AddLogger(logger); + //Generate an NUnit compatible XML File + nunitLogger := TDUnitXXMLNUnitFileLogger.Create(TDUnitX.Options.XMLOutputFile); + runner.AddLogger(nunitLogger); + runner.FailsOnNoAsserts := False; //When true, Assertions must be made during tests; + + //Run tests + results := runner.Execute; + if not results.AllPassed then + System.ExitCode := EXIT_ERRORS; + + {$IFNDEF CI} + //We don't want this happening when running under CI. + if TDUnitX.Options.ExitBehavior = TDUnitXExitBehavior.Pause then + begin + System.Write('Done.. press key to quit.'); + System.Readln; + end; + {$ENDIF} + except + on E: Exception do + System.Writeln(E.ClassName, ': ', E.Message); + end; +end. diff --git a/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dproj b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dproj new file mode 100644 index 0000000000..bbfac6492b --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.dproj @@ -0,0 +1,765 @@ + + + {0158CB55-B9BC-4F32-B860-FD6073AE2EB7} + 18.7 + None + ModelTsParserTest.dpr + True + Debug + Win32 + 1 + Console + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + $(BDS)\bin\delphi_PROJECTICON.ico + $(BDS)\bin\delphi_PROJECTICNS.icns + $(DUnitX);$(DCC_UnitSearchPath) + ModelTsParserTest + + + DBXSqliteDriver;IndyIPCommon;RESTComponents;bindcompdbx;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;svnui;mbColorLibD10;dsnapcon;FireDACADSDriver;scFontCombo;DCPdelphi2009;FireDACMSAccDriver;fmxFireDAC;vclimg;Jcl;FireDAC;vcltouch;JvCore;vcldb;bindcompfmx;svn;FireDACSqliteDriver;FireDACPgDriver;inetdb;CEF4Delphi;soaprtl;DbxCommonDriver;fmx;FireDACIBDriver;fmxdae;xmlrtl;soapmidas;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;EmbeddedWebBrowser_XE;VCLRESTComponents;soapserver;dbxcds;VclSmp;JvDocking;adortl;JclVcl;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;IndyProtocols;inetdbxpress;keyman_components;FireDACCommonODBC;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + DBXSqliteDriver;IndyIPCommon;RESTComponents;bindcompdbx;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;dsnapcon;FireDACADSDriver;FireDACMSAccDriver;fmxFireDAC;vclimg;Jcl;FireDAC;vcltouch;vcldb;bindcompfmx;FireDACSqliteDriver;FireDACPgDriver;inetdb;soaprtl;DbxCommonDriver;fmx;FireDACIBDriver;fmxdae;xmlrtl;soapmidas;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;VCLRESTComponents;soapserver;dbxcds;VclSmp;adortl;JclVcl;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;IndyProtocols;inetdbxpress;FireDACCommonODBC;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage) + + + DEBUG;$(DCC_Define) + true + false + true + true + true + + + false + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + + MainSource + + + + + + Cfg_2 + Base + + + Base + + + Cfg_1 + Base + + + + Delphi.Personality.12 + Console + + + + ModelTsParserTest.dpr + + + + + + true + + + + + true + + + + + true + + + + + ModelTsParserTest.exe + true + + + + + 1 + + + 0 + + + + + classes + 1 + + + + + res\xml + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\armeabi + 1 + + + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + + + res\values + 1 + + + + + res\values-v21 + 1 + + + + + res\values + 1 + + + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + + + 1 + + + 1 + + + 0 + + + + + 1 + .framework + + + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + 1 + + + 1 + + + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + + + + 1 + + + 1 + + + 1 + + + + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 0 + + + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + + + + + + + + + + True + False + + + 12 + + + + + diff --git a/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.res b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.res new file mode 100644 index 0000000000..6876088a66 Binary files /dev/null and b/windows/src/test/unit-tests/model-ts-parser/ModelTsParserTest.res differ diff --git a/windows/src/test/unit-tests/model-ts-parser/assets/custom.model.ts b/windows/src/test/unit-tests/model-ts-parser/assets/custom.model.ts new file mode 100644 index 0000000000..64e8f8e122 --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/assets/custom.model.ts @@ -0,0 +1,8 @@ +/*Testing*/ +const source: LexicalModelSource = { + format: 'custom-1.0', + wordBreaker: 'ascii', + sources: ['foo.tsv', 'bar.tsv'] +}; + +export default source; diff --git a/windows/src/test/unit-tests/model-ts-parser/assets/nocomment.model.ts b/windows/src/test/unit-tests/model-ts-parser/assets/nocomment.model.ts new file mode 100644 index 0000000000..3d709e48a3 --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/assets/nocomment.model.ts @@ -0,0 +1,7 @@ +const source: LexicalModelSource = { + format: 'trie-1.0', + wordBreaker: 'default', + sources: ['mtnt.tsv'] +}; + +export default source; diff --git a/windows/src/test/unit-tests/model-ts-parser/assets/nrc.en.mtnt.model.ts b/windows/src/test/unit-tests/model-ts-parser/assets/nrc.en.mtnt.model.ts new file mode 100644 index 0000000000..2d1cf76cda --- /dev/null +++ b/windows/src/test/unit-tests/model-ts-parser/assets/nrc.en.mtnt.model.ts @@ -0,0 +1,10 @@ +/* + Sample model for testing parser +*/ +const source: LexicalModelSource = { + format: 'trie-1.0', + wordBreaker: 'default', + sources: ['mtnt.tsv'] +}; + +export default source;