diff --git a/.github/workflows/deb-packaging.README.md b/.github/workflows/deb-packaging.README.md index a1b1c12d122..6bbeabd9e1e 100644 --- a/.github/workflows/deb-packaging.README.md +++ b/.github/workflows/deb-packaging.README.md @@ -14,9 +14,28 @@ You can manually trigger a deb-packaging action on GitHub, e.g. to test changes: --data "{ \"event_type\": \"deb-pr-packaging: master\", \ \"client_payload\": { \ \"buildSha\": \"$(git rev-parse refs/heads/master)\", \ - \"headSha\": \"$(git rev-parse refs/heads/master)\", \ \"branch\": \"master\", \ + \"baseBranch\": \"master\", + \"baseRef\": \"$(git rev-parse refs/heads/master^)\", + \"user\": \"${USER}\", \"isTestBuild\": \"true\"}" \ https://api.github.com/repos//keyman/dispatches + ``` + + To trigger a build for PR #1234 in a branch `pr-1234` this would look similar + to this: + ```bash + curl --write-out '\n' --request POST \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: token $GITHUB_TOKEN" \ + --data "{ \"event_type\": \"deb-pr-packaging: PR #1234\", \ + \"client_payload\": { \ + \"buildSha\": \"$(git rev-parse refs/heads/pr-1234)\", \ + \"branch\": \"pr-1234\", \ + \"baseBranch\": \"master\", + \"baseRef\": \"$(git rev-parse refs/heads/master)\", + \"user\": \"${USER}\", + \"isTestBuild\": \"true\"}" \ + https://api.github.com/repos//keyman/dispatches ``` diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 10edcbab173..a86c60ec3de 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -1,15 +1,16 @@ name: "Ubuntu packaging" -run-name: "Ubuntu packaging - ${{ github.event.client_payload.branch }} (branch ${{ github.head_ref }}), by @${{ github.actor }}" +run-name: "Ubuntu packaging - ${{ github.event.client_payload.branch }} (branch ${{ github.event.client_payload.baseBranch }}), by @${{ github.event.client_payload.user }}" on: repository_dispatch: types: ['deb-release-packaging:*', 'deb-pr-packaging:*'] # Input: # buildSha: The SHA of the commit to build, e.g. of the branch or -# refs/pull/1234/merge for PR -# headSha: The SHA of the branch head. Same as buildSha for regular -# builds, SHA of refs/pull/1234/head for PRs +# refs/pull/1234/head for PR # branch: The branch to build, for a PR in the form `PR-1234` +# baseBranch: For a PR the base branch, otherwise the same as `branch` +# baseRef: The ref of the previous commit. For a PR the same as `baseBranch`. +# user: The user that triggered the build or created the PR # isTestBuild: false for Releases, otherwise true env: @@ -40,7 +41,7 @@ jobs: gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ - /repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.headSha }} \ + /repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \ -f state='pending' \ -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ -f description='Debian packaging started' \ @@ -236,6 +237,7 @@ jobs: uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c #v3.3.0 with: ref: '${{ github.event.client_payload.buildSha }}' + fetch-depth: 0 - name: Download Artifacts uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 @@ -251,10 +253,12 @@ jobs: run: | cd linux PKG_NAME=libkeymancore - SRC_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-srcpkg/keyman_${{ needs.sourcepackage.outputs.VERSION }}-1.debian.tar.xz" \ - BIN_PKG="${GITHUB_WORKSPACE}/artifacts/keyman-binarypkgs/${PKG_NAME}_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+jammy1_amd64.deb" \ - PKG_VERSION="${{ needs.sourcepackage.outputs.VERSION }}" \ - ./scripts/deb-packaging.sh --gha verify 2>> $GITHUB_STEP_SUMMARY + ./scripts/deb-packaging.sh \ + --gha \ + --bin-pkg "${GITHUB_WORKSPACE}/artifacts/keyman-binarypkgs/${PKG_NAME}_${{ needs.sourcepackage.outputs.VERSION }}-1${{ needs.sourcepackage.outputs.PRERELEASE_TAG }}+jammy1_amd64.deb" \ + --git-sha "${{ github.event.client_payload.buildSha }}" \ + --git-base "${{ github.event.client_payload.baseRef }}" \ + verify 2>> $GITHUB_STEP_SUMMARY - name: Archive .symbols file uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 @@ -292,7 +296,7 @@ jobs: gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ - /repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.headSha }} \ + /repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \ -f state="$RESULT" \ -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ -f description="$MSG" \ diff --git a/HISTORY.md b/HISTORY.md index a8c2c21b1f7..2342633deda 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,67 @@ # Keyman Version History +## 17.0.216 alpha 2023-11-23 + +* fix(common): kmx struct alignment (#9977) +* fix(developer): vis kbd callbacks instead of throw (#9979) +* fix(core): dx: ldml: startContext should be optional in ldml test data (#10021) +* chore(windows): remove `wm_keymandebug` messages and functions (#10055) +* chore(windows): remove legacy LoadKeyboard (#10057) +* chore(windows): remove unused globals relating to old keyboard debugging (#10058) +* chore(core): remove vkey output from kmx processor (#10060) +* fix(web): app/webview did not clear deadkeys on context-reset (#10039) + +## 17.0.215 alpha 2023-11-22 + +* feat(core): ldml marker normalization (#9761) +* feat(core): ldml backspace transform (#9960) +* feat(core): ldml tertiary reordering (#9962) +* fix(web): fixes OSK viewport scaling on iOS devices (#10035) + +## 17.0.214 alpha 2023-11-20 + +* fix(ios): fv: replace Zip framework to prevent crash on startup (#10018) + +## 17.0.213 alpha 2023-11-17 + +* fix(linux): Fix packaging GHA (#10020) +* fix(android): Always display HTML banner when suggestions aren't available (#9696) + +## 17.0.212 alpha 2023-11-16 + +* chore(web): splits banner.ts into separate files per banner type (#9987) +* refactor(web): inactive banner management (#9988) + +## 17.0.211 alpha 2023-11-15 + +* fix(windows): setup.inf generation had whitespace (#10000) +* feat(developer): Support v2.0 projects in TIKE (#9949) +* feat(developer): New Project - Description field and tweaks (#9950) +* chore(developer): hide 'Remove From Project' for v2.0 projects (#9956) +* feat(developer): Support loading XML LDML keyboards in TIKE (#9963) +* feat(developer): show .xml LDML keyboards in project (#9964) +* fix(developer): kmc build ldml keyboard should create output folder (#9966) +* fix(developer): Project upgrade messages now show in Messages panel (#9969) +* feat(developer): Project Settings Form for 17.0+ projects (#9984) +* chore(developer): only build source files (#9985) +* chore(developer): manage SourcePath in project upgrade (#9986) +* feat(developer): handle errors loading projects (#9989) +* chore(developer): show only source path files in project views (#9995) +* chore(developer): handle project version checks cleanly in kmc (#9996) +* fix(common): dx: don't call exit(0) on failure (#10005) +* fix(linux): Improve triggering of packaging GHA (#10009) +* chore(web,developer): Move keymanweb-osk.ttf to common/resources (#9993) + +## 17.0.210 alpha 2023-11-14 + +* docs(common): Update CODEOWNERS for web (#9997) +* chore(core): Add additional API checks (#9867) + +## 17.0.209 alpha 2023-11-13 + +* docs(windows): update meson and emscripten details (#9933) +* fix(web): Increase size of spacebar text (#9954) + ## 17.0.208 alpha 2023-11-08 * fix(web): Fix clearing of deadkeys (#9944) diff --git a/VERSION.md b/VERSION.md index 8b8d8ba5257..980da1ad1ae 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.209 \ No newline at end of file +17.0.217 \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/assets/banner.html b/android/KMAPro/kMAPro/src/main/assets/banner.html new file mode 100644 index 00000000000..41107f11424 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/assets/banner.html @@ -0,0 +1,11 @@ + +
+ + + + + +
+
+
+
diff --git a/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg b/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg new file mode 100644 index 00000000000..3888a0937f9 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg @@ -0,0 +1,372 @@ + + + + image/svg+xml diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java new file mode 100644 index 00000000000..f716faa9aab --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java @@ -0,0 +1,29 @@ +package com.keyman.android; + +import android.content.Context; + +import com.keyman.engine.KMManager; +import com.keyman.engine.util.FileUtils; + +import java.io.File; + +public class BannerController { + + // Paths relative to assets folder for banner themes + public static final String KM_BANNER_DIR = "banner"; + public static final String KM_BANNER_THEME_KEYMAN = "banner.html"; + + public static void setHTMLBanner(Context context, KMManager.KeyboardType keyboardType) { + if (keyboardType == KMManager.KeyboardType.KEYBOARD_TYPE_UNDEFINED) { + return; + } + + KMManager.copyHTMLBannerAssets(context, KM_BANNER_DIR); + + // Always use Keyman banner theme + String contents = FileUtils.readContents(context, KM_BANNER_THEME_KEYMAN); + KMManager.setHTMLBanner(keyboardType, contents); + KMManager.setBanner(keyboardType, KMManager.BannerType.HTML); + KMManager.showBanner(true); + } +} diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 697825094bf..cf919deddb4 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -6,6 +6,7 @@ import com.tavultesoft.kmapro.BuildConfig; import com.tavultesoft.kmapro.KeymanSettingsActivity; +import com.keyman.android.BannerController; import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; import com.keyman.engine.KMHardwareKeyboardInterpreter; @@ -71,6 +72,9 @@ public void onCreate() { KMManager.SpacebarText spacebarText = KMManager.SpacebarText.fromString(prefs.getString(KeymanSettingsActivity.spacebarTextKey, KMManager.SpacebarText.LANGUAGE_KEYBOARD.toString())); KMManager.setSpacebarText(spacebarText); + // Set the system keyboard HTML banner + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); + boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsActivity.hapticFeedbackKey, false); KMManager.setHapticFeedback(mayHaveHapticFeedback); @@ -93,6 +97,9 @@ public void onDestroy() { @Override public void onInitializeInterface() { super.onInitializeInterface(); + + // KeymanWeb reloaded, so we have to pass the banner again + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); } /** diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 6b92d4c2eb5..16899f7c2d9 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -18,6 +18,7 @@ import java.util.Map; import com.keyman.android.CheckInstallReferrer; +import com.keyman.android.BannerController; import com.keyman.engine.BaseActivity; import com.keyman.engine.KMHelpFileActivity; import com.keyman.engine.KMKeyboardDownloaderActivity; @@ -499,6 +500,8 @@ public void onKeyboardChanged(String newKeyboard) { @Override public void onKeyboardShown() { + // Refresh banner theme + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_INAPP); resizeTextView(true); } diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index afa2825e33d..7930c76f11f 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -8,6 +8,9 @@ if(window.parent && window.parent.jsInterface && !window.jsInterface) { var device = window.jsInterface.getDeviceType(); var oskHeight = Math.ceil(window.jsInterface.getKeyboardHeight() / window.devicePixelRatio); var oskWidth = 0; +var bannerHeight = 0; +var bannerImagePath = ''; +var bannerHTMLContents = ''; var fragmentToggle = 0; var sentryManager = new KeymanSentryManager({ @@ -37,11 +40,13 @@ function init() { oninserttext: insertText, root:'./' }).then(function () { // Note: For non-upgraded API 21, arrow functions will break the keyboard! - const bannerHeight = Math.ceil(window.jsInterface.getDefaultBannerHeight() / window.devicePixelRatio); + bannerHeight = Math.ceil(window.jsInterface.getDefaultBannerHeight() / window.devicePixelRatio); + if (bannerHeight > 0) { - // The OSK is not available until initialization is complete. - keyman.osk.bannerView.activeBannerHeight = bannerHeight; - keyman.refreshOskLayout(); + // The OSK is not available until initialization is complete. + keyman.osk.bannerView.activeBannerHeight = bannerHeight; + keyman.refreshOskLayout(); + } }); keyman.addEventListener('keyboardloaded', setIsChiral); @@ -53,6 +58,29 @@ function init() { notifyHost('pageLoaded'); } +function showBanner(flag) { + console_debug("Setting banner display for dictionaryless keyboards to " + flag); + console_debug("bannerHTMLContents: " + bannerHTMLContents); + var bc = keyman.osk.bannerController; + if (bc) { + if (bannerHTMLContents != '') { + bc.inactiveBanner = flag ? new bc.HTMLBanner(bannerHTMLContents) : null; + } else { + bc.inactiveBanner = flag ? new bc.ImageBanner(bannerImagePath) : null; + } + } +} + +function setBannerImage(path) { + bannerImagePath = path; +} + +// Set the HTML banner to use when predictive-text is not available +// contents - HTML content to use for the banner +function setBannerHTML(contents) { + bannerHTMLContents = contents; +} + function notifyHost(event, params) { console_debug('notifyHost(event='+event+',params='+params+')'); // TODO: Update all other host notifications to use notifyHost instead of directly setting window.location.hash @@ -65,12 +93,19 @@ function notifyHost(event, params) { } // Update the KMW banner height +// h is in dpi (different from iOS) function setBannerHeight(h) { if (h > 0) { - var osk = keyman.osk; - osk.banner.height = Math.ceil(h / window.devicePixelRatio); + // The banner itself may not be loaded yet. This will preemptively help set + // its eventual display height. + bannerHeight = Math.ceil(h / window.devicePixelRatio); + + if (keyman.osk) { + keyman.osk.bannerView.activeBannerHeight = bannerHeight; + } } - // Refresh KMW OSK + + // Refresh KMW's OSK keyman.refreshOskLayout(); } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index 6471e875101..215630cd03d 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -7,13 +7,11 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import com.keyman.engine.BaseActivity; import com.keyman.engine.data.Keyboard; import com.keyman.engine.data.KeyboardController; import com.keyman.engine.KMManager.KeyboardType; @@ -25,19 +23,11 @@ import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMString; -import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Typeface; -import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; @@ -46,7 +36,6 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -56,12 +45,9 @@ import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.GridLayout; import android.widget.PopupWindow; import android.widget.PopupWindow.OnDismissListener; import android.widget.RelativeLayout; -import android.widget.TextView; import android.widget.Toast; import io.sentry.Breadcrumb; @@ -84,19 +70,10 @@ final class KMKeyboard extends WebView { private static String currentKeyboard = null; - /** - * Banner state value: "blank" - no banner available. - */ - protected static final String KM_BANNER_STATE_BLANK = "blank"; - /** - * Banner state value: "suggestion" - dictionary suggestions are shown. - */ - protected static final String KM_BANNER_STATE_SUGGESTION = "suggestion"; - /** * Current banner state. */ - protected static String currentBanner = KM_BANNER_STATE_BLANK; + protected static KMManager.BannerType currentBanner = KMManager.BannerType.HTML; private static String txtFont = ""; private static String oskFont = null; @@ -105,6 +82,10 @@ final class KMKeyboard extends WebView { private GestureDetector gestureDetector; private static ArrayList kbEventListeners = null; + // Stores the current html string for use by the Banner + // when predictive text is not active + protected String htmlBannerString = ""; + // Facilitates a 'lazy init' - we'll only check the preference when it matters, // rather than at construction time. private Boolean _shouldShowHelpBubble = null; @@ -400,6 +381,9 @@ public void onConfigurationChanged(Configuration newConfig) { int bannerHeight = KMManager.getBannerHeight(context); int oskHeight = KMManager.getKeyboardHeight(context); + if (this.htmlBannerString != null && !this.htmlBannerString.isEmpty()) { + setHTMLBanner(this.htmlBannerString); + } loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight)); loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp)); loadJavascript(KMString.format("setOskHeight(%d)", oskHeight)); @@ -425,22 +409,15 @@ public static String currentKeyboard() { return currentKeyboard; } - public static void setCurrentBanner(String banner) { - currentBanner = banner; - } - - public static String currentBanner() { return currentBanner; } - protected void toggleSuggestionBanner(HashMap associatedLexicalModel, boolean keyboardChanged) { //reset banner state if new language has no lexical model - if (currentBanner != null && currentBanner.equals(KM_BANNER_STATE_SUGGESTION) + if (currentBanner == KMManager.BannerType.SUGGESTION && associatedLexicalModel == null) { - setCurrentBanner(KMKeyboard.KM_BANNER_STATE_BLANK); + currentBanner = KMManager.BannerType.HTML; } - if(keyboardChanged) { - setLayoutParams(KMManager.getKeyboardLayoutParams()); - } + showBanner(true); + // Since there's always a banner, no need to update setLayoutParams() } /** @@ -653,6 +630,30 @@ public boolean setKeyboard(String packageID, String keyboardID, String languageI return retVal; } + public void showBanner(boolean flag) { + String jsString = KMString.format("showBanner(%b)", flag); + loadJavascript(jsString); + } + + public KMManager.BannerType getBanner() { + return currentBanner; + } + + public void setBanner(KMManager.BannerType bannerType) { + currentBanner = bannerType; + } + + public String getHTMLBanner() { + return this.htmlBannerString; + } + + public void setHTMLBanner(String contents) { + this.htmlBannerString = contents; + String jsString = KMString.format("setBannerHTML(%s)", + JSONObject.quote(this.htmlBannerString)); + loadJavascript(jsString); + } + public void setChirality(boolean flag) { this.isChiral = flag; } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index e7188a94f96..4d458bd0616 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -173,8 +173,7 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { if (KMManager.currentLexicalModel != null) { modelPredictionPref = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), true); } - kmKeyboard.setCurrentBanner((isModelActive && modelPredictionPref) ? - KMKeyboard.KM_BANNER_STATE_SUGGESTION : KMKeyboard.KM_BANNER_STATE_BLANK); + KMManager.setBannerOptions(isModelActive && modelPredictionPref); RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams(); kmKeyboard.setLayoutParams(params); } else if (url.indexOf("suggestPopup") >= 0) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index cbcb28053a1..df90f1d869d 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -25,32 +25,21 @@ import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.RectF; import android.graphics.Typeface; import android.inputmethodservice.InputMethodService; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.net.Uri; import android.os.Build; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.text.InputType; import android.util.Log; -import android.view.HapticFeedbackConstants; -import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.webkit.JavascriptInterface; import android.webkit.WebView; -import android.webkit.WebViewClient; import android.widget.FrameLayout; import android.widget.RelativeLayout; @@ -71,7 +60,6 @@ import com.keyman.engine.packages.LexicalModelPackageProcessor; import com.keyman.engine.packages.PackageProcessor; import com.keyman.engine.util.BCP47; -import com.keyman.engine.util.CharSequenceUtil; import com.keyman.engine.util.DependencyUtil; import com.keyman.engine.util.DependencyUtil.LibraryType; import com.keyman.engine.util.FileUtils; @@ -157,6 +145,34 @@ public String toString() { } }; + // Maps to enum BannerType in bannerView.ts + public enum BannerType { + BLANK, + IMAGE, + SUGGESTION, + HTML; + + public static BannerType fromString(String mode) { + if (mode == null) return BLANK; + switch (mode) { + case "BLANK": + return BLANK; + case "image": + return IMAGE; + case "suggestion": + return SUGGESTION; + case "html": + return HTML; + } + return BLANK; + } + + public String toString() { + String modes[] = { "blank", "image", "suggestion", "html"}; + return modes[this.ordinal()]; + } + } + protected static InputMethodService IMService; private static boolean debugMode = false; @@ -287,6 +303,9 @@ public String toString() { public static final String KMFilename_LexicalModelsList = "lexical_models_list.dat"; + public static final String KMBLACK_BANNER = "
"; + public static final String KMGRAY_BANNER = "
"; + private static Context appContext; public static String getResourceRoot() { @@ -636,6 +655,16 @@ private static void initKeyboard(Context appContext, KeyboardType keyboardType) keyboard.addJavascriptInterface(new KMKeyboardJSHandler(appContext, keyboard), "jsInterface"); keyboard.loadKeyboard(); + if (!isTestMode()) { + // For apps that don't specify an HTML banner, specify a default phone/tablet HTML banner + if (getFormFactor() == FormFactor.PHONE) { + keyboard.setHTMLBanner(KMBLACK_BANNER); + } else { + keyboard.setHTMLBanner(KMGRAY_BANNER); + } + keyboard.setBanner(KMManager.BannerType.HTML); + keyboard.showBanner(true); + } setEngineWebViewVersionStatus(appContext, keyboard); } @@ -793,6 +822,31 @@ public static boolean hasInternetPermission(Context context) { return hasPermission(context, Manifest.permission.INTERNET); } + /** + * Copy HTML banner assets to the app + * @param context - The context + * @param path - Folder relative to assets/ containing the banner file. + * @return boolean - true if assets copied + */ + public static boolean copyHTMLBannerAssets(Context context, String path) { + AssetManager assetManager = context.getAssets(); + try { + File bannerDir = new File(getResourceRoot() + File.separator + path); + if (!bannerDir.exists()) { + bannerDir.mkdir(); + } + + String[] bannerFiles = assetManager.list(path); + for (String bannerFile : bannerFiles) { + copyAsset(context, bannerFile, path, true); + } + return true; + } catch (Exception e) { + KMLog.LogException(TAG, "copyHTMLBannerAssets() failed. Error: ", e); + } + return false; + } + private static void copyAssets(Context context) { AssetManager assetManager = context.getAssets(); try { @@ -1370,7 +1424,13 @@ public static void deleteLexicalModel(Context context, int position, boolean sil KeyboardPickerActivity.deleteLexicalModel(context, position, silenceNotification); } - public static boolean setBannerOptions(boolean mayPredict) { + /** + * setBannerOptions - Update KMW whether to generate predictions. + * For now, also display banner + * @param mayPredict - boolean whether KMW should generate predictions + * @return boolean - Success + */ + public static boolean setBannerOptions(boolean mayPredict) { String url = KMString.format("setBannerOptions(%s)", mayPredict); if (InAppKeyboard != null) { InAppKeyboard.loadJavascript(url); @@ -1379,6 +1439,73 @@ public static boolean setBannerOptions(boolean mayPredict) { if (SystemKeyboard != null) { SystemKeyboard.loadJavascript(url); } + + return true; + } + + /** + * Update KeymanWeb banner type + * @param {KeyboardType} keyboard + * @param {BannerType} bannerType + * @return status + */ + public static boolean setBanner(KeyboardType keyboard, BannerType bannerType) { + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) { + InAppKeyboard.setBanner(bannerType); + } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) { + SystemKeyboard.setBanner(bannerType); + } else { + return false; + } + return true; + } + + /** + * Set the HTML content to use with the HTML banner + * @param {KeyboardType} keyboard + * @param {String} HTMl string + * @return {boolean} + */ + public static boolean setHTMLBanner(KeyboardType keyboard, String htmlContent) { + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) { + InAppKeyboard.setHTMLBanner(htmlContent); + } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) { + SystemKeyboard.setHTMLBanner(htmlContent); + } else { + Log.d(TAG, "setHTMLBanner() but keyboard is null"); + return false; + } + return true; + } + + /** + * Get the HTML content associated with the HTML banner + * @param {KeyboardType} keyboard + * @return {String} + */ + public static String getHTMLBanner(KeyboardType keyboard) { + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) { + return InAppKeyboard.getHTMLBanner(); + } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) { + return SystemKeyboard.getHTMLBanner(); + } + return ""; + } + + /** + * showBanner - Update KMW whether to display banner. + * For now, always keep displaying banner + * @param flag - boolean whether KMW should display banner + * @return boolean - Success + */ + public static boolean showBanner(boolean flag) { + if (InAppKeyboard != null) { + InAppKeyboard.showBanner(flag); + } + + if (SystemKeyboard != null) { + SystemKeyboard.showBanner(flag); + } return true; } @@ -1845,9 +1972,9 @@ public static void removeKeyboardEventListener(OnKeyboardEventListener listener) public static int getBannerHeight(Context context) { int bannerHeight = 0; - if (InAppKeyboard != null && InAppKeyboard.currentBanner().equals(KMKeyboard.KM_BANNER_STATE_SUGGESTION)) { + if (InAppKeyboard != null && InAppKeyboard.getBanner() != BannerType.BLANK) { bannerHeight = (int) context.getResources().getDimension(R.dimen.banner_height); - } else if (SystemKeyboard != null && SystemKeyboard.currentBanner().equals(KMKeyboard.KM_BANNER_STATE_SUGGESTION)) { + } else if (SystemKeyboard != null && SystemKeyboard.getBanner() != BannerType.BLANK) { bannerHeight = (int) context.getResources().getDimension(R.dimen.banner_height); } return bannerHeight; diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java index d5610dedec2..6b04b974f8a 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java @@ -4,16 +4,22 @@ package com.keyman.engine.util; import android.content.Context; +import android.content.res.AssetManager; +import android.util.Log; + +import com.keyman.engine.KMManager; import org.json.JSONObject; import org.json.JSONArray; import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; @@ -248,6 +254,35 @@ public static boolean saveList(File filepath, Object obj) { return result; } + /** + * Read the contents of asset file as a string + * Reference: https://stackoverflow.com/questions/16110002/read-assets-file-as-string + * @param context + * @param path - path of file relative to assets folder + * @return String + */ + public static String readContents(Context context, String path) { + StringBuilder sb = new StringBuilder(); + String str = ""; + AssetManager assetManager = context.getAssets(); + try { + InputStream inputStream = assetManager.open(path); + if (inputStream == null) { + KMLog.LogInfo(TAG, "Unable to read contents of asset: " + path); + return str; + } + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + while ((str = reader.readLine()) != null) { + sb.append(str); + } + reader.close(); + } catch (Exception e) { + KMLog.LogException(TAG, "Error reading asset file", e); + return str; + } + return sb.toString(); + } + /** * Utility to parse a URL and extract the filename * @param urlStr String diff --git a/common/include/km_types.h b/common/include/km_types.h index aa7fb2e5182..0ae6252752d 100644 --- a/common/include/km_types.h +++ b/common/include/km_types.h @@ -67,3 +67,29 @@ typedef wchar_t KMX_UCHAR; #endif typedef KMX_UCHAR* KMX_PUCHAR; + +// Alignment + +/* + When we read .kmx files, they have no alignment guarantees, so we need to tell + the compiler to generate unaligned-safe code for accesses to COMP_ structure + members. Note we are assuming that COMP_KEYBOARD is aligned because it is + always the start of the file, so will be at the start of any buffer which will + automatically be aligned correctly. + + TODO: consider using c++11 alignas +*/ +#if defined(__EMSCRIPTEN__) || defined (__GNUC__) || defined (__clang__) +typedef KMX_DWORD __attribute__((aligned(1))) KMX_DWORD_unaligned; +typedef KMX_BOOL __attribute__((aligned(1))) KMX_BOOL_unaligned; +typedef KMX_WORD __attribute__((aligned(1))) KMX_WORD_unaligned; +#elif defined(_MSC_VER) +typedef KMX_DWORD __declspec(align(1)) KMX_DWORD_unaligned; +typedef KMX_BOOL __declspec(align(1)) KMX_BOOL_unaligned; +typedef KMX_WORD __declspec(align(1)) KMX_WORD_unaligned; +#else +// TODO: consider other platforms +#define KMX_DWORD_unaligned KMX_DWORD +#define KMX_BOOL_unaligned KMX_BOOL +#define KMX_WORD_unaligned KMX_WORD +#endif diff --git a/common/include/kmx_file.h b/common/include/kmx_file.h index 7dbc2261d0d..cd379489895 100644 --- a/common/include/kmx_file.h +++ b/common/include/kmx_file.h @@ -7,24 +7,6 @@ #include -/* - When we read .kmx files, they have no alignment guarantees, so we need to tell - the compiler to generate unaligned-safe code for accesses to COMP_ structure - members. Note we are assuming that COMP_KEYBOARD is aligned because it is - always the start of the file, so will be at the start of any buffer which will - automatically be aligned correctly. -*/ -#ifdef __EMSCRIPTEN__ -typedef KMX_DWORD __attribute__((aligned(1))) KMX_DWORD_unaligned; -typedef KMX_BOOL __attribute__((aligned(1))) KMX_BOOL_unaligned; -typedef KMX_WORD __attribute__((aligned(1))) KMX_WORD_unaligned; -#else -// TODO: consider other platforms -#define KMX_DWORD_unaligned KMX_DWORD -#define KMX_BOOL_unaligned KMX_BOOL -#define KMX_WORD_unaligned KMX_WORD -#endif - #ifdef KM_CORE_LIBRARY // TODO: move this to a common namespace keyman::common::kmx_file or similar in the future namespace km { diff --git a/common/include/test_assert.h b/common/include/test_assert.h index 8cc0f3b624d..75c586bcc67 100644 --- a/common/include/test_assert.h +++ b/common/include/test_assert.h @@ -22,7 +22,7 @@ << console_color::reset() \ << std::endl \ << " " << (exprText) << std::endl; \ - std::exit(100*__LINE__+(result)); \ + std::exit(EXIT_FAILURE); \ } #ifdef try_status @@ -55,7 +55,7 @@ << std::endl \ << "expected: " << (expected) << std::endl \ << "actual: " << (actual) << std::endl; \ - std::exit(100*__LINE__); \ + std::exit(EXIT_FAILURE); \ } \ } @@ -70,6 +70,6 @@ << std::endl \ << "expected: " << Debug_UnicodeString((PKMX_WCHAR)(expected)) << std::endl \ << "actual: " << Debug_UnicodeString((PKMX_WCHAR)(actual)) << std::endl; \ - std::exit(100*__LINE__); \ + std::exit(EXIT_FAILURE); \ } \ } diff --git a/developer/src/tike/xml/layoutbuilder/keymanweb-osk.ttf b/common/resources/fonts/keymanweb-osk.ttf similarity index 100% rename from developer/src/tike/xml/layoutbuilder/keymanweb-osk.ttf rename to common/resources/fonts/keymanweb-osk.ttf diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index 2a2dc0bb70b..14e819a56dd 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -570,6 +570,9 @@ export default class KeyboardProcessor extends EventEmitter { resetContext(target?: OutputTarget) { this.layerId = 'default'; + + // Make sure all deadkeys for the context get cleared properly. + target?.resetContext(); this.keyboardInterface.resetContextCache(); // May be null if it's a keyboard swap. diff --git a/common/web/types/src/kpj/keyman-developer-project.ts b/common/web/types/src/kpj/keyman-developer-project.ts index 27ea6ddf16e..a53cbca20e9 100644 --- a/common/web/types/src/kpj/keyman-developer-project.ts +++ b/common/web/types/src/kpj/keyman-developer-project.ts @@ -37,7 +37,7 @@ export class KeymanDeveloperProject { for(let filename of files) { let fullPath = this.callbacks.path.join(sourcePath, filename); if(KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Source.LdmlKeyboard)) { - if(!this.callbacks.fs.readFileSync(fullPath, 'utf-8').match(/ldmlKeyboard\.dtd/)) { + if(!this.callbacks.fs.readFileSync(fullPath, 'utf-8').match(/ldmlKeyboard3\.dtd/)) { // Skip this .xml because we assume it isn't really a keyboard .xml continue; } diff --git a/common/web/types/src/kpj/kpj-file-reader.ts b/common/web/types/src/kpj/kpj-file-reader.ts index 72b6da3b0f5..29ffe8009d9 100644 --- a/common/web/types/src/kpj/kpj-file-reader.ts +++ b/common/web/types/src/kpj/kpj-file-reader.ts @@ -28,10 +28,12 @@ export class KPJFileReader { data = r as KPJFile; }); data = this.boxArrays(data); - for(let file of data.KeymanDeveloperProject?.Files?.File) { - // xml2js imports
as '' so we will just delete the empty string - if(typeof file.Details == 'string') { - delete file.Details; + if(data.KeymanDeveloperProject?.Files?.File?.length) { + for(let file of data.KeymanDeveloperProject?.Files?.File) { + // xml2js imports
as '' so we will just delete the empty string + if(typeof file.Details == 'string') { + delete file.Details; + } } } return data as KPJFile; diff --git a/common/web/types/test/fixtures/invalid-conforms-to.xml b/common/web/types/test/fixtures/invalid-conforms-to.xml index 01e463d817e..de19c3a3bf0 100644 --- a/common/web/types/test/fixtures/invalid-conforms-to.xml +++ b/common/web/types/test/fixtures/invalid-conforms-to.xml @@ -1,7 +1,7 @@ diff --git a/common/windows/delphi/general/utilfiletypes.pas b/common/windows/delphi/general/utilfiletypes.pas index 56a718bcd06..539a3557b1b 100644 --- a/common/windows/delphi/general/utilfiletypes.pas +++ b/common/windows/delphi/general/utilfiletypes.pas @@ -94,7 +94,7 @@ TKeymanFileTypeInfo = class end; type - TKeymanProjectType = (kptUnknown, kptBasic, kptBlank, kptImportWindowsKeyboard, kptBlankLexicalModel, kptWordlistLexicalModel); + TKeymanProjectType = (kptUnknown, kptBasic, kptImportWindowsKeyboard, kptWordlistLexicalModel); implementation diff --git a/core/build.sh b/core/build.sh index f4e2140c2d6..08ba60bcb79 100755 --- a/core/build.sh +++ b/core/build.sh @@ -165,7 +165,7 @@ fi # ------------------------------------------------------------------------------- -testparams="${builder_extra_params[@]}" +testparams="${builder_extra_params[@]} --print-errorlogs" if builder_has_option --test; then testparams="$opt_tests $testparams" fi diff --git a/core/src/debuglog.cpp b/core/src/debuglog.cpp index a4f8847892c..408de2126cc 100644 --- a/core/src/debuglog.cpp +++ b/core/src/debuglog.cpp @@ -451,6 +451,25 @@ const char *Debug_UnicodeString(::std::u16string s, int x) { return bufout[x]; } +const char *Debug_UnicodeString(::std::u32string s, int x) { + if (!ShouldDebug()) { + return ""; + } +#ifdef _MSC_VER + __declspec(thread) +#endif + static char bufout[2][MEDIUM_BUF_SIZ]; + auto p = s.begin(); + char *q; + bufout[x][0] = 0; + for (q = bufout[x]; (intptr_t)(q-bufout[x]) < (128*7) && p != s.end(); p++) + { + snprintf(q, MEDIUM_BUF_SIZ - (q - bufout[x]), "U+%4.6X ", (unsigned int)*p); + q = strchr(q, 0); + } + return bufout[x]; +} + void write_console(KMX_BOOL error, const wchar_t *fmt, ...) { if (!g_silent || error) { va_list vars; diff --git a/core/src/debuglog.h b/core/src/debuglog.h index 67e6ce601f9..5e3f6278a8e 100644 --- a/core/src/debuglog.h +++ b/core/src/debuglog.h @@ -44,6 +44,7 @@ const char *Debug_UnicodeString(PKMX_WCHAR s, int x = 0); * @return pointer to temporary buffer */ const char *Debug_UnicodeString(std::u16string s, int x = 0); +const char *Debug_UnicodeString(std::u32string s, int x = 0); const char *Debug_ModifierName(KMX_UINT modifiers); inline KMX_BOOL ShouldDebug() { diff --git a/core/src/kmx/kmx_actions.cpp b/core/src/kmx/kmx_actions.cpp index 4e3f613357f..5ea5f4e8a14 100644 --- a/core/src/kmx/kmx_actions.cpp +++ b/core/src/kmx/kmx_actions.cpp @@ -35,9 +35,6 @@ KMX_BOOL KMX_Actions::QueueAction(int ItemType, KMX_DWORD dwData) switch(ItemType) { - case QIT_VKEYDOWN: - break; - case QIT_DEADKEY: m_context->Add(UC_SENTINEL); m_context->Add(CODE_DEADKEY); diff --git a/core/src/kmx/kmx_actions.h b/core/src/kmx/kmx_actions.h index b536a3018a0..c2ec61f43e8 100644 --- a/core/src/kmx/kmx_actions.h +++ b/core/src/kmx/kmx_actions.h @@ -20,10 +20,10 @@ typedef struct } KMX_Action; // QueueAction ItemTypes -#define QIT_VKEYDOWN 0 -#define QIT_VKEYUP 1 -#define QIT_VSHIFTDOWN 2 -#define QIT_VSHIFTUP 3 +// QIT_VKEYDOWN 0 Not supported by Core, legacy Windows-only, see #10049 +// QIT_VKEYUP 1 Not supported by Core, legacy Windows-only, see #10049 +// QIT_VSHIFTDOWN 2 Not supported by Core, legacy Windows-only, see #10049 +// QIT_VSHIFTUP 3 Not supported by Core, legacy Windows-only, see #10049 #define QIT_CHAR 4 #define QIT_DEADKEY 5 #define QIT_BELL 6 @@ -35,10 +35,6 @@ typedef struct #define QIT_MAX 11 -#define QVK_EXTENDED 0x00010000 // Flag for QIT_VKEYDOWN to indicate an extended key -#define QVK_KEYMASK 0x0000FFFF -#define QVK_FLAGMASK 0xFFFF0000 - class KMX_Actions { private: diff --git a/core/src/kmx/kmx_processevent.cpp b/core/src/kmx/kmx_processevent.cpp index 8e07e963c0f..4b98312c369 100644 --- a/core/src/kmx/kmx_processevent.cpp +++ b/core/src/kmx/kmx_processevent.cpp @@ -434,7 +434,7 @@ int KMX_ProcessEvent::PostString(PKMX_WCHAR str, LPKEYBOARD lpkb, PKMX_WCHAR end PKMX_WCHAR p, q, temp; LPSTORE s; int n1, n2; - int i, n, shift; + int i, n; KMX_BOOL FoundUse = FALSE; // TODO: Refactor to use incxstr for(p = str; *p && (p < endstr || !endstr); p++) @@ -443,21 +443,10 @@ int KMX_ProcessEvent::PostString(PKMX_WCHAR str, LPKEYBOARD lpkb, PKMX_WCHAR end switch(*(++p)) { case CODE_EXTENDED: // Start of a virtual key section w/shift codes - p++; - - shift = *p; //(*p<<8) | *(p+1); - m_actions.QueueAction(QIT_VSHIFTDOWN, shift); - - p++; - - m_actions.QueueAction(QIT_VKEYDOWN, *p); - m_actions.QueueAction(QIT_VKEYUP, *p); - - m_actions.QueueAction(QIT_VSHIFTUP, shift); - + // virtual keys in output are not supported + p++; // modifier + p++; // vkey p++; // CODE_EXTENDEDEND - ////// CODE_EXTENDEDEND will be incremented by loop - break; case CODE_DEADKEY: // A deadkey to be output diff --git a/core/src/kmx/kmx_processor.cpp b/core/src/kmx/kmx_processor.cpp index bb18db9edda..c72d2fe8577 100644 --- a/core/src/kmx/kmx_processor.cpp +++ b/core/src/kmx/kmx_processor.cpp @@ -205,12 +205,6 @@ kmx_processor::internal_process_queued_actions(km_core_state *state) { case QIT_EMIT_KEYSTROKE: state->actions().push_emit_keystroke(); break; - case QIT_VKEYDOWN: - case QIT_VKEYUP: - case QIT_VSHIFTDOWN: - case QIT_VSHIFTUP: - // TODO: eliminate?? - break; case QIT_CHAR: state->context().push_character(a.dwData); state->actions().push_character(a.dwData); diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index f4bb2648f1e..0c36e5c0aa0 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -194,60 +194,14 @@ ldml_processor::process_event( state->actions().clear(); switch (vk) { + // Currently, only one VK gets spoecial treatment. // Special handling for backspace VK case KM_CORE_VKEY_BKSP: - { - if (!!bksp_transforms) { - // TODO-LDML: process bksp - // std::u16string outputString; - // // don't bother if no backspace transforms! - // // TODO-LDML: unroll ctxt into a str - // std::u16string ctxtstr; - // for (size_t i = 0; i < ctxt.size(); i++) { - // ctxtstr.append(ctxt[i]); - // } - // const size_t matchedContext = transforms->apply(ctxtstr, outputString); - } - KMX_DWORD last_char = 0UL; - // attempt to get the last char - auto end = state->context().rbegin(); - if(end != state->context().rend()) { - if((*end).type == KM_CORE_CT_CHAR) { - last_char = (*end).character; - // TODO-LDML: markers! - } - } - if (last_char == 0UL) { - /* - We couldn't find a character at end of context (context is empty), - so we'll pass the backspace keystroke on to the app to process; the - app might want to use backspace to move between contexts or delete - a text box, etc. Or it might be a legacy app and we've had our caret - dumped in somewhere unknown, so we will have to depend on the app to - be sensible about backspacing because we know nothing. - */ - state->actions().push_backspace(KM_CORE_BT_UNKNOWN); - } else { - state->actions().push_backspace(KM_CORE_BT_CHAR, last_char); - state->context().pop_back(); - } - } + process_backspace(state); break; default: // all other VKs - { - // Look up the key - const std::u16string key_str = keys.lookup(vk, modifier_state); - - if (key_str.empty()) { - // no key was found, so pass the keystroke on to the Engine - state->actions().push_invalidate_context(); - state->actions().push_emit_keystroke(); - break; // ----- commit and exit - } - - process_key_string(state, key_str); - } // end of processing a 'normal' vk + process_key(state, vk, modifier_state); } // end of switch // end of normal processing: commit and exit state->actions().commit(); @@ -259,6 +213,56 @@ ldml_processor::process_event( return KM_CORE_STATUS_OK; } +void +ldml_processor::process_backspace(km_core_state *state) const { + if (!!bksp_transforms) { + // process with an empty string + auto matchedContext = process_output(state, std::u32string(), bksp_transforms.get()); + + if (matchedContext > 0) { + return; // The transform took care of the backspacing. + } // else, fall through to default processing below. + } + + // Find out what the last actual character was and remove it. + // attempt to get the last char + // TODO-LDML: emoji backspace + auto end = state->context().rbegin(); + if (end != state->context().rend()) { + if ((*end).type == KM_CORE_CT_CHAR) { + state->actions().push_backspace(KM_CORE_BT_CHAR, (*end).character); + state->context().pop_back(); + return; + } else if ((*end).type == KM_CORE_BT_MARKER) { + state->actions().push_backspace(KM_CORE_BT_MARKER, (*end).marker); + state->context().pop_back(); + } + } + /* + We couldn't find a character at end of context (context is empty), + so we'll pass the backspace keystroke on to the app to process; the + app might want to use backspace to move between contexts or delete + a text box, etc. Or it might be a legacy app and we've had our caret + dumped in somewhere unknown, so we will have to depend on the app to + be sensible about backspacing because we know nothing. + */ + state->actions().push_backspace(KM_CORE_BT_UNKNOWN); +} + +void +ldml_processor::process_key(km_core_state *state, km_core_virtual_key vk, uint16_t modifier_state) const { + // Look up the key + const std::u16string key_str = keys.lookup(vk, modifier_state); + + if (key_str.empty()) { + // no key was found, so pass the keystroke on to the Engine + state->actions().push_invalidate_context(); + state->actions().push_emit_keystroke(); + } else { + process_key_string(state, key_str); + } +} + void ldml_processor::process_key_string(km_core_state *state, const std::u16string &key_str) const { // We know that key_str is not empty per the caller. @@ -267,19 +271,23 @@ ldml_processor::process_key_string(km_core_state *state, const std::u16string &k // we convert the keys str to UTF-32 here instead of using the emit_text() overload // so that we don't have to reconvert it inside the transform code. std::u32string key_str32 = kmx::u16string_to_u32string(key_str); - assert(ldml::normalize_nfd(key_str32)); // TODO-LDML: else fail? + (void)process_output(state, key_str32, transforms.get()); +} - // extract context string, in NFC - std::u32string old_ctxtstr_nfc; - (void)context_to_string(state, old_ctxtstr_nfc, false); - assert(ldml::normalize_nfc(old_ctxtstr_nfc)); // TODO-LDML: else fail? +size_t ldml_processor::process_output(km_core_state *state, const std::u32string &str, ldml::transforms *with_transforms) const { + std::u32string nfd_str = str; + assert(ldml::normalize_nfd_markers(nfd_str)); // TODO-LDML: else fail? + // extract context string, in NFD + std::u32string old_ctxtstr_nfd; + (void)context_to_string(state, old_ctxtstr_nfd, false); + assert(ldml::normalize_nfd_markers(old_ctxtstr_nfd)); // TODO-LDML: else fail? // context string in NFD std::u32string ctxtstr; (void)context_to_string(state, ctxtstr, true); // with markers // add the newly added key output to ctxtstr - ctxtstr.append(key_str32); - assert(ldml::normalize_nfd(ctxtstr)); // TODO-LDML: else fail? + ctxtstr.append(nfd_str); + assert(ldml::normalize_nfd_markers(ctxtstr)); // TODO-LDML: else fail? /** transform output string */ std::u32string outputString; @@ -288,34 +296,55 @@ ldml_processor::process_key_string(km_core_state *state, const std::u16string &k // begin modifications to the string - if(transforms) { - matchedContext = transforms->apply(ctxtstr, outputString); + if(with_transforms != nullptr) { + matchedContext = with_transforms->apply(ctxtstr, outputString); } else { // no transforms, no output } + // Short Circuit: if no transforms matched, and no new text is being output, + // just return. + if (matchedContext == 0 && str.empty()) { + return matchedContext; + } + // drop last 'matchedContext': ctxtstr.resize(ctxtstr.length() - matchedContext); ctxtstr.append(outputString); // TODO-LDML: should be able to do a normalization-safe append here. - assert(ldml::normalize_nfd(ctxtstr)); // TODO-LDML: else fail? + ldml::marker_map markers; + assert(ldml::normalize_nfd_markers(ctxtstr, markers)); // TODO-LDML: Need marker-safe normalize here. // Ok. We've done all the happy manipulations. - /** NFC and no markers */ - std::u32string ctxtstr_cleanedup = ctxtstr; - // TODO-LDML: remove markers! - assert(ldml::normalize_nfc(ctxtstr_cleanedup)); // TODO-LDML: else fail? - - // find common prefix - auto ctxt_prefix = mismatch(old_ctxtstr_nfc.begin(), old_ctxtstr_nfc.end(), ctxtstr_cleanedup.begin(), ctxtstr_cleanedup.end()); - /** the part of the old str that changed */ - std::u32string old_ctxtstr_changed(ctxt_prefix.first,old_ctxtstr_nfc.end()); + /** NFD and no markers */ + std::u32string ctxtstr_cleanedup = ldml::remove_markers(ctxtstr); + assert(ldml::normalize_nfd_markers(ctxtstr_cleanedup)); + + // find common prefix. + // For example, if the context previously had "aaBBBBB" and it is changing to "aaCCC" then we will have: + // - old_ctxtstr_changed = "BBBBB" + // - new_ctxtstr_changed = "CCC" + // So the BBBBB needs to be removed and then CCC added. + auto ctxt_prefix = mismatch(old_ctxtstr_nfd.begin(), old_ctxtstr_nfd.end(), ctxtstr_cleanedup.begin(), ctxtstr_cleanedup.end()); + /** The part of the old string to be removed */ + std::u32string old_ctxtstr_changed(ctxt_prefix.first,old_ctxtstr_nfd.end()); + /** The new context to be added */ std::u32string new_ctxtstr_changed(ctxt_prefix.second,ctxtstr_cleanedup.end()); // drop the old suffix. Note: this mutates old_ctxtstr_changed. remove_text(state, old_ctxtstr_changed, old_ctxtstr_changed.length()); assert(old_ctxtstr_changed.length() == 0); + // old_ctxtstr_changed is now empty because it's been removed. + // context is "aa" in the above example. emit_text(state, new_ctxtstr_changed); + + // TODO-LDML: need to emit marker here - need to emit text w/ markers, and handle appropriately. + // // TODO-LDML: 1-marker hack! need to support a string with intermixed markers. + if (str.length() == 3 && str[0] == LDML_UC_SENTINEL && str[1] == LDML_MARKER_CODE) { + emit_marker(state, str[2]); + } + + return matchedContext; } void @@ -334,18 +363,7 @@ ldml_processor::remove_text(km_core_state *state, std::u32string &str, size_t le str.pop_back(); state->actions().push_backspace(KM_CORE_BT_CHAR, c->character); // Cause prior char to be removed } else if (type == KM_CORE_BT_MARKER) { - // it's a marker, 'worth' 3 uchars - assert(length >= 3); - assert(lastCtx == c->marker); // end of list - length -= 3; - // pop off the three-part sentinel string (in reverse order of course) - assert(str.back() == c->marker); // marker # - str.pop_back(); - assert(str.back() == LDML_MARKER_CODE); - str.pop_back(); - assert(str.back() == LDML_UC_SENTINEL); - str.pop_back(); - // push a special backspace to delete the marker + // just pop off any markers. state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker); } } @@ -433,7 +451,7 @@ ldml_processor::context_to_string(km_core_state *state, std::u32string &str, boo } else if (last_type == KM_CORE_BT_MARKER) { assert(km::core::kmx::is_valid_marker(c->marker)); if (include_markers) { - prepend_marker(str, c->marker); + ldml::prepend_marker(str, c->marker); } } else { break; diff --git a/core/src/ldml/ldml_processor.hpp b/core/src/ldml/ldml_processor.hpp index 79c6ae4ac4b..40cd89a9db7 100644 --- a/core/src/ldml/ldml_processor.hpp +++ b/core/src/ldml/ldml_processor.hpp @@ -100,9 +100,23 @@ namespace core { */ static void remove_text(km_core_state *state, std::u32string &str, size_t length); + /** process a key */ + void process_key(km_core_state *state, km_core_virtual_key vk, uint16_t modifier_state) const; + /** process a typed key */ void process_key_string(km_core_state *state, const std::u16string &key_str) const; + /** process a backspace */ + void process_backspace(km_core_state *state) const; + + /** + * common function for outputting a string with transforms/normalization applied. + * @param str string to output (such as from a key), or empty + * @param with_transforms transforms to use or nullptr + * @returns length of matched input context + */ + size_t process_output(km_core_state *state, const std::u32string &str, ldml::transforms *with_transforms) const; + /** * add the string+marker portion of the context to the beginning of str. * Stop when a non-string and non-marker is hit. @@ -111,15 +125,6 @@ namespace core { */ static size_t context_to_string(km_core_state *state, std::u32string &str, bool include_markers = true); - /** prepend the marker string in UC_SENTINEL format to the str */ - inline static void prepend_marker(std::u32string &str, KMX_DWORD marker); }; - - void - ldml_processor::prepend_marker(std::u32string &str, KMX_DWORD marker) { - km_core_usv triple[] = {LDML_UC_SENTINEL, LDML_MARKER_CODE, marker}; - str.insert(0, triple, 3); - } - } // namespace core } // namespace km diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 6ae8feafc87..973117f36b9 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -10,10 +10,7 @@ #include #include #include "kmx/kmx_xstring.h" - -#ifndef assert -#define assert(x) ((void)0) -#endif +#include namespace km { namespace core { @@ -48,16 +45,16 @@ element::is_uset() const { return (flags & LDML_ELEM_FLAGS_TYPE) == LDML_ELEM_FLAGS_TYPE_USET; } -signed char +reorder_weight element::get_order() const { unsigned char uorder = ((flags & LDML_ELEM_FLAGS_ORDER_MASK) >> LDML_ELEM_FLAGS_ORDER_BITSHIFT); - return (signed char)uorder; + return (reorder_weight)uorder; // unsigned to signed } -signed char +reorder_weight element::get_tertiary() const { unsigned char uorder = ((flags & LDML_ELEM_FLAGS_TERTIARY_MASK) >> LDML_ELEM_FLAGS_TERTIARY_BITSHIFT); - return (signed char)uorder; + return (reorder_weight)uorder; // unsigned to signed } bool @@ -96,19 +93,19 @@ element::dump() const { int reorder_sort_key::compare(const reorder_sort_key &other) const { - int primaryResult = (int)primary - (int)other.primary; - int secondaryResult = (int)secondary - (int)other.secondary; - int tertiaryResult = (int)tertiary - (int)other.tertiary; - int quaternaryResult = (int)quaternary - (int)other.quaternary; + auto primaryResult = primary - other.primary; + auto secondaryResult = secondary - other.secondary; + auto tertiaryResult = tertiary - other.tertiary; + auto quaternaryResult = quaternary - other.quaternary; if (primaryResult) { - return primaryResult; + return (int)primaryResult; } else if (secondaryResult) { - return secondaryResult; + return (int)secondaryResult; } else if (tertiaryResult) { - return tertiaryResult; + return (int)tertiaryResult; } else if (quaternaryResult) { - return quaternaryResult; + return (int)quaternaryResult; } else { // We don't expect to get here. quaternaryResult is the string index, which // should be unequal. @@ -134,14 +131,14 @@ reorder_sort_key::from(const std::u32string &str) { // construct a 'baseline' sort key, that is, in the absence of // any match rules. std::deque keylist; - auto s = str.begin(); // str iterator - size_t c = 0; // str index + auto s = str.begin(); // str iterator + reorder_weight c = 0; // str index for (auto e = str.begin(); e < str.end(); e++, s++, c++) { // primary weight: 0 // seconary weight: c (the string index) // tertiary weight: 0 // quaternary weight: c (the index again) - keylist.emplace_back(reorder_sort_key{*s, 0, c, 0, c}); + keylist.emplace_back(reorder_sort_key{*s, 0, c, 0, c, false}); } return keylist; } @@ -149,7 +146,9 @@ reorder_sort_key::from(const std::u32string &str) { void reorder_sort_key::dump() const { // for debugging… - DebugLog("- U+%04X\t(%d, %d, %d, %d)", ch, (int)primary, (int)secondary, (int)tertiary, (int)quaternary); + DebugLog( + "- U+%04X\t(%d, %d, %d, %d) %c", ch, (int)primary, (int)secondary, (int)tertiary, (int)quaternary, + is_tertiary_base ? 'T' : ' '); } size_t @@ -216,9 +215,14 @@ std::deque & element_list::update_sort_key(size_t offset, std::deque &key) const { /** string index */ size_t c = 0; + bool have_last_base = false; + reorder_weight last_base_primary = -1; + reorder_weight last_base_secondary = -1; for (auto e = begin(); e < end(); e++, c++) { + /** position in the key */ + auto n = offset + c; /** update this key */ - auto &k = key.at(offset + c); + auto &k = key.at(n); // we double check that the character matches. otherwise something // has really gone awry, because we shouldn't be here if this element list doesn't apply. if (!e->matches(k.ch)) { @@ -228,9 +232,30 @@ element_list::update_sort_key(size_t offset, std::deque &key) assert(e->matches(k.ch)); // double check that this element matches } // we only update primary and tertiary weights - k.primary = e->get_order(); - // TODO-LDML: need more detailed tertiary work - k.tertiary = e->get_tertiary(); + k.primary = e->get_order(); + k.tertiary = e->get_tertiary(); + k.is_tertiary_base = e->is_tertiary_base(); + + if (k.tertiary != 0 && n > 0) { + // search backwards for a base + auto n2 = n; + // TODO-LDML: odd loop here because n2 is signed. + do { + n2--; + auto &k2 = key.at(n2); + if (k2.is_tertiary_base) { + last_base_primary = k2.primary; + last_base_secondary = k2.secondary; + have_last_base = true; + } + } while (!have_last_base && n2 > 0); + // we may not have found the base. but the common case is that the base is found. + if (have_last_base) { + // copy the primary and secondary from the last_base + k.primary = last_base_primary; + k.secondary = last_base_secondary; + } + } #if KMXPLUS_DEBUG_TRANSFORM DebugTran("Updating at +%d", c); k.dump(); @@ -274,6 +299,29 @@ reorder_entry::match_end(std::u32string &str, size_t offset, size_t len) const { return match_len; } +int +reorder_entry::compare(const reorder_entry &other) const { + if (this == &other) { + return 0; + } else if (elements.size() < other.elements.size()) { + return -1; + } else if (elements.size() > other.elements.size()) { + return 1; + } else { + return 0; // punt + } +} + +bool +reorder_entry::operator<(const reorder_entry &other) const { + return (compare(other) < 0); +} + +bool +reorder_entry::operator>(const reorder_entry &other) const { + return (compare(other) > 0); +} + bool reorder_group::apply(std::u32string &str) const { /** did we apply anything */ @@ -352,7 +400,9 @@ reorder_group::apply(std::u32string &str) const { /** pointer to the beginning of the current run. */ std::deque::iterator run_start = sort_keys.begin(); for(auto e = run_start; e != sort_keys.end(); e++) { - if ((e->primary == 0) && (e != run_start)) { // it's a base + // find the actual beginning base: primary weight = 0 and tertiary = 0. + // (tertiary chars will have primary=0 BUT will have tertiary nonzero.) + if ((e->primary == 0) && (e->tertiary == 0) && (e != run_start)) { auto run_end = e - 1; DebugTran("Sorting subrange quaternary=[%d..]", run_start->quaternary); std::sort(run_start, run_end); // reversed because it's a reverse iterator…? @@ -368,7 +418,7 @@ reorder_group::apply(std::u32string &str) const { // recombine into a string by pulling out the 'ch' value // that's in each sortkey element. std::u32string newSuffix; - size_t q = sort_keys.begin()->quaternary; // start with the first quaternary + signed char q = sort_keys.begin()->quaternary; // start with the first quaternary for (auto e = sort_keys.begin(); e < sort_keys.end(); e++, q++) { if (q != e->quaternary) { // something rearranged in this subrange, because the quaternary values are out of order. @@ -466,15 +516,13 @@ transform_entry::init() { return false; } // TODO-LDML: if we have mapFrom, may need to do other processing. - const std::u16string patstr = km::core::kmx::u32string_to_u16string(fFrom); + std::u16string patstr = km::core::kmx::u32string_to_u16string(fFrom); + // normalize, including markers + normalize_nfd_markers(patstr); UErrorCode status = U_ZERO_ERROR; - /* const */ icu::UnicodeString patustr_raw = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); + /* const */ icu::UnicodeString patustr = icu::UnicodeString(patstr.data(), (int32_t)patstr.length()); // add '$' to match to end - patustr_raw.append(u'$'); - icu::UnicodeString patustr; - const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); - // NFD normalize on pattern creation - nfd->normalize(patustr_raw, patustr, status); + patustr.append(u'$'); // TODO-LDML: may need to escape some markers. Marker #91 will look like a `[` to the pattern fFromPattern.reset(icu::RegexPattern::compile(patustr, 0, status)); return (UASSERT_SUCCESS(status)); } @@ -556,7 +604,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons } const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); icu::UnicodeString rustr2; - nfd->normalize(rustr, rustr2, status); + nfd->normalize(rustr, rustr2, status); // TODO-LDML: must be normalize with markers! UASSERT_SUCCESS(status); // here we replace the match output. icu::UnicodeString entireOutput = matcher->replaceFirst(rustr2, status); @@ -567,7 +615,7 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons // normalize the replaced string icu::UnicodeString outu; - nfd->normalize(outu_raw, outu, status); + nfd->normalize(outu_raw, outu, status); // TODO-LDML: must be normalize with markers! UASSERT_SUCCESS(status); // Special case if there's no output, save some allocs @@ -585,8 +633,6 @@ transform_entry::apply(const std::u32string &input, std::u32string &output) cons outu.toUTF32((UChar32 *)s, out32len + 1, status); UASSERT_SUCCESS(status); output.assign(s, out32len); - // now, build a u32string - std::u32string out32(s, out32len); // clean up buffer delete [] s; } @@ -845,6 +891,7 @@ transforms::load( return nullptr; } } + std::sort(newGroup.list.begin(), newGroup.list.end()); // sort list by size, so that longer matches match last transforms->addGroup(newGroup); } else { // internal error - some other type - should have been caught by validation @@ -863,16 +910,6 @@ transforms::load( // string manipulation -bool normalize_nfd(std::u32string &str) { - std::u16string rstr = km::core::kmx::u32string_to_u16string(str); - if(!normalize_nfd(rstr)) { - return false; - } else { - str = km::core::kmx::u16string_to_u32string(rstr); - return true; - } -} - /** internal function to normalize with a specified mode */ static bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode &status) { UASSERT_SUCCESS(status); @@ -886,6 +923,16 @@ static bool normalize(const icu::Normalizer2 *n, std::u16string &str, UErrorCode return U_SUCCESS(status); } +bool normalize_nfd(std::u32string &str) { + std::u16string rstr = km::core::kmx::u32string_to_u16string(str); + if(!normalize_nfd(rstr)) { + return false; + } else { + str = km::core::kmx::u16string_to_u32string(rstr); + return true; + } +} + bool normalize_nfd(std::u16string &str) { UErrorCode status = U_ZERO_ERROR; const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status); @@ -893,6 +940,87 @@ bool normalize_nfd(std::u16string &str) { return normalize(nfd, str, status); } +bool normalize_nfd_markers(std::u16string &str, marker_map &map) { + std::u32string rstr = km::core::kmx::u16string_to_u32string(str); + if(!normalize_nfd_markers(rstr, map)) { + return false; + } else { + str = km::core::kmx::u32string_to_u16string(rstr); + return true; + } +} + +void add_back_markers(std::u32string &str, const std::u32string &src, const marker_map &map) { + // need to reconstitute. + marker_map map2(map); // make a copy of the map + // clear the string + str.clear(); + // add the end-of-text marker + { + const auto ch = MARKER_BEFORE_EOT; + const auto m = map2.find(ch); + if (m != map2.end()) { + prepend_marker(str, m->second); + map2.erase(ch); // remove it + } + } + // go from end to beginning of string + for (auto p = src.rbegin(); p != src.rend(); p++) { + const auto ch = *p; + str.insert(0, 1, ch); // prepend + + const auto m = map2.find(ch); + if (m != map2.end()) { + prepend_marker(str, m->second); + map2.erase(ch); // remove it + } + } +} + +/** + * TODO-LDML: + * - doesn't support >1 marker per char - may need a set instead of a map! + * - ideally this should be used on a normalization safe subsequence + */ +bool normalize_nfd_markers(std::u32string &str, marker_map &map) { + /** original string, but no markers */ + std::u32string str_unmarked = remove_markers(str, map); + /** original string, no markers, NFD */ + std::u32string str_unmarked_nfd = str_unmarked; + if(!normalize_nfd(str_unmarked_nfd)) { + return false; // normalize failed. + } else if (map.size() == 0) { + // no markers. Return the normalized unmarked str + str = str_unmarked_nfd; + } else if (str_unmarked_nfd == str_unmarked) { + // Normalization produced no change when markers were removed. + // So, we'll call this a no-op. + } else { + add_back_markers(str, str_unmarked_nfd, map); + } + return true; // all OK +} + +bool normalize_nfc_markers(std::u32string &str, marker_map &map) { + /** original string, but no markers */ + std::u32string str_unmarked = remove_markers(str, map); + /** original string, no markers, NFC */ + std::u32string str_unmarked_nfc = str_unmarked; + if(!normalize_nfc(str_unmarked_nfc)) { + return false; // normalize failed. + } else if (map.size() == 0) { + // no markers. Return the normalized unmarked str + str = str_unmarked_nfc; + } else if (str_unmarked_nfc == str_unmarked) { + // Normalization produced no change when markers were removed. + // So, we'll call this a no-op. + } else { + add_back_markers(str, str_unmarked_nfc, map); + } + return true; // all OK +} + + bool normalize_nfc(std::u32string &str) { std::u16string rstr = km::core::kmx::u32string_to_u16string(str); if(!normalize_nfc(rstr)) { @@ -910,6 +1038,52 @@ bool normalize_nfc(std::u16string &str) { return normalize(nfc, str, status); } +std::u32string remove_markers(const std::u32string &str, marker_map *markers) { + std::u32string out; + auto i = str.begin(); + auto last = i; + for (i = find(i, str.end(), LDML_UC_SENTINEL); i != str.end(); i = find(i, str.end(), LDML_UC_SENTINEL)) { + // append any prefix (from prior pos'n to here) + out.append(last, i); + + // #1: LDML_UC_SENTINEL (what we searched for) + assert(*i == LDML_UC_SENTINEL); // assert that find() worked + i++; + last = i; + if (i == str.end()) { + break; // hit end + } + + // #2 LDML_MARKER_CODE + if (*i != LDML_MARKER_CODE) { + continue; // can't process this, get out + } + i++; + last = i; + if (i == str.end()) { + break; // hit end + } + + // #3 marker number + const KMX_DWORD marker_no = *i; + assert(marker_no >= LDML_MARKER_MIN_INDEX && marker_no <= LDML_MARKER_MAX_INDEX); + i++; // if end, we'll break out of the loop + last = i; + + // record the marker + if (markers != nullptr) { + if (i == str.end()) { + markers->emplace(MARKER_BEFORE_EOT, marker_no); + } else { + markers->emplace(*i, marker_no); + } + } + } + // get the suffix between the last marker and the end + out.append(last, str.end()); + return out; +} + } // namespace ldml } // namespace core } // namespace km diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index 044520ade5e..dd082895744 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -8,6 +8,7 @@ #pragma once #include "kmx/kmx_plus.h" +#include "kmx/kmx_xstring.h" #include #include #include @@ -46,6 +47,11 @@ inline bool uassert_success(const char *file, int line, const char *function, UE using km::core::kmx::SimpleUSet; +/** a reorder weight, such as primary, secondary, etc. */ +typedef signed char reorder_weight; + + + /** * Type of a group */ @@ -70,9 +76,9 @@ class element { /** @returns true if tertiary base bit set */ bool is_tertiary_base() const; /** @returns the primary order */ - signed char get_order() const; + reorder_weight get_order() const; /** @returns the tertiary order */ - signed char get_tertiary() const; + reorder_weight get_tertiary() const; /** @returns raw elem flags */ KMX_DWORD get_flags() const; /** @returns true if matches this character*/ @@ -155,11 +161,12 @@ class transform_group : public std::deque { /** a single char, categorized according to reorder rules*/ struct reorder_sort_key { - km_core_usv ch; // the single char value - signed char primary; // primary order value - size_t secondary; // index position - signed char tertiary; // tertiary value, defaults to 0 - size_t quaternary; // index again + km_core_usv ch; // the single char value + reorder_weight primary; // primary order value + reorder_weight secondary; // index position + reorder_weight tertiary; // tertiary value, defaults to 0 + reorder_weight quaternary; // index again + bool is_tertiary_base; // remember that this key was a tertiary base /** @returns -1, 0, 1 depending on ordering */ int compare(const reorder_sort_key &other) const; @@ -212,6 +219,11 @@ class reorder_entry { */ size_t match_end(std::u32string &str, size_t offset, size_t len) const; + /** @returns -1, 0, 1 depending on ordering */ + int compare(const reorder_entry &other) const; + bool operator<(const reorder_entry &other) const; + bool operator>(const reorder_entry &other) const; + public: element_list elements; element_list before; @@ -287,14 +299,73 @@ class transforms { // string routines +/** indicates that the marker was before the end of text. */ +const char32_t MARKER_BEFORE_EOT = km::core::kmx::Uni_FFFE_NONCHARACTER; + +/** map from following-char to marker number. */ +typedef std::map marker_map; + /** Normalize a u32string inplace to NFD. @return false on failure */ bool normalize_nfd(std::u32string &str); /** Normalize a u16string inplace to NFD. @return false on failure */ bool normalize_nfd(std::u16string &str); +/** Normalize a u32string inplace to NFD, retaining markers. + * @param markers will be populated with marker chars + * @return false on failure + **/ +bool normalize_nfd_markers(std::u32string &str, marker_map &markers); +bool normalize_nfd_markers(std::u16string &str, marker_map &markers); +inline bool normalize_nfd_markers(std::u32string &str); +inline bool normalize_nfd_markers(std::u16string &str); + +/** Normalize a u32string inplace to NFC, retaining markers. + * @param markers will be populated with marker chars + * @return false on failure + **/ +bool normalize_nfc_markers(std::u32string &str, marker_map &markers); +bool normalize_nfc_markers(std::u16string &str, marker_map &markers); +inline bool normalize_nfc_markers(std::u32string &str); +inline bool normalize_nfc_markers(std::u16string &str); + /** Normalize a u32string inplace to NFC. @return false on failure */ bool normalize_nfc(std::u32string &str); /** Normalize a u16string inplace to NFC. @return false on failure */ bool normalize_nfc(std::u16string &str); +/** Remove markers and optionally note their glue characters in the map */ +std::u32string remove_markers(const std::u32string &str, marker_map *markers = nullptr); +/** same but with a reference */ +inline std::u32string remove_markers(const std::u32string &str, marker_map &markers) { + return remove_markers(str, &markers); +} + +/** prepend the marker string in UC_SENTINEL format to the str */ +inline static void prepend_marker(std::u32string &str, KMX_DWORD marker); + +void +prepend_marker(std::u32string &str, KMX_DWORD marker) { + km_core_usv triple[] = {LDML_UC_SENTINEL, LDML_MARKER_CODE, marker}; + str.insert(0, triple, 3); +} + +bool normalize_nfd_markers(std::u16string &str) { + marker_map m; + return normalize_nfd_markers(str, m); +} + +bool normalize_nfc_markers(std::u16string &str) { + marker_map m; + return normalize_nfc_markers(str, m); +} +bool normalize_nfd_markers(std::u32string &str) { + marker_map m; + return normalize_nfd_markers(str, m); +} + +bool normalize_nfc_markers(std::u32string &str) { + marker_map m; + return normalize_nfc_markers(str, m); +} + } // namespace ldml } // namespace core diff --git a/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml b/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml index 52ca6903c48..efa0597670c 100644 --- a/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml +++ b/core/tests/unit/ldml/keyboards/k_001_tiny-test.xml @@ -6,7 +6,6 @@ - diff --git a/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml index 7ef89381aaa..5239fe3409c 100644 --- a/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml +++ b/core/tests/unit/ldml/keyboards/k_006_backspace-test.xml @@ -4,12 +4,11 @@ - - + diff --git a/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml b/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml index 8d451467048..d4c5138a922 100644 --- a/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml +++ b/core/tests/unit/ldml/keyboards/k_007_transform_rgx-test.xml @@ -4,14 +4,12 @@ - - diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml index 025fe36a483..3ea269beec1 100644 --- a/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm-test.xml @@ -5,21 +5,18 @@ - - - @@ -27,7 +24,6 @@ - @@ -35,19 +31,16 @@ - - - @@ -55,7 +48,6 @@ - @@ -63,7 +55,6 @@ - @@ -71,19 +62,16 @@ - - - @@ -91,7 +79,6 @@ - @@ -99,7 +86,6 @@ - @@ -107,19 +93,16 @@ - - - @@ -127,10 +110,109 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml index 1162a7769e5..06ad0695f7d 100644 --- a/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml +++ b/core/tests/unit/ldml/keyboards/k_008_transform_norm.xml @@ -15,6 +15,7 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke + @@ -26,7 +27,7 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke - + @@ -43,6 +44,29 @@ from https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-ke + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml index c5270ac868c..e207d191931 100644 --- a/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml +++ b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml @@ -4,12 +4,10 @@ - - @@ -22,12 +20,10 @@ - - @@ -36,7 +32,6 @@ - @@ -45,7 +40,6 @@ - diff --git a/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml b/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml new file mode 100644 index 00000000000..e97ebd35b66 --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_201_reorder_esk-test.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_201_reorder_esk.xml b/core/tests/unit/ldml/keyboards/k_201_reorder_esk.xml new file mode 100644 index 00000000000..0b49446ab1f --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_201_reorder_esk.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml index 24f104f4d71..8b05aca859d 100644 --- a/core/tests/unit/ldml/keyboards/k_210_marker-test.xml +++ b/core/tests/unit/ldml/keyboards/k_210_marker-test.xml @@ -4,25 +4,21 @@ - - - - @@ -37,20 +33,17 @@ - - - diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index f4b7f23a29d..c703d542ab8 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -39,6 +39,7 @@ tests_with_testdata = [ 'k_008_transform_norm', 'k_020_fr', # TODO-LDML: move to cldr above (fix vkey) 'k_200_reorder_nod_Lana', + 'k_201_reorder_esk', 'k_210_marker', ] diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index eaf764e18eb..e22367a7f96 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -27,6 +27,9 @@ #include // for surrogate pair macros #include "ldml_test_source.hpp" +#include "debuglog.h" + +#include "ldml/ldml_transforms.hpp" namespace { @@ -105,6 +108,7 @@ apply_action( assert(!context.empty()); assert(context.back().type == KM_CORE_CT_MARKER); context.pop_back(); + // no change to text store. } else if (text_store.length() > 0) { assert(!context.empty() && !text_store.empty()); km_core_usv ch = text_store.back(); @@ -118,11 +122,16 @@ apply_action( text_store.pop_back(); } } - assert(ch == act.backspace.expected_value); - - assert(context.back().type == KM_CORE_CT_CHAR); - assert(context.back().character == ch); - context.pop_back(); + if (act.backspace.expected_type == KM_CORE_BT_CHAR) { + assert(ch == act.backspace.expected_value); + assert(context.back().type == KM_CORE_CT_CHAR); + assert(context.back().character == ch); + context.pop_back(); + } else { + // assume it's otherwise KM-coRE_BT_UNKNOWN + assert(act.backspace.expected_type == KM_CORE_BT_UNKNOWN); + assert(context.empty()); // if KM_CORE_BT_UNKNOWN, context should be empty. + } } break; case KM_CORE_IT_PERSIST_OPT: @@ -156,6 +165,21 @@ verify_context(std::u16string& text_store, km_core_state* &test_state, std::vect km_core_cp *buf = new km_core_cp[n]; try_status(km_core_context_items_to_utf16(citems, buf, &n)); std::cout << "context : " << string_to_hex(buf) << " [" << buf << "]" << std::endl; + std::cout << "testcontext "; + for (auto i = test_context.begin(); i < test_context.end(); i++) { + switch(i->type) { + case KM_CORE_CT_CHAR: + std::cout << "U+" << std::hex << i->character << std::dec << " "; + break; + case KM_CORE_CT_MARKER: + std::cout << "\\m{" << i->character << "} "; + break; + default: + std::cout << "type#" << i->type << " "; + } + } + std::cout << std::endl; + std::cout << "context : " << string_to_hex(buf) << " [" << buf << "]" << std::endl; // Verify that both our local test_context and the core's test_state.context have // not diverged @@ -216,7 +240,8 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests // handle backspace here if (action.type == km::tests::LDML_ACTION_KEY_EVENT) { auto &p = action.k; - std::cout << "- key action: 0x" << std::hex << p.vk << "/modifier 0x" << p.modifier_state << std::dec << std::endl; + std::cout << "- key action: " << km::core::kmx::Debug_VirtualKey(p.vk) << "/modifier " << km::core::kmx::Debug_ModifierName(p.modifier_state) << " 0x" << p.modifier_state + << std::dec << std::endl; // Because a normal system tracks caps lock state itself, // we mimic that in the tests. We assume caps lock state is // updated on key_down before the processor receives the @@ -250,6 +275,7 @@ run_test(const km::core::path &source, const km::core::path &compiled, km::tests verify_context(text_store, test_state, test_context); } else if (action.type == km::tests::LDML_ACTION_CHECK_EXPECTED) { + assert(km::core::ldml::normalize_nfd(action.string)); // TODO-LDML: should be NFC std::cout << "- check expected" << std::endl; std::cout << "expected : " << string_to_hex(action.string) << " [" << action.string << "]" << std::endl; std::cout << "text store: " << string_to_hex(text_store) << " [" << text_store << "]" << std::endl; diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 9171f25163f..793f71b91dc 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -472,7 +472,7 @@ LdmlJsonTestSource::next_action(ldml_action &fillin) { if (type == "check") { fillin.type = LDML_ACTION_CHECK_EXPECTED; fillin.string = LdmlTestSource::parse_u8_source_string(result.get()); - assert(km::core::ldml::normalize_nfc(fillin.string)); + assert(km::core::ldml::normalize_nfd(fillin.string)); // TODO-LDML: should be NFC return; } else if (type == "keystroke") { fillin.type = LDML_ACTION_KEY_EVENT; @@ -483,7 +483,7 @@ LdmlJsonTestSource::next_action(ldml_action &fillin) { } else if (type == "emit") { fillin.type = LDML_ACTION_EMIT_STRING; fillin.string = LdmlTestSource::parse_u8_source_string(to.get()); - assert(km::core::ldml::normalize_nfc(fillin.string)); + assert(km::core::ldml::normalize_nfd(fillin.string)); // TODO-LDML: should be NFC return; } else if (type == "backspace") { // backspace is handled as a key event @@ -504,10 +504,15 @@ LdmlJsonTestSource::get_context() const { } int LdmlJsonTestSource::load(const nlohmann::json &data) { - this->data = data; // TODO-LDML - auto startContext = data["/startContext/to"_json_pointer]; - context = LdmlTestSource::parse_u8_source_string(startContext); - assert(km::core::ldml::normalize_nfc(context)); + this->data = data; // TODO-LDML: restructure to not need this pointer? + // TODO-LDML: Need an update to json.hpp to use contains() + // if (data.contains("/startContext"_json_pointer)) { + if (data.find("startContext") != data.end()) { + // only set startContext if present - it's optional. + auto startContext = data["/startContext/to"_json_pointer]; + context = LdmlTestSource::parse_u8_source_string(startContext); + assert(km::core::ldml::normalize_nfd(context)); // TODO-LDML: should be NFC + } return 0; } diff --git a/core/tests/unit/ldml/meson.build b/core/tests/unit/ldml/meson.build index 2416906473f..f85014bebeb 100644 --- a/core/tests/unit/ldml/meson.build +++ b/core/tests/unit/ldml/meson.build @@ -61,22 +61,6 @@ ldml = executable('ldml', objects: lib.extract_all_objects(recursive: false), ) -# Run tests on all keyboards (`tests` defined in keyboards/meson.build) - -foreach kbd : tests - kbd_src = join_paths(test_path, kbd) + '.xml' - kbd_obj = join_paths(test_path, kbd) + '.kmx' - test(kbd, ldml, args: [kbd_src, kbd_obj], suite: 'ldml-keyboards') -endforeach - -# Run tests on all invalid keyboards (`invalid_tests` defined in invalid-keyboards/meson.build) - -foreach kbd : invalid_tests - kbd_src = join_paths(invalid_test_path, kbd) + '.xml' - kbd_obj = join_paths(invalid_test_path, kbd) + '.kmx' - test(kbd, ldml, args: [kbd_src, kbd_obj], suite: 'ldml-invalid-keyboards') - # todo: consider if we should use `should_fail: true`? -endforeach # Build and run additional test_kmx_plus test @@ -89,6 +73,8 @@ e = executable('test_kmx_plus', 'test_kmx_plus.cpp', objects: lib.extract_all_objects(recursive: false)) test('test_kmx_plus', e, suite: 'ldml') +# run transforms / ldml utilities unit test + t = executable('test_transforms', 'test_transforms.cpp', cpp_args: defns + warns, include_directories: [inc, libsrc, '../../../../developer/src/ext/json'], @@ -96,3 +82,20 @@ t = executable('test_transforms', 'test_transforms.cpp', dependencies: [icu_uc, icu_i18n], objects: lib.extract_all_objects(recursive: false)) test('test_transforms', t, suite: 'ldml') + +# Run tests on all keyboards (`tests` defined in keyboards/meson.build) + +foreach kbd : tests + kbd_src = join_paths(test_path, kbd) + '.xml' + kbd_obj = join_paths(test_path, kbd) + '.kmx' + test(kbd, ldml, args: [kbd_src, kbd_obj], suite: 'ldml-keyboards') +endforeach + +# Run tests on all invalid keyboards (`invalid_tests` defined in invalid-keyboards/meson.build) + +foreach kbd : invalid_tests + kbd_src = join_paths(invalid_test_path, kbd) + '.xml' + kbd_obj = join_paths(invalid_test_path, kbd) + '.kmx' + test(kbd, ldml, args: [kbd_src, kbd_obj], suite: 'ldml-invalid-keyboards') + # todo: consider if we should use `should_fail: true`? +endforeach diff --git a/core/tests/unit/ldml/test_transforms.cpp b/core/tests/unit/ldml/test_transforms.cpp index cb0db12bfe4..fe47f043950 100644 --- a/core/tests/unit/ldml/test_transforms.cpp +++ b/core/tests/unit/ldml/test_transforms.cpp @@ -4,6 +4,7 @@ #include #include #include +#include "test_color.h" // TODO-LDML: normal asserts wern't working, so using some hacks. // #include "ldml_test_utils.hpp" @@ -11,18 +12,25 @@ // #include "debuglog.h" #ifndef zassert_string_equal -#define zassert_string_equal(actual, expected) \ - { \ - if (actual != expected) { \ - std::cerr << __FILE__ << ":" << __LINE__ << ": " \ - << "got: " << actual << " expected " << expected << std::endl; \ - return EXIT_FAILURE; \ - } \ +#define zassert_string_equal(actual, expected) \ + { \ + if (actual != expected) { \ + std::wcerr << __FILE__ << ":" << __LINE__ << ": " << console_color::fg(console_color::BRIGHT_RED) << "got: " << km::core::kmx::Debug_UnicodeString(actual, 0) \ + << " expected " << km::core::kmx::Debug_UnicodeString(expected, 1) << console_color::reset() << std::endl; \ + return EXIT_FAILURE; \ + } \ } #endif #ifndef zassert_equal -#define zassert_equal(actual, expected) zassert_string_equal(actual, expected) +#define zassert_equal(actual, expected) \ + { \ + if (actual != expected) { \ + std::wcerr << __FILE__ << ":" << __LINE__ << ": " << console_color::fg(console_color::BRIGHT_RED) << "got: " << actual \ + << " expected " << expected << console_color::reset() << std::endl; \ + return EXIT_FAILURE; \ + } \ + } #endif // needed for streaming operators @@ -150,6 +158,22 @@ int test_reorder_standalone() { std::cout << "== " << __FUNCTION__ << std::endl; + std::cout << __FILE__ << ":" << __LINE__ << " - element API test " << std::endl; + // element API test - not a real element, just here for testing + { + element es(U'a', 0xF4500000 | LDML_ELEM_FLAGS_PREBASE | LDML_ELEM_FLAGS_TERTIARY_BASE); // tertiary -12, primary 80 + std::cout << "es flags" << std::hex << es.get_flags() << std::dec << std::endl; + // verify element metadata + assert_equal(es.is_uset(), false); + assert_equal(es.get_order(), 0x50); + assert_equal(es.get_tertiary(), -12); + assert_equal(es.is_prebase(), true); + assert_equal(es.is_tertiary_base(), true); + // verify element matching + assert_equal(es.matches(U'a'), true); + assert_equal(es.matches(U'b'), false); + } + std::cout << __FILE__ << ":" << __LINE__ << " - nod-Lana " << std::endl; { const std::u32string roasts[] = { @@ -171,17 +195,17 @@ test_reorder_standalone() { assert_equal(toneMarks.contains(0x1A76), true); assert_equal(toneMarks.contains(0x1A60), false); - std::cout << __FILE__ << ":" << __LINE__ << " - element test " << std::endl; + std::cout << __FILE__ << ":" << __LINE__ << " - element API test " << std::endl; // element test { - element es(U'a', 0xF4500000 | LDML_ELEM_FLAGS_PREBASE | LDML_ELEM_FLAGS_TERTIARY_BASE); // tertiary -12, primary 80 + element es(U'a', (80 << LDML_ELEM_FLAGS_ORDER_BITSHIFT) | LDML_ELEM_FLAGS_PREBASE); // tertiary -12, primary 80 std::cout << "es flags" << std::hex << es.get_flags() << std::dec << std::endl; // verify element metadata assert_equal(es.is_uset(), false); assert_equal(es.get_order(), 0x50); - assert_equal(es.get_tertiary(), -12); + assert_equal(es.get_tertiary(), 0); assert_equal(es.is_prebase(), true); - assert_equal(es.is_tertiary_base(), true); + assert_equal(es.is_tertiary_base(), false); // verify element matching assert_equal(es.matches(U'a'), true); assert_equal(es.matches(U'b'), false); @@ -227,7 +251,7 @@ test_reorder_standalone() { l.update_sort_key(0, keylist); std::cout << __FILE__ << ":" << __LINE__ << " updated sortkey" << std::endl; assert_equal(keylist.size(), 2); - size_t secondary = 0; + reorder_weight secondary = 0; for (auto i = keylist.begin(); i < keylist.end(); i++) { i->dump(); assert_equal(i->secondary, secondary); @@ -238,7 +262,7 @@ test_reorder_standalone() { // spot check first sortkey assert_equal(keylist.begin()->primary, 80); - assert_equal(keylist.begin()->tertiary, -12); + assert_equal(keylist.begin()->tertiary, 0); assert_equal(keylist.begin()->ch, 0x61); std::cout << __FILE__ << ":" << __LINE__ << " sorted sortkey" << std::endl; @@ -406,6 +430,133 @@ test_reorder_standalone() { return EXIT_SUCCESS; } + +// this test case is also in XML form under 'k_201_*' +int +test_reorder_esk() { + std::cout << "== " << __FUNCTION__ << std::endl; + + std::cout << __FILE__ << ":" << __LINE__ << " - k_201_reorder_esk (tertiary reordering) " << std::endl; + { + // now setup the rules + // rules are a little bit simplified, having only the vowel 'a' + + std::cout << "now prepare the reorder elements" << std::endl; + transforms tr; + { + reorder_group rg; + + // + { + element_list e; + e.emplace_back(U'a', (0 << LDML_ELEM_FLAGS_ORDER_BITSHIFT) | LDML_ELEM_FLAGS_TERTIARY_BASE); + rg.list.emplace_back(e); + } + + // + { + element_list e; + e.emplace_back(0x0332, (1 << LDML_ELEM_FLAGS_TERTIARY_BITSHIFT)); + rg.list.emplace_back(e); + } + // + { + element_list e; + e.emplace_back(0x305, (2 << LDML_ELEM_FLAGS_TERTIARY_BITSHIFT)); + // (should be a unicodeset, but for simplicity we're dropping the U+302) + // e.emplace_back(0x302, (2 << LDML_ELEM_FLAGS_TERTIARY_BITSHIFT)); + rg.list.emplace_back(e); + } + // + { + element_list e; + e.emplace_back(U'x', (1 << LDML_ELEM_FLAGS_ORDER_BITSHIFT)); + rg.list.emplace_back(e); + } + // + { + element_list e; + e.emplace_back(U'y', (2 << LDML_ELEM_FLAGS_ORDER_BITSHIFT)); + rg.list.emplace_back(e); + } + // + { + element_list e; + e.emplace_back(U'z', (3 << LDML_ELEM_FLAGS_ORDER_BITSHIFT)); + rg.list.emplace_back(e); + } + + tr.addGroup(rg); + } + + // now actually test it + std::cout << __FILE__ << ":" << __LINE__ << " - cases " << std::endl; + const std::u32string orig_expect[] = { + // 1short + U"ax\u0305", // orig + U"a\u0305x", // expect + + // 2longer + U"az\u0305x\u0332", // orig + U"a\u0332\u0305xz", // expect + }; + // TODO-LDML: move this into test code perhaps + for (size_t r = 0; r < sizeof(orig_expect) / sizeof(orig_expect[0]); r+= 2) { + const auto &orig = orig_expect[r + 0]; + const auto &expect = orig_expect[r + 1]; + std::cout << __FILE__ << ":" << __LINE__ << " - trying str #" << r+1 << "=" << orig << std::endl; + // try apply with string + { + std::cout << "- try apply(text, output)" << std::endl; + std::u32string text = orig; + std::u32string output; + size_t len = tr.apply(text, output); + if (len == 0) { + std::cout << " (did not apply)" << std::endl; + } else { + std::cout << " applied, matchLen= " << len << std::endl; + text.resize(text.size()-len); // shrink + text.append(output); + std::cout << " = " << text << std::endl; + } + zassert_string_equal(text, expect); + } + // try all-at-once + { + std::cout << "- try apply(text)" << std::endl; + std::u32string text = orig; + if (!tr.apply(text)) { + std::cout << " (did not apply)" << std::endl; + } else if (text == orig) { + std::cout << " (suboptimal: apply returned true but made no change)" << std::endl; + } else { + std::cout << " changed to " << text; + } + zassert_string_equal(text, expect); + std::cout << " matched (converting all at once)!" << std::endl; + } + // simulate typing this one char at a time; + { + std::cout << "- try key-at-a-time" << std::endl; + std::u32string text; + for (auto ch = orig.begin(); ch < orig.end(); ch++) { + // append the string + text.append(1, *ch); + std::cout << "-: " << text << std::endl; + if (!tr.apply(text)) { + std::cout << " (did not apply)" << std::endl; + } + } + // now the moment of truth + zassert_string_equal(text, expect); + std::cout << " matched! (converting char at a time)" << std::endl; + std::cout << std::endl; + } + } + } + return EXIT_SUCCESS; +} + int test_map() { std::cout << "== " << __FUNCTION__ << std::endl; @@ -427,10 +578,209 @@ int test_map() { return EXIT_SUCCESS; } +int test_strutils() { + std::cout << "== " << __FUNCTION__ << std::endl; + + std::cout << __FILE__ << ":" << __LINE__ << " * remove_markers" << std::endl; + + + { + std::cout << __FILE__ << ":" << __LINE__ << " - basic test0" << std::endl; + const std::u32string src = U"abc"; + const std::u32string dst = remove_markers(src); + zassert_string_equal(dst, src); // unchanged + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - basic test" << std::endl; + const std::u32string src = U"abc"; + const std::u32string dst = remove_markers(src, map); + zassert_string_equal(dst, src); // unchanged + assert_equal(map.size(), 0); + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - marker test" << std::endl; + const std::u32string src = U"6\U0000ffff\U00000008\U00000001e"; + const std::u32string dst = remove_markers(src, map); + const std::u32string expect = U"6e"; + zassert_string_equal(dst, expect); + assert_equal(map.size(), 1); + assert_equal(map[U'e'], 0x1L); // marker 1 @ e + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - bad0" << std::endl; + const std::u32string src = U"6\U0000ffff\U00000008"; // missing trailing marker # + const std::u32string dst = remove_markers(src, map); + const std::u32string expect = U"6"; + zassert_string_equal(dst, expect); + assert_equal(map.size(), 0); + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - bad1" << std::endl; + const std::u32string src = U"6\U0000ffffq"; // missing code + const std::u32string dst = remove_markers(src, map); + const std::u32string expect = U"6q"; + zassert_string_equal(dst, expect); + assert_equal(map.size(), 0); + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - bad1" << std::endl; + const std::u32string src = U"6\U0000ffff"; // missing code + const std::u32string dst = remove_markers(src, map); + const std::u32string expect = U"6"; + zassert_string_equal(dst, expect); + assert_equal(map.size(), 0); + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - marker end test" << std::endl; + const std::u32string src = U"6\U0000ffff\U00000008\U00000001"; + const std::u32string dst = remove_markers(src, map); + const std::u32string expect = U"6"; + zassert_string_equal(dst, expect); + assert_equal(map.size(), 1); + assert_equal(map[MARKER_BEFORE_EOT], 0x1L); // marker 1 @ e + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - complex test" << std::endl; + const std::u32string src = U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000320\U0000ffff\U00000008\U00000003\U00000300\U0000ffff\U00000008\U00000004"; + const std::u32string dst = remove_markers(src, map); + const std::u32string expect = U"6e\U00000320\U00000300"; + zassert_string_equal(dst, expect); + assert_equal(map.size(), 4); + assert_equal(map[U'e'], 0x1L); + assert_equal(map[0x0320], 0x2L); + assert_equal(map[0x0300], 0x3L); + assert_equal(map[MARKER_BEFORE_EOT], 0x4L); + } + return EXIT_SUCCESS; +} + +int test_normalize() { + std::cout << "== " << __FUNCTION__ << std::endl; + + std::cout << __FILE__ << ":" << __LINE__ << " * normalize_nfd_markers" << std::endl; + + + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - noop test" << std::endl; + const std::u32string src = U"6e\U00000320\U00000300"; // already NFD + const std::u32string expect = src; + std::u32string dst = src; + assert(normalize_nfd_markers(dst, map)); + zassert_string_equal(dst, expect); + assert_equal(map.size(), 0); + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - medium test" << std::endl; + const std::u32string src = U"6e\U00000300\U00000320"; // swapped + const std::u32string expect = U"6e\U00000320\U00000300"; // correct NFD + std::u32string dst = src; + assert(normalize_nfd_markers(dst, map)); + zassert_string_equal(dst, expect); + assert_equal(map.size(), 0); + } + + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - noop test w markers" << std::endl; + const std::u32string src = + U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000320\U0000ffff\U00000008\U00000003\U00000300" + U"\U0000ffff\U00000008\U00000004"; + const std::u32string expect = src; + std::u32string dst = src; + assert(normalize_nfd_markers(dst, map)); + zassert_string_equal(dst, expect); + assert_equal(map.size(), 4); + assert_equal(map[U'e'], 0x1L); + assert_equal(map[0x0320], 0x2L); + assert_equal(map[0x0300], 0x3L); + assert_equal(map[MARKER_BEFORE_EOT], 0x4L); + } + + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - complex test" << std::endl; + const std::u32string src = // already in order: 320+300 + U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000320\U0000ffff\U00000008\U00000003\U00000300\U0000ffff\U00000008\U00000004"; + const std::u32string expect = src; + std::u32string dst = src; + assert(normalize_nfd_markers(dst, map)); + zassert_string_equal(dst, expect); + assert_equal(map.size(), 4); + assert_equal(map[U'e'], 0x1L); + assert_equal(map[0x0320], 0x2L); + assert_equal(map[0x0300], 0x3L); + assert_equal(map[MARKER_BEFORE_EOT], 0x4L); + + } + { + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - complex test2" << std::endl; + const std::u32string src = // out of order, 300-320 + U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000300\U0000ffff\U00000008\U00000003\U00000320\U0000ffff\U00000008\U00000004"; + const std::u32string expect = + U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000003\U00000320\U0000ffff\U00000008\U00000002\U00000300\U0000ffff\U00000008\U00000004"; + std::u32string dst = src; + assert(normalize_nfd_markers(dst, map)); + if (dst != expect) { + std::cout << "dst: " << Debug_UnicodeString(dst) << std::endl; + std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl; + } + zassert_string_equal(dst, expect); + assert_equal(map.size(), 4); + assert_equal(map[U'e'], 0x1L); + assert_equal(map[0x0320], 0x3L); + assert_equal(map[0x0300], 0x2L); + assert_equal(map[MARKER_BEFORE_EOT], 0x4L); + + } + + { + // u"4è\U0000ffff\b\U00000001̠" + marker_map map; + std::cout << __FILE__ << ":" << __LINE__ << " - complex test 4a" << std::endl; + const std::u32string src = U"4e\u0300\uFFFF\b\u0001\u0320"; + const std::u32string expect = U"4e\uFFFF\b\u0001\u0320\u0300"; + std::u32string dst = src; + assert(normalize_nfd_markers(dst, map)); + if (dst != expect) { + std::cout << "dst: " << Debug_UnicodeString(dst) << std::endl; + std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl; + } + zassert_string_equal(dst, expect); + assert_equal(map.size(), 1); + assert_equal(map[0x0320], 0x1L); + } + + return EXIT_SUCCESS; +} + + int main(int argc, const char *argv[]) { int rc = EXIT_SUCCESS; + bool arg_color = false; + + int first_arg = 1; + + if (first_arg < argc) { + arg_color = std::string(argv[first_arg]) == "--color"; + if(arg_color) { + first_arg++; + } + } + + console_color::enabled = console_color::isaterminal() || arg_color; + if (test_transforms() != EXIT_SUCCESS) { rc = EXIT_FAILURE; } @@ -439,10 +789,21 @@ main(int argc, const char *argv[]) { rc = EXIT_FAILURE; } + if (test_reorder_esk() != EXIT_SUCCESS) { + rc = EXIT_FAILURE; + } + if (test_map() != EXIT_SUCCESS) { rc = EXIT_FAILURE; } + if (test_strutils() != EXIT_SUCCESS) { + rc = EXIT_FAILURE; + } + + if (test_normalize() != EXIT_SUCCESS) { + rc = EXIT_FAILURE; + } if (rc == EXIT_FAILURE) { std::cout << "== FAILURE" << std::endl; diff --git a/developer/build.sh b/developer/build.sh index 47cc05f533b..f0b1373dff6 100755 --- a/developer/build.sh +++ b/developer/build.sh @@ -40,4 +40,9 @@ builder_describe \ builder_parse "$@" +builder_describe_outputs \ + configure /developer/src/tike/xml/layoutbuilder/keymanweb-osk.ttf + +builder_run_action configure cp "$KEYMAN_ROOT/common/resources/fonts/keymanweb-osk.ttf" "$KEYMAN_ROOT/developer/src/tike/xml/layoutbuilder/" + builder_run_child_actions clean configure build test diff --git a/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts b/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts index 6869f1441ea..a35739247b4 100644 --- a/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/keymanweb-compiler.ts @@ -9,7 +9,6 @@ export class LdmlKeyboardKeymanWebCompiler { private readonly options: LdmlCompilerOptions; private readonly nl: string; private readonly tab: string; - constructor(private callbacks: CompilerCallbacks, options?: LdmlCompilerOptions) { this.options = { ...options }; this.nl = this.options.saveDebug ? "\n" : ''; @@ -18,7 +17,7 @@ export class LdmlKeyboardKeymanWebCompiler { public compileVisualKeyboard(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile) { const nl = this.nl, tab = this.tab; - const vkc = new LdmlKeyboardVisualKeyboardCompiler(); + const vkc = new LdmlKeyboardVisualKeyboardCompiler(this.callbacks); const vk: VisualKeyboard.VisualKeyboard = vkc.compile(source); let result = diff --git a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts index 2db9da2b539..bbb2d83abf9 100644 --- a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts @@ -1,7 +1,11 @@ -import { VisualKeyboard, LDMLKeyboard } from "@keymanapp/common-types"; +import { VisualKeyboard, LDMLKeyboard, CompilerCallbacks } from "@keymanapp/common-types"; import { KeysCompiler } from "./keys.js"; +import { CompilerMessages } from "./messages.js"; export class LdmlKeyboardVisualKeyboardCompiler { + public constructor(private callbacks: CompilerCallbacks) { + } + public compile(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile): VisualKeyboard.VisualKeyboard { let result = new VisualKeyboard.VisualKeyboard(); @@ -29,12 +33,16 @@ export class LdmlKeyboardVisualKeyboardCompiler { layer: LDMLKeyboard.LKLayer, hardware: string, ) { + const layerId = layer.id; if (hardware === 'touch') { hardware = 'us'; // TODO-LDML: US Only. Do something different here? } const keymap = KeysCompiler.getKeymapFromForms(source.keyboard3?.forms?.form, hardware); if (!keymap) { - throw Error(`Internal error: could not find keymap for form ${hardware}`); + this.callbacks.reportMessage( + CompilerMessages.Error_InvalidHardware({ formId: hardware }) + ); + return; } const shift = this.translateLayerIdToVisualKeyboardShift(layer.id); @@ -45,20 +53,23 @@ export class LdmlKeyboardVisualKeyboardCompiler { const keys = row.keys.split(' '); let x = -1; for(let key of keys) { + const keyId = key; x++; let keydef = source.keyboard3.keys?.key?.find(x => x.id == key); if (!keydef) { - throw Error(`Internal Error: could not find key id="${key}" in layer "${layer.id || ''}", row "${y}"`); + this.callbacks.reportMessage( + CompilerMessages.Error_KeyNotFoundInKeyBag({ keyId, layer: layerId, row: y, col: x, form: hardware }) + ); + } else { + vk.keys.push({ + flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, + shift: shift, + text: keydef.output, // TODO-LDML: displays + vkey: keymap[y][x], + }); } - - vk.keys.push({ - flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, - shift: shift, - text: keydef.output, // TODO-LDML: displays - vkey: keymap[y][x], - }); } } } diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index c919e729ac0..ea76da94b35 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -146,7 +146,7 @@ export function compileVisualKeyboard(inputFilename: string, options: LdmlCompil checkMessages(); assert.isTrue(valid, 'k.validate should not have failed'); - const vk = (new LdmlKeyboardVisualKeyboardCompiler()).compile(source); + const vk = (new LdmlKeyboardVisualKeyboardCompiler(compilerTestCallbacks)).compile(source); checkMessages(); assert.isNotNull(vk, 'LdmlKeyboardVisualKeyboardCompiler.compile should not have returned null'); diff --git a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts index 1b0c2b22e5c..551ce66fa4b 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildLdmlKeyboard.ts @@ -4,6 +4,7 @@ import * as kmcLdml from '@keymanapp/kmc-ldml'; import { KvkFileWriter, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader, CompilerOptions, defaultCompilerOptions, KeymanFileTypes } from '@keymanapp/common-types'; import { BuildActivity } from './BuildActivity.js'; import { fileURLToPath } from 'url'; +import { InfrastructureMessages } from '../../messages/infrastructureMessages.js'; export class BuildLdmlKeyboard extends BuildActivity { public get name(): string { return 'LDML keyboard'; } @@ -20,21 +21,32 @@ export class BuildLdmlKeyboard extends BuildActivity { const outFileBase = path.basename(fileBaseName, path.extname(fileBaseName)); const outFileDir = path.dirname(fileBaseName); + try { + fs.mkdirSync(outFileDir, {recursive: true}); + } catch(e) { + callbacks.reportMessage(InfrastructureMessages.Error_CannotCreateFolder({folderName:outFileDir, e})); + return false; + } + if(kmx && kvk) { const outFileKmx = path.join(outFileDir, outFileBase + KeymanFileTypes.Binary.Keyboard); + // TODO: console needs to be replaced with InfrastructureMessages console.log(`Writing compiled keyboard to ${outFileKmx}`); fs.writeFileSync(outFileKmx, kmx); const outFileKvk = path.join(outFileDir, outFileBase + KeymanFileTypes.Binary.VisualKeyboard); + // TODO: console needs to be replaced with InfrastructureMessages console.log(`Writing compiled visual keyboard to ${outFileKvk}`); fs.writeFileSync(outFileKvk, kvk); } else { + // TODO: console needs to be replaced with InfrastructureMessages console.error(`An error occurred compiling ${infile}`); return false; } if(kmw) { const outFileKmw = path.join(outFileDir, outFileBase + KeymanFileTypes.Binary.WebKeyboard); + // TODO: console needs to be replaced with InfrastructureMessages console.log(`Writing compiled js keyboard to ${outFileKmw}`); fs.writeFileSync(outFileKmw, kmw); } @@ -70,7 +82,7 @@ async function buildLdmlKeyboardToMemory(inputFilename: string, callbacks: Compi const builder = new kmcLdml.KMXBuilder(kmx, options.saveDebug); const kmx_binary = builder.compile(); - const vkcompiler = new kmcLdml.LdmlKeyboardVisualKeyboardCompiler(); + const vkcompiler = new kmcLdml.LdmlKeyboardVisualKeyboardCompiler(callbacks); const vk = vkcompiler.compile(source); const writer = new KvkFileWriter(); const kvk_binary = writer.write(vk); diff --git a/developer/src/kmc/src/messages/infrastructureMessages.ts b/developer/src/kmc/src/messages/infrastructureMessages.ts index f291c5b4cf7..50334a6fd64 100644 --- a/developer/src/kmc/src/messages/infrastructureMessages.ts +++ b/developer/src/kmc/src/messages/infrastructureMessages.ts @@ -81,7 +81,11 @@ export class InfrastructureMessages { static ERROR_CannotCreateFolder = SevError | 0x0011; static Error_InvalidProjectFolder = (o:{folderName:string}) => m(this.ERROR_InvalidProjectFolder, - `The folder ${o.folderName} does not appear to be a Keyman Developer project`); + `The folder ${o.folderName} does not appear to be a Keyman Developer project.`); static ERROR_InvalidProjectFolder = SevError | 0x0012; + + static Error_UnsupportedProjectVersion = (o:{version:string}) => m(this.ERROR_UnsupportedProjectVersion, + `Project version ${o.version} is not supported by this version of Keyman Developer.`); + static ERROR_UnsupportedProjectVersion = SevError | 0x0013; } diff --git a/developer/src/kmc/src/util/projectLoader.ts b/developer/src/kmc/src/util/projectLoader.ts index 10cd58fc818..b26399ee395 100644 --- a/developer/src/kmc/src/util/projectLoader.ts +++ b/developer/src/kmc/src/util/projectLoader.ts @@ -47,6 +47,10 @@ function loadProjectFromFile(infile: string, callbacks: CompilerCallbacks): Keym let kpj = null; try { kpj = reader.read(kpjData); + if(kpj.KeymanDeveloperProject?.Options?.Version && kpj.KeymanDeveloperProject.Options.Version != "1.0" && kpj.KeymanDeveloperProject.Options.Version != "2.0") { + callbacks.reportMessage(InfrastructureMessages.Error_UnsupportedProjectVersion({version: kpj.KeymanDeveloperProject.Options.Version})); + return null; + } reader.validate(kpj); } catch(e) { callbacks.reportMessage(InfrastructureMessages.Error_InvalidProjectFile({message: (e??'').toString()})); diff --git a/developer/src/kmc/test/fixtures/invalid-projects/error_unsupported_project_version.kpj b/developer/src/kmc/test/fixtures/invalid-projects/error_unsupported_project_version.kpj new file mode 100644 index 00000000000..2c97588f0a7 --- /dev/null +++ b/developer/src/kmc/test/fixtures/invalid-projects/error_unsupported_project_version.kpj @@ -0,0 +1,13 @@ + + + + $PROJECTPATH\build + $PROJECTPATH\source + True + True + False + keyboard + + 3.0 + + diff --git a/developer/src/kmc/test/test-infrastructureMessages.ts b/developer/src/kmc/test/test-infrastructureMessages.ts index fde352f3dd3..6d543a5fd7c 100644 --- a/developer/src/kmc/test/test-infrastructureMessages.ts +++ b/developer/src/kmc/test/test-infrastructureMessages.ts @@ -4,7 +4,7 @@ import { InfrastructureMessages } from '../src/messages/infrastructureMessages.j import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; import { NodeCompilerCallbacks } from '../src/util/NodeCompilerCallbacks.js'; -import { CompilerErrorNamespace } from '@keymanapp/common-types'; +import { CompilerErrorNamespace, CompilerEvent } from '@keymanapp/common-types'; import { unitTestEndpoints } from '../src/commands/build.js'; import { KmnCompilerMessages } from '@keymanapp/kmc-kmn'; @@ -21,12 +21,13 @@ describe('InfrastructureMessages', function () { it('should generate FATAL_UnexpectedException if an exception is raised', async function() { const ncb = new NodeCompilerCallbacks({logLevel: 'silent'}); + const expectedMessages = [InfrastructureMessages.FATAL_UnexpectedException]; + process.env.SENTRY_CLIENT_TEST_BUILD_EXCEPTION = '1'; await unitTestEndpoints.build(null, ncb, {}); delete process.env.SENTRY_CLIENT_TEST_BUILD_EXCEPTION; - assert.isTrue(ncb.hasMessage(InfrastructureMessages.FATAL_UnexpectedException), - `FATAL_UnexpectedException not generated, instead got: `+JSON.stringify(ncb.messages,null,2)); - assert.lengthOf(ncb.messages, 1); + + assertMessagesEqual(ncb.messages, expectedMessages); assert.instanceOf(ncb.messages[0].exceptionVar, Error); }); @@ -62,15 +63,14 @@ describe('InfrastructureMessages', function () { // This message is generated by NodeCompilerCallbacks, because that's where the filesystem is visible, // so we can't use our usual testForMessage pattern. const ncb = new NodeCompilerCallbacks({logLevel: 'silent'}); + const expectedMessages = [InfrastructureMessages.HINT_FilenameHasDifferingCase]; ncb.loadFile(makePathToFixture('invalid-keyboards', 'Hint_Filename_Has_Differing_Case.kmn')); - assert.isTrue(ncb.hasMessage(InfrastructureMessages.HINT_FilenameHasDifferingCase), - `HINT_FilenameHasDifferingCase not generated, instead got: `+JSON.stringify(ncb.messages,null,2)); + assertMessagesEqual(ncb.messages, expectedMessages); }); // INFO_WarningsHaveFailedBuild it('should generate INFO_WarningsHaveFailedBuild if only warnings failed the build', async function() { - // NOTE: we can probably re-use this format for most other message tests in the future const ncb = new NodeCompilerCallbacks({logLevel: 'silent'}); const filename = makePathToFixture('compiler-warnings-as-errors', 'keyboard.kmn'); const expectedMessages = [ @@ -80,8 +80,26 @@ describe('InfrastructureMessages', function () { InfrastructureMessages.INFO_FileNotBuiltSuccessfully ]; await unitTestEndpoints.build(filename, ncb, {compilerWarningsAsErrors: true}); - assert.deepEqual(ncb.messages.map(m => m.code), expectedMessages, - `actual callbacks.messages:\n${JSON.stringify(ncb.messages,null,2)}\n\n`+ - `did not match expected:\n${JSON.stringify(expectedMessages,null,2)}\n\n`); + assertMessagesEqual(ncb.messages, expectedMessages); + }); + + // ERROR_UnsupportedProjectVersion + + it('should generate ERROR_UnsupportedProjectVersion if a project file with an unsupported version is loaded', async function() { + const ncb = new NodeCompilerCallbacks({logLevel: 'silent'}); + const filename = makePathToFixture('invalid-projects', 'error_unsupported_project_version.kpj'); + const expectedMessages = [ + InfrastructureMessages.INFO_BuildingFile, + InfrastructureMessages.ERROR_UnsupportedProjectVersion, + InfrastructureMessages.INFO_ProjectNotBuiltSuccessfully + ]; + await unitTestEndpoints.build(filename, ncb, {compilerWarningsAsErrors: true}); + assertMessagesEqual(ncb.messages, expectedMessages); }); }); + +function assertMessagesEqual(actualMessages: CompilerEvent[], expectedMessages: number[]) { + assert.deepEqual(actualMessages.map(m => m.code), expectedMessages, + `actual callbacks.messages:\n${JSON.stringify(actualMessages,null,2)}\n\n`+ + `did not match expected:\n${JSON.stringify(expectedMessages,null,2)}\n\n`); +} \ No newline at end of file diff --git a/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas b/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas index 92f7366217c..fb0e22541cd 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas @@ -28,6 +28,7 @@ TImportWindowsKeyboard = class FCopyright: string; FTargets: TKeymanTargets; FFullCopyright: string; + FDescription: string; function LoadKLIDDetails: Boolean; function ImportKeyboard(const DestinationFilename, DestinationKVKSFilename: string): Boolean; function GenerateIcon(const IconFilename: string): Boolean; @@ -46,6 +47,7 @@ TImportWindowsKeyboard = class function GetProjectFilename: string; procedure SetTargets(const Value: TKeymanTargets); procedure SetFullCopyright(const Value: string); + procedure SetDescription(const Value: string); public function Execute: Boolean; overload; @@ -62,6 +64,7 @@ TImportWindowsKeyboard = class property Version: string read FVersion write SetVersion; property BCP47Tags: string read FBCP47Tags write SetBCP47Tags; property Author: string read FAuthor write SetAuthor; + property Description: string read FDescription write SetDescription; property Targets: TKeymanTargets read FTargets write SetTargets; property ProjectFilename: string read GetProjectFilename; @@ -151,6 +154,11 @@ procedure TImportWindowsKeyboard.SetCopyright(const Value: string); FCopyright := Value; end; +procedure TImportWindowsKeyboard.SetDescription(const Value: string); +begin + FDescription := Value; +end; + procedure TImportWindowsKeyboard.SetDestinationPath(const Value: string); begin FDestinationPath := IncludeTrailingPathDelimiter(Value); @@ -219,6 +227,7 @@ function TImportWindowsKeyboard.Execute: Boolean; FTemplate.Copyright := FCopyright; FTemplate.FullCopyright := FFullCopyright; FTemplate.Author := FAuthor; + FTemplate.Description := FDescription; FTemplate.Version := FVersion; FTemplate.IncludeIcon := True; diff --git a/developer/src/kmconvert/Keyman.Developer.System.KMConvertParameters.pas b/developer/src/kmconvert/Keyman.Developer.System.KMConvertParameters.pas index b58e5dd8fb5..c6f6053417d 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.KMConvertParameters.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.KMConvertParameters.pas @@ -26,6 +26,7 @@ TKMConvertParameters = record FModelIdLanguage: string; FModelIdUniq: string; FEmitUsage: Boolean; + FDescription: string; function CheckParam(name, value: string): Boolean; function SetKLID(const value: string): Boolean; @@ -44,6 +45,7 @@ TKMConvertParameters = record function IsValidModelComponent(const component, value: string): Boolean; function ValidateBCP47Tag(const component, tag: string): Boolean; procedure OutputText(const msg: string = ''); + function SetDescription(const value: string): Boolean; public type TOutputTextProc = reference to procedure(msg: string); var OnOutputText: TOutputTextProc; @@ -62,6 +64,7 @@ TKMConvertParameters = record property Version: string read FVersion; property BCP47Tags: string read FBCP47Tags; property Author: string read FAuthor; + property Description: string read FDescription; property Targets: TKeymanTargets read FTargets; property Mode: TKMConvertMode read FMode; property NoLogo: Boolean read FNoLogo; @@ -90,8 +93,8 @@ function TKMConvertParameters.CheckParams(Params: TArray): Boolean; FEmitUsage := False; FDestination := '.'; - FCopyright := 'Copyright (C)'; - FFullCopyright := 'Copyright (C) '+FormatDateTime('yyyy', Now); + FCopyright := 'Copyright '+Char($00A9 {copyright}); + FFullCopyright := 'Copyright '+Char($00A9 {copyright})+' '+FormatDateTime('yyyy', Now); FVersion := '1.0'; FTargets := [ktAny]; @@ -161,6 +164,7 @@ procedure TKMConvertParameters.WriteUsage; OutputText(' (in `import-windows` mode, can be a format string)'); OutputText(' -o The target folder to write the project into, defaults to "."'); OutputText(' -author Name of author of the keyboard/model, no default'); + OutputText(' -description Short plain-text description of the keyboard/model, no default'); OutputText(' -name Name of the keyboard/model, e.g. "My First Keyboard", "%s Basic" '); OutputText(' (format strings are only valid in `import-windows` mode)'); OutputText(' -copyright Copyright string for the keyboard/model, defaults to "Copyright (C)"'); @@ -189,6 +193,7 @@ function TKMConvertParameters.CheckParam(name, value: string): Boolean; else if name = '-version' then Result := SetVersion(value) else if name = '-languages' then Result := SetBCP47Tags(value) else if name = '-author' then Result := SetAuthor(value) + else if name = '-description' then Result := SetDescription(value) else if name = '-targets' then Result := SetTargets(value) else if name = '-id-author' then Result := SetModelIdAuthor(value) else if name = '-id-language' then Result := SetModelIdLanguage(value) @@ -314,6 +319,12 @@ function TKMConvertParameters.SetAuthor(const value: string): Boolean; Result := True; end; +function TKMConvertParameters.SetDescription(const value: string): Boolean; +begin + FDescription := Value; + Result := True; +end; + function TKMConvertParameters.SetTargets(const value: string): Boolean; begin FTargets := StringToKeymanTargets(Value); diff --git a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas index 09aa463909b..4846dd49bb3 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.KeyboardProjectTemplate.pas @@ -233,6 +233,7 @@ procedure TKeyboardProjectTemplate.WriteKPS; kps.Info.Desc[PackageInfo_Name] := Name; kps.Info.Desc[PackageInfo_Copyright] := Copyright; kps.Info.Desc[PackageInfo_Author] := Author; + kps.Info.Desc[PackageInfo_Description] := Description; kps.KPSOptions.FollowKeyboardVersion := True; kps.FileName := GetPackageFilename; diff --git a/developer/src/kmconvert/Keyman.Developer.System.KeymanConvertMain.pas b/developer/src/kmconvert/Keyman.Developer.System.KeymanConvertMain.pas index b45264fc5cc..0b9309f2d16 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.KeymanConvertMain.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.KeymanConvertMain.pas @@ -50,6 +50,7 @@ function DoImportWindowsKeyboard(FParameters: TKMConvertParameters): Boolean; iwk.DestinationPath := FParameters.Destination; iwk.KeyboardIDTemplate := FParameters.KeyboardID; iwk.NameTemplate := FParameters.Name; + iwk.Description := FParameters.Description; iwk.Copyright := FParameters.Copyright; iwk.FullCopyright := FParameters.FullCopyright; iwk.Version := FParameters.Version; diff --git a/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas index be0d0c98254..50dfa8bbb83 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas @@ -161,6 +161,7 @@ procedure TModelProjectTemplate.WriteKPS; kps.Info.Desc[PackageInfo_Copyright] := Copyright; kps.Info.Desc[PackageInfo_Author] := Author; kps.Info.Desc[PackageInfo_Version] := Version; + kps.Info.Desc[PackageInfo_Description] := Description; kps.FileName := GetPackageFilename; // Add model.js diff --git a/developer/src/kmconvert/Keyman.Developer.System.ProjectTemplate.pas b/developer/src/kmconvert/Keyman.Developer.System.ProjectTemplate.pas index 7137c8f8c00..7e47753ae87 100644 --- a/developer/src/kmconvert/Keyman.Developer.System.ProjectTemplate.pas +++ b/developer/src/kmconvert/Keyman.Developer.System.ProjectTemplate.pas @@ -25,6 +25,7 @@ TProjectTemplate = class FProjectType: TKeymanProjectType; FTargets: TKeymanTargets; FFullCopyright: string; + FDescription: string; protected const @@ -61,6 +62,7 @@ TProjectTemplate = class property Targets: TKeymanTargets read FTargets; property Name: string read FName write FName; + property Description: string read FDescription write FDescription; property Copyright: string read FCopyright write FCopyright; property FullCopyright: string read FFullCopyright write FFullCopyright; property Author: string read FAuthor write FAuthor; @@ -235,10 +237,12 @@ procedure TProjectTemplate.Transform(const SourceFile: string; DestFile: string end; s := ReplaceStr(s, '$NAME', FName); + s := ReplaceStr(s, '$ID', FId); s := ReplaceStr(s, '$VERSION', FVersion); s := ReplaceStr(s, '$COPYRIGHT', FCopyright); s := ReplaceStr(s, '$FULLCOPYRIGHT', FFullCopyright); s := ReplaceStr(s, '$AUTHOR', FAuthor); + s := ReplaceStr(s, '$DESCRIPTION', FDescription); s := ReplaceStr(s, '$DATE', FormatDateTime('yyyy-mm-dd', Now)); if Pos('$LANGUAGES_KEYBOARD_INFO', s) > 0 then s := ReplaceStr(s, '$LANGUAGES_KEYBOARD_INFO', GetLanguageTagListForKeyboardInfo); diff --git a/developer/src/kmconvert/data/basic-keyboard/README.md b/developer/src/kmconvert/data/basic-keyboard/README.md index c6af71ca9be..56c60908394 100644 --- a/developer/src/kmconvert/data/basic-keyboard/README.md +++ b/developer/src/kmconvert/data/basic-keyboard/README.md @@ -1,14 +1,13 @@ $NAME keyboard ============== -Version $VERSION - Description ----------- -$NAME generated from template +$DESCRIPTION Links ----- +Keyboard Homepage: https://keyman.com/keyboards/$ID Copyright --------- diff --git a/developer/src/kmconvert/data/basic-keyboard/source/readme.htm b/developer/src/kmconvert/data/basic-keyboard/source/readme.htm index f85052999d7..041cf910315 100644 --- a/developer/src/kmconvert/data/basic-keyboard/source/readme.htm +++ b/developer/src/kmconvert/data/basic-keyboard/source/readme.htm @@ -15,7 +15,7 @@

$NAME

- $NAME $VERSION generated from template. + $DESCRIPTION

$COPYRIGHT

diff --git a/developer/src/kmconvert/data/basic-keyboard/source/welcome.htm b/developer/src/kmconvert/data/basic-keyboard/source/welcome.htm index ccfed9656a8..c003dc2fbf1 100644 --- a/developer/src/kmconvert/data/basic-keyboard/source/welcome.htm +++ b/developer/src/kmconvert/data/basic-keyboard/source/welcome.htm @@ -15,7 +15,7 @@

Start Using $NAME

- $NAME $VERSION generated from template. + $DESCRIPTION

Keyboard Layout

diff --git a/developer/src/kmconvert/data/wordlist-lexical-model/README.md b/developer/src/kmconvert/data/wordlist-lexical-model/README.md index 40feb44f827..c94a8f66b8a 100644 --- a/developer/src/kmconvert/data/wordlist-lexical-model/README.md +++ b/developer/src/kmconvert/data/wordlist-lexical-model/README.md @@ -1,11 +1,9 @@ $NAME lexical model =================== -Version $VERSION - Description ----------- -$NAME generated from template +$DESCRIPTION Links ----- diff --git a/developer/src/kmconvert/data/wordlist-lexical-model/model.model_info b/developer/src/kmconvert/data/wordlist-lexical-model/model.model_info index 62777e5e129..c8048b8268b 100644 --- a/developer/src/kmconvert/data/wordlist-lexical-model/model.model_info +++ b/developer/src/kmconvert/data/wordlist-lexical-model/model.model_info @@ -3,5 +3,5 @@ "languages": [ $LANGUAGES_KEYBOARD_INFO ], - "description": "$NAME generated from template" + "description": "$DESCRIPTION" } diff --git a/developer/src/kmconvert/data/wordlist-lexical-model/source/model.ts b/developer/src/kmconvert/data/wordlist-lexical-model/source/model.ts index 38affb78262..529b9df996c 100644 --- a/developer/src/kmconvert/data/wordlist-lexical-model/source/model.ts +++ b/developer/src/kmconvert/data/wordlist-lexical-model/source/model.ts @@ -1,6 +1,6 @@ /* - $NAME $VERSION generated from template. - + $NAME $VERSION + This is a minimal lexical model source that uses a tab delimited wordlist. See documentation online at https://help.keyman.com/developer/ for additional parameters. diff --git a/developer/src/kmconvert/data/wordlist-lexical-model/source/readme.htm b/developer/src/kmconvert/data/wordlist-lexical-model/source/readme.htm index f85052999d7..041cf910315 100644 --- a/developer/src/kmconvert/data/wordlist-lexical-model/source/readme.htm +++ b/developer/src/kmconvert/data/wordlist-lexical-model/source/readme.htm @@ -15,7 +15,7 @@

$NAME

- $NAME $VERSION generated from template. + $DESCRIPTION

$COPYRIGHT

diff --git a/developer/src/kmconvert/data/wordlist-lexical-model/source/welcome.htm b/developer/src/kmconvert/data/wordlist-lexical-model/source/welcome.htm index 1f99b6cda87..e8e6e7b4f5c 100644 --- a/developer/src/kmconvert/data/wordlist-lexical-model/source/welcome.htm +++ b/developer/src/kmconvert/data/wordlist-lexical-model/source/welcome.htm @@ -15,7 +15,7 @@

Start Using $NAME

- $NAME $VERSION generated from template. + $DESCRIPTION

Wordlist Model Documentation

diff --git a/developer/src/tike/Makefile b/developer/src/tike/Makefile index c442ddd9329..c8b0cff87e0 100644 --- a/developer/src/tike/Makefile +++ b/developer/src/tike/Makefile @@ -6,6 +6,7 @@ build: version.res manifest.res icons dirs xml xsd pull-core cd $(DEVELOPER_ROOT)\src\tike + $(COPY) $(KEYMAN_ROOT)\common\resources\fonts\keymanweb-osk.ttf $(DEVELOPER_ROOT)\src\tike\xml\layoutbuilder\keymanweb-osk.ttf $(DELPHI_MSBUILD) tike.dproj "/p:Platform=Win32" $(SENTRYTOOL_DELPHIPREP) $(WIN32_TARGET_PATH)\tike.exe -dpr tike.dpr $(TDS2DBG) $(WIN32_TARGET_PATH)\tike.exe diff --git a/developer/src/tike/actions/dmActionsDebugger.dfm b/developer/src/tike/actions/dmActionsDebugger.dfm new file mode 100644 index 00000000000..98dc623b72d --- /dev/null +++ b/developer/src/tike/actions/dmActionsDebugger.dfm @@ -0,0 +1,149 @@ +object modActionsDebugger: TmodActionsDebugger + OldCreateOrder = False + Height = 333 + Width = 408 + object actionsDebug: TActionList + Images = frmKeymanDeveloper.lstImages + Left = 60 + Top = 88 + object actDebugStartDebugger: TAction + Category = 'Debug Control' + Caption = '&Start Debugging' + ImageIndex = 44 + ShortCut = 116 + OnExecute = actDebugStartDebuggerExecute + OnUpdate = actDebugStartDebuggerUpdate + end + object actDebugStopDebugger: TAction + Category = 'Debug Control' + Caption = 'Stop Debugger' + ImageIndex = 45 + ShortCut = 8308 + OnExecute = actDebugStopDebuggerExecute + OnUpdate = actDebugStopDebuggerUpdate + end + object actDebugSetClearBreakpoint: TAction + Category = 'Debug Control' + Caption = 'Set/Clear &Breakpoint' + ImageIndex = 43 + ShortCut = 120 + OnExecute = actDebugSetClearBreakpointExecute + OnUpdate = actDebugSetClearBreakpointUpdate + end + object actDebugSingleStepMode: TAction + Category = 'Debug Action' + Caption = 'Single Step &Mode' + ImageIndex = 46 + OnExecute = actDebugSingleStepModeExecute + OnUpdate = actDebugSingleStepModeUpdate + end + object actDebugStepForward: TAction + Category = 'Debug Action' + Caption = 'Step &Forward' + ImageIndex = 47 + OnExecute = actDebugStepForwardExecute + OnUpdate = actDebugStepForwardUpdate + end + object actDebugRun: TAction + Category = 'Debug Action' + Caption = '&Run' + ImageIndex = 48 + OnExecute = actDebugRunExecute + OnUpdate = actDebugRunUpdate + end + object actDebugSelectSystemKeyboard: TAction + Category = 'Debug Control' + Caption = 'Select System &Keyboard...' + ImageIndex = 49 + Visible = False + OnExecute = actDebugSelectSystemKeyboardExecute + OnUpdate = actDebugSelectSystemKeyboardUpdate + end + object actDebugANSITestMode: TAction + Category = 'Debug Control' + Caption = '&ANSI Test Mode' + end + object actDebugViewElements: TAction + Category = 'Debug Control' + Caption = '&Elements' + ImageIndex = 50 + OnExecute = actDebugViewElementsExecute + OnUpdate = actDebugViewElementsUpdate + end + object actDebugViewCallStack: TAction + Category = 'Debug Control' + Caption = '&Call Stack' + ImageIndex = 51 + OnExecute = actDebugViewCallStackExecute + OnUpdate = actDebugViewCallStackUpdate + end + object actDebugViewDeadkeys: TAction + Category = 'Debug Control' + Caption = '&Deadkeys' + ImageIndex = 52 + OnExecute = actDebugViewDeadkeysExecute + OnUpdate = actDebugViewDeadkeysUpdate + end + object actDebugViewRegressionTesting: TAction + Category = 'Debug Control' + Caption = '&Regression Testing' + OnExecute = actDebugViewRegressionTestingExecute + OnUpdate = actDebugViewRegressionTestingUpdate + end + object actDebugTestMode: TAction + Category = 'Debug Control' + Caption = '&Test Mode' + GroupIndex = 1 + OnExecute = actDebugTestModeExecute + OnUpdate = actDebugTestModeUpdate + end + object actDebugDebuggerMode: TAction + Category = 'Debug Control' + Caption = '&Debugger Mode' + GroupIndex = 2 + OnExecute = actDebugDebuggerModeExecute + OnUpdate = actDebugDebuggerModeUpdate + end + object actDebugPause: TAction + Category = 'Debug Action' + Caption = '&Pause' + ImageIndex = 57 + OnExecute = actDebugPauseExecute + OnUpdate = actDebugPauseUpdate + end + object actDebugViewFont: TAction + Category = 'Debug Control' + Caption = '&Font...' + OnExecute = actDebugViewFontExecute + OnUpdate = actDebugViewFontUpdate + end + object actDebugViewDefaultFont: TAction + Category = 'Debug Control' + Caption = 'Use Editor &Character Font' + OnExecute = actDebugViewDefaultFontExecute + OnUpdate = actDebugViewDefaultFontUpdate + end + object actDebugViewState: TAction + Category = 'Debug Control' + Caption = '&State' + OnExecute = actDebugViewStateExecute + OnUpdate = actDebugViewStateUpdate + end + object actDebugSwitchToDebuggerWindow: TAction + Category = 'Debug Control' + Caption = 'Switch to debugger' + ShortCut = 117 + OnExecute = actDebugSwitchToDebuggerWindowExecute + OnUpdate = actDebugSwitchToDebuggerWindowUpdate + end + end + object dlgFont: TFontDialog + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'MS Sans Serif' + Font.Style = [] + Left = 256 + Top = 73 + end +end diff --git a/developer/src/tike/actions/dmActionsDebugger.pas b/developer/src/tike/actions/dmActionsDebugger.pas new file mode 100644 index 00000000000..bd0b3730ec4 --- /dev/null +++ b/developer/src/tike/actions/dmActionsDebugger.pas @@ -0,0 +1,428 @@ +(* + Name: dmActionsKeyboardEditor + Copyright: Copyright (C) SIL International. + Documentation: + Description: + Create Date: 23 Aug 2006 + + Modified Date: 4 May 2015 + Authors: mcdurdin + Related Files: + Dependencies: + + Bugs: + Todo: + Notes: + History: 23 Aug 2006 - mcdurdin - Initial version + 14 Sep 2006 - mcdurdin - Add debugger actions + 28 Sep 2006 - mcdurdin - Added Test Keyboard menu item + 06 Oct 2006 - mcdurdin - Add Test Keyman Web keyboard + 04 Dec 2006 - mcdurdin - Implement all debug actions + 04 Dec 2006 - mcdurdin - Localize + 04 Dec 2006 - mcdurdin - Add keyboard upload + 22 Jan 2007 - mcdurdin - Reset debugger before compiling + 23 Aug 2007 - mcdurdin - I962 - Fix crash when uploading a keyboard and pressing Cancel + 12 Oct 2007 - mcdurdin - I654 - handle error when installing or uninstalling an invalid keyboard file + 16 Jan 2009 - mcdurdin - Add keyboard font helper command (incomplete) + 26 Jul 2010 - mcdurdin - I2468 - KeymanWeb Pack elimination, part of standard build + 17 Dec 2010 - mcdurdin - I2595 - Remove GnuGetText + 18 May 2012 - mcdurdin - I3306 - V9.0 - Remove TntControls + Win9x support + 18 May 2012 - mcdurdin - I3323 - V9.0 - Change from Plus-MemoU to Plus-Memo + 04 Dec 2013 - mcdurdin - I3655 - V9.0 - Keyboard debugger does not appear to function in 9.0.419.0 + 21 Feb 2014 - mcdurdin - I4057 - V9.0 - Keyman Developer Keyboard Font dialog helpful to reduce font confusion + 04 Nov 2014 - mcdurdin - I4504 - V9.0 - Consolidate the compile action into single command + 04 May 2015 - mcdurdin - I4686 - V9.0 - Refactor compile into project file action + 04 May 2015 - mcdurdin - I4687 - V9.0 - Split project UI actions into separate classes + +*) +unit dmActionsDebugger; // I3306 // I3323 + +interface + +uses + SysUtils, + Classes, + ActnList, + UfrmSelectSystemKeyboard, + ImgList, + Controls, + Dialogs, + System.Actions; + +type + TmodActionsDebugger = class(TDataModule) + actionsDebug: TActionList; + actDebugStartDebugger: TAction; + actDebugSetClearBreakpoint: TAction; + actDebugStopDebugger: TAction; + actDebugSingleStepMode: TAction; + actDebugStepForward: TAction; + actDebugRun: TAction; + actDebugSelectSystemKeyboard: TAction; + actDebugANSITestMode: TAction; + actDebugViewElements: TAction; + actDebugViewCallStack: TAction; + actDebugViewDeadkeys: TAction; + actDebugViewRegressionTesting: TAction; + actDebugTestMode: TAction; + actDebugDebuggerMode: TAction; + actDebugPause: TAction; + actDebugViewFont: TAction; + actDebugViewDefaultFont: TAction; + dlgFont: TFontDialog; + actDebugViewState: TAction; + actDebugSwitchToDebuggerWindow: TAction; + procedure actionsKeyboardEditorUpdate(Action: TBasicAction; + var Handled: Boolean); + procedure actDebugViewElementsExecute(Sender: TObject); + procedure actDebugSetClearBreakpointExecute(Sender: TObject); + procedure actDebugSetClearBreakpointUpdate(Sender: TObject); + procedure actDebugStartDebuggerExecute(Sender: TObject); + procedure actDebugStartDebuggerUpdate(Sender: TObject); + procedure actDebugStopDebuggerExecute(Sender: TObject); + procedure actDebugStopDebuggerUpdate(Sender: TObject); + procedure actDebugSingleStepModeUpdate(Sender: TObject); + procedure actDebugSingleStepModeExecute(Sender: TObject); + procedure actDebugStepForwardUpdate(Sender: TObject); + procedure actDebugStepForwardExecute(Sender: TObject); + procedure actDebugRunUpdate(Sender: TObject); + procedure actDebugRunExecute(Sender: TObject); + procedure actDebugSelectSystemKeyboardExecute(Sender: TObject); + procedure actDebugDebuggerModeExecute(Sender: TObject); + procedure actDebugTestModeExecute(Sender: TObject); + procedure actDebugTestModeUpdate(Sender: TObject); + procedure actDebugDebuggerModeUpdate(Sender: TObject); + procedure actDebugPauseUpdate(Sender: TObject); + procedure actDebugPauseExecute(Sender: TObject); + procedure actDebugSelectSystemKeyboardUpdate(Sender: TObject); + procedure actDebugViewDefaultFontExecute(Sender: TObject); + procedure actDebugViewDefaultFontUpdate(Sender: TObject); + procedure actDebugViewFontExecute(Sender: TObject); + procedure actDebugViewFontUpdate(Sender: TObject); + procedure actDebugViewCallStackExecute(Sender: TObject); + procedure actDebugViewDeadkeysExecute(Sender: TObject); + procedure actDebugViewRegressionTestingExecute(Sender: TObject); + procedure actDebugViewElementsUpdate(Sender: TObject); + procedure actDebugViewCallStackUpdate(Sender: TObject); + procedure actDebugViewDeadkeysUpdate(Sender: TObject); + procedure actDebugViewRegressionTestingUpdate(Sender: TObject); + procedure actDebugViewStateExecute(Sender: TObject); + procedure actDebugViewStateUpdate(Sender: TObject); + procedure actDebugSwitchToDebuggerWindowUpdate(Sender: TObject); + procedure actDebugSwitchToDebuggerWindowExecute(Sender: TObject); + private + function IsDebuggerVisible: Boolean; + function IsDebuggerInTestMode: Boolean; + { Private declarations } + public + { Public declarations } + procedure SelectDebugSystemKeyboard(k: TSystemKeyboardItem); + end; + +var + modActionsDebugger: TmodActionsDebugger; + +implementation + +uses + Vcl.Forms, + Vcl.Menus, + System.Win.ComObj, + Winapi.Windows, + + KMDActions, + KMDActionInterfaces, + Keyman.System.Debug.DebugUIStatus, + Keyman.Developer.System.Project.kmnProjectFile, + Keyman.Developer.UI.Project.kmnProjectFileUI, + UfrmKeymanWizard, + UfrmDebug, + UfrmDebugStatus, + UfrmMain; + +{$R *.dfm} + +function ActiveKmnKeyboardEditor: TfrmKeymanWizard; +begin + if Assigned(frmKeymanDeveloper.ActiveChild) and (frmKeymanDeveloper.ActiveChild is TfrmKeymanWizard) + then Result := frmKeymanDeveloper.ActiveChild as TfrmKeymanWizard + else Result := nil; +end; + +function ActiveKmnKeyboardProjectFile: TkmnProjectFile; +begin + Result := ActiveKmnKeyboardEditor.ProjectFile as TkmnProjectFile; +end; + +function ActiveMemo: IKMDTextEditorActions; +begin + Result := GetTextEditorController(Screen.ActiveControl); +end; + +procedure TmodActionsDebugger.actionsKeyboardEditorUpdate(Action: TBasicAction; var Handled: Boolean); +begin + with Action as TAction do + begin + if not Assigned(Action.OnUpdate) then + begin + Enabled := ActiveKmnKeyboardEditor <> nil; + if not Enabled then Handled := True; + end; + end; +end; + +{ ---- Debug Menu ---- } + +procedure TmodActionsDebugger.actDebugDebuggerModeExecute(Sender: TObject); +begin + if ActiveKmnKeyboardEditor.DebugForm.UIStatus = duiTest then + ActiveKmnKeyboardEditor.DebugForm.UIStatus := duiReadyForInput; +end; + +procedure TmodActionsDebugger.actDebugDebuggerModeUpdate(Sender: TObject); +begin + actDebugDebuggerMode.Checked := (ActiveKmnKeyboardEditor <> nil) and (ActiveKmnKeyboardEditor.DebugForm.UIStatus <> duiTest); +end; + +procedure TmodActionsDebugger.actDebugPauseExecute(Sender: TObject); +begin + if ActiveKmnKeyboardEditor.DebugForm.UIStatus = duiPaused + then ActiveKmnKeyboardEditor.DebugForm.Unpause + else ActiveKmnKeyboardEditor.DebugForm.Pause; +end; + +procedure TmodActionsDebugger.actDebugPauseUpdate(Sender: TObject); +begin + actDebugPause.Enabled := + (ActiveKmnKeyboardEditor <> nil) and + IsDebuggerVisible and + not IsDebuggerInTestMode and + (ActiveKmnKeyboardEditor.DebugForm.UIStatus in [duiPaused, duiFocusedForInput, duiReadyForInput]); + + if actDebugPause.Enabled + then actDebugPause.Checked := ActiveKmnKeyboardEditor.DebugForm.UIStatus = duiPaused + else actDebugPause.Checked := False; +end; + +procedure TmodActionsDebugger.actDebugRunExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugForm.Run; +end; + +procedure TmodActionsDebugger.actDebugRunUpdate(Sender: TObject); +begin + actDebugRun.Enabled := IsDebuggerVisible and not IsDebuggerInTestMode and + (ActiveKmnKeyboardEditor.DebugForm.UIStatus = duiDebugging); +end; + +procedure TmodActionsDebugger.actDebugSelectSystemKeyboardExecute(Sender: TObject); +var + FKeyboardID: string; +begin + if SelectSystemKeyboard(frmKeymanDeveloper, FKeyboardID) then + begin + // TODO: this is disabled? + //ActiveKmnKeyboardEditor.DebugForm.SetSystemKeyboardID(FKeyboardID); // I3655 + end; +end; + +procedure TmodActionsDebugger.actDebugSelectSystemKeyboardUpdate(Sender: TObject); +begin + actDebugSelectSystemKeyboard.Enabled := IsDebuggerVisible; +end; + +procedure TmodActionsDebugger.actDebugSetClearBreakpointExecute(Sender: TObject); +begin + if ActiveKmnKeyboardEditor.DebugForm.IsBreakPointLine(ActiveMemo.SelectedRow) + then ActiveKmnKeyboardEditor.DebugForm.ClearBreakpoint(ActiveMemo.SelectedRow) + else ActiveKmnKeyboardEditor.DebugForm.SetBreakpoint(ActiveMemo.SelectedRow); +end; + +procedure TmodActionsDebugger.actDebugSetClearBreakpointUpdate(Sender: TObject); +begin + if (ActiveKmnKeyboardEditor <> nil) and (ActiveMemo <> nil) and Assigned(ActiveKmnKeyboardEditor.DebugForm) then + begin + actDebugSetClearBreakpoint.Enabled := True; + actDebugSetClearBreakpoint.Visible := True; + if ActiveKmnKeyboardEditor.DebugForm.IsBreakPointLine(ActiveMemo.SelectedRow) + then actDebugSetClearBreakpoint.Caption := 'Clear &Breakpoint' + else actDebugSetClearBreakpoint.Caption := 'Set &Breakpoint'; + end + else + begin + actDebugSetClearBreakpoint.Enabled := False; + actDebugSetClearBreakpoint.Visible := False; + end; +end; + +procedure TmodActionsDebugger.actDebugSingleStepModeExecute( + Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugForm.SingleStepMode := not ActiveKmnKeyboardEditor.DebugForm.SingleStepMode; +end; + +procedure TmodActionsDebugger.actDebugSingleStepModeUpdate(Sender: TObject); +begin + actDebugSingleStepMode.Enabled := IsDebuggerVisible and not IsDebuggerInTestMode; + if actDebugSingleStepMode.Enabled + then actDebugSingleStepMode.Checked := ActiveKmnKeyboardEditor.DebugForm.SingleStepMode + else actDebugSingleStepMode.Checked := False; +end; + +procedure TmodActionsDebugger.actDebugStartDebuggerExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.StartDebugging; +end; + +procedure TmodActionsDebugger.actDebugStartDebuggerUpdate(Sender: TObject); +begin + actDebugStartDebugger.Enabled := (ActiveKmnKeyboardEditor <> nil) and not ActiveKmnKeyboardEditor.IsDebugVisible; +end; + +procedure TmodActionsDebugger.actDebugStepForwardExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugForm.StepForward; +end; + +procedure TmodActionsDebugger.actDebugStepForwardUpdate(Sender: TObject); +begin + actDebugStepForward.Enabled := IsDebuggerVisible and not IsDebuggerInTestMode and + (ActiveKmnKeyboardEditor.DebugForm.UIStatus = duiDebugging); +end; + +procedure TmodActionsDebugger.actDebugStopDebuggerExecute( + Sender: TObject); +begin + ActiveKmnKeyboardEditor.StopDebugging; +end; + +procedure TmodActionsDebugger.actDebugStopDebuggerUpdate(Sender: TObject); +begin + actDebugStopDebugger.Enabled := (ActiveKmnKeyboardEditor <> nil) and ActiveKmnKeyboardEditor.IsDebugVisible; +end; + +procedure TmodActionsDebugger.actDebugSwitchToDebuggerWindowExecute( + Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugForm.SetFocus; +end; + +procedure TmodActionsDebugger.actDebugSwitchToDebuggerWindowUpdate( + Sender: TObject); +begin + actDebugSwitchToDebuggerWindow.Enabled := (ActiveKmnKeyboardEditor <> nil) and ActiveKmnKeyboardEditor.IsDebugVisible and + ((Screen.ActiveControl = nil) or (Screen.ActiveControl.Owner <> ActiveKmnKeyboardEditor.DebugForm)); +end; + +procedure TmodActionsDebugger.actDebugTestModeExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugForm.UIStatus := duiTest; +end; + +procedure TmodActionsDebugger.actDebugTestModeUpdate(Sender: TObject); +begin + actDebugTestMode.Enabled := (ActiveKmnKeyboardEditor <> nil); + actDebugTestMode.Checked := actDebugTestMode.Enabled and (ActiveKmnKeyboardEditor.DebugForm.UIStatus = duiTest); +end; + +procedure TmodActionsDebugger.actDebugViewCallStackExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugStatusForm.Visible := True; + ActiveKmnKeyboardEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveKmnKeyboardEditor.DebugStatusForm.tabDebugCallStack; +end; + +procedure TmodActionsDebugger.actDebugViewCallStackUpdate( + Sender: TObject); +begin + actDebugViewCallStack.Enabled := (ActiveKmnKeyboardEditor <> nil) and ActiveKmnKeyboardEditor.IsDebugVisible; +end; + +procedure TmodActionsDebugger.actDebugViewDeadkeysExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugStatusForm.Visible := True; + ActiveKmnKeyboardEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveKmnKeyboardEditor.DebugStatusForm.tabDebugDeadkeys; +end; + +procedure TmodActionsDebugger.actDebugViewDeadkeysUpdate(Sender: TObject); +begin + actDebugViewDeadkeys.Enabled := (ActiveKmnKeyboardEditor <> nil) and ActiveKmnKeyboardEditor.IsDebugVisible; +end; + +procedure TmodActionsDebugger.actDebugViewDefaultFontExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugForm.UpdateFont(nil); +end; + +procedure TmodActionsDebugger.actDebugViewDefaultFontUpdate(Sender: TObject); +begin + actDebugViewDefaultFont.Enabled := IsDebuggerVisible; + actDebugViewDefaultFont.Checked := actDebugViewDefaultFont.Enabled and + ActiveKmnKeyboardEditor.DebugForm.DefaultFont; +end; + +procedure TmodActionsDebugger.actDebugViewFontExecute(Sender: TObject); +begin + dlgFont.Font := ActiveKmnKeyboardEditor.DebugForm.memo.Font; + if dlgFont.Execute then + ActiveKmnKeyboardEditor.DebugForm.UpdateFont(dlgFont.Font); +end; + +procedure TmodActionsDebugger.actDebugViewFontUpdate(Sender: TObject); +begin + actDebugViewFont.Enabled := IsDebuggerVisible; + actDebugViewFont.Checked := actDebugViewFont.Enabled and not ActiveKmnKeyboardEditor.DebugForm.DefaultFont; +end; + +procedure TmodActionsDebugger.actDebugViewRegressionTestingExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugStatusForm.Visible := True; + ActiveKmnKeyboardEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveKmnKeyboardEditor.DebugStatusForm.tabDebugRegressionTesting; +end; + +procedure TmodActionsDebugger.actDebugViewRegressionTestingUpdate(Sender: TObject); +begin + actDebugViewRegressionTesting.Enabled := (ActiveKmnKeyboardEditor <> nil) and ActiveKmnKeyboardEditor.IsDebugVisible; +end; + +procedure TmodActionsDebugger.actDebugViewStateExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugStatusForm.Visible := True; + ActiveKmnKeyboardEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveKmnKeyboardEditor.DebugStatusForm.tabDebugKey; +end; + +procedure TmodActionsDebugger.actDebugViewStateUpdate(Sender: TObject); +begin + actDebugViewState.Enabled := (ActiveKmnKeyboardEditor <> nil) and ActiveKmnKeyboardEditor.IsDebugVisible; +end; + +procedure TmodActionsDebugger.actDebugViewElementsExecute(Sender: TObject); +begin + ActiveKmnKeyboardEditor.DebugStatusForm.Visible := True; + ActiveKmnKeyboardEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveKmnKeyboardEditor.DebugStatusForm.tabDebugStores; +end; + +procedure TmodActionsDebugger.actDebugViewElementsUpdate(Sender: TObject); +begin + actDebugViewElements.Enabled := (ActiveKmnKeyboardEditor <> nil) and ActiveKmnKeyboardEditor.IsDebugVisible; +end; + +{ ---- Keyboard Menu ---- } + +function TmodActionsDebugger.IsDebuggerInTestMode: Boolean; +begin + Result := (ActiveKmnKeyboardEditor <> nil) and (ActiveKmnKeyboardEditor.DebugForm.UIStatus = duiTest); +end; + +function TmodActionsDebugger.IsDebuggerVisible: Boolean; +begin + Result := (ActiveKmnKeyboardEditor <> nil) and ActiveKmnKeyboardEditor.IsDebugVisible; +end; + +procedure TmodActionsDebugger.SelectDebugSystemKeyboard(k: TSystemKeyboardItem); +begin +//TODO: #1225, #1074 (note both menu item and toolbar combo are hidden while this is not working) +// if IsDebuggerVisible then // I3655 +// ActiveKmnKeyboardEditor.DebugForm.SetSystemKeyboardID(k.KeyboardID); +end; + +end. diff --git a/developer/src/tike/actions/dmActionsKeyboardEditor.dfm b/developer/src/tike/actions/dmActionsKeyboardEditor.dfm index 3a2c010099c..be0f4097d5a 100644 --- a/developer/src/tike/actions/dmActionsKeyboardEditor.dfm +++ b/developer/src/tike/actions/dmActionsKeyboardEditor.dfm @@ -4,7 +4,6 @@ object modActionsKeyboardEditor: TmodActionsKeyboardEditor Width = 408 object actionsKeyboardEditor: TActionList Images = frmKeymanDeveloper.lstImages - OnUpdate = actionsKeyboardEditorUpdate Left = 60 Top = 24 object actKeyboardCompile: TAction @@ -27,176 +26,38 @@ object modActionsKeyboardEditor: TmodActionsKeyboardEditor ImageIndex = 41 ShortCut = 16504 OnExecute = actKeyboardInstallExecute + OnUpdate = actKeyboardInstallUpdate end object actKeyboardUninstall: TAction Category = 'Keyboard' Caption = '&Uninstall...' ImageIndex = 42 OnExecute = actKeyboardUninstallExecute + OnUpdate = actKeyboardUninstallUpdate end object actKeyboardTest: TAction Category = 'Keyboard' Caption = '&Test Keyboard' OnExecute = actKeyboardTestExecute + OnUpdate = actKeyboardTestUpdate end object actKeyboardTestKeymanWeb: TAction Category = 'Keyboard' Caption = 'Test &Keyboard on web' OnExecute = actKeyboardTestKeymanWebExecute + OnUpdate = actKeyboardTestKeymanWebUpdate end object actKeyboardFontHelper: TAction Category = 'Keyboard' Caption = 'Font &Helper...' OnExecute = actKeyboardFontHelperExecute + OnUpdate = actKeyboardFontHelperUpdate end object actKeyboardFonts: TAction Category = 'Keyboard' Caption = '&Fonts...' OnExecute = actKeyboardFontsExecute + OnUpdate = actKeyboardFontsUpdate end end - object actionsDebug: TActionList - Images = frmKeymanDeveloper.lstImages - Left = 60 - Top = 88 - object actDebugStartDebugger: TAction - Category = 'Debug Control' - Caption = '&Start Debugging' - ImageIndex = 44 - ShortCut = 116 - OnExecute = actDebugStartDebuggerExecute - OnUpdate = actDebugStartDebuggerUpdate - end - object actDebugStopDebugger: TAction - Category = 'Debug Control' - Caption = 'Stop Debugger' - ImageIndex = 45 - ShortCut = 8308 - OnExecute = actDebugStopDebuggerExecute - OnUpdate = actDebugStopDebuggerUpdate - end - object actDebugSetClearBreakpoint: TAction - Category = 'Debug Control' - Caption = 'Set/Clear &Breakpoint' - ImageIndex = 43 - ShortCut = 120 - OnExecute = actDebugSetClearBreakpointExecute - OnUpdate = actDebugSetClearBreakpointUpdate - end - object actDebugSingleStepMode: TAction - Category = 'Debug Action' - Caption = 'Single Step &Mode' - ImageIndex = 46 - OnExecute = actDebugSingleStepModeExecute - OnUpdate = actDebugSingleStepModeUpdate - end - object actDebugStepForward: TAction - Category = 'Debug Action' - Caption = 'Step &Forward' - ImageIndex = 47 - OnExecute = actDebugStepForwardExecute - OnUpdate = actDebugStepForwardUpdate - end - object actDebugRun: TAction - Category = 'Debug Action' - Caption = '&Run' - ImageIndex = 48 - OnExecute = actDebugRunExecute - OnUpdate = actDebugRunUpdate - end - object actDebugSelectSystemKeyboard: TAction - Category = 'Debug Control' - Caption = 'Select System &Keyboard...' - ImageIndex = 49 - Visible = False - OnExecute = actDebugSelectSystemKeyboardExecute - OnUpdate = actDebugSelectSystemKeyboardUpdate - end - object actDebugANSITestMode: TAction - Category = 'Debug Control' - Caption = '&ANSI Test Mode' - end - object actDebugViewElements: TAction - Category = 'Debug Control' - Caption = '&Elements' - ImageIndex = 50 - OnExecute = actDebugViewElementsExecute - OnUpdate = actDebugViewElementsUpdate - end - object actDebugViewCallStack: TAction - Category = 'Debug Control' - Caption = '&Call Stack' - ImageIndex = 51 - OnExecute = actDebugViewCallStackExecute - OnUpdate = actDebugViewCallStackUpdate - end - object actDebugViewDeadkeys: TAction - Category = 'Debug Control' - Caption = '&Deadkeys' - ImageIndex = 52 - OnExecute = actDebugViewDeadkeysExecute - OnUpdate = actDebugViewDeadkeysUpdate - end - object actDebugViewRegressionTesting: TAction - Category = 'Debug Control' - Caption = '&Regression Testing' - OnExecute = actDebugViewRegressionTestingExecute - OnUpdate = actDebugViewRegressionTestingUpdate - end - object actDebugTestMode: TAction - Category = 'Debug Control' - Caption = '&Test Mode' - GroupIndex = 1 - OnExecute = actDebugTestModeExecute - OnUpdate = actDebugTestModeUpdate - end - object actDebugDebuggerMode: TAction - Category = 'Debug Control' - Caption = '&Debugger Mode' - GroupIndex = 2 - OnExecute = actDebugDebuggerModeExecute - OnUpdate = actDebugDebuggerModeUpdate - end - object actDebugPause: TAction - Category = 'Debug Action' - Caption = '&Pause' - ImageIndex = 57 - OnExecute = actDebugPauseExecute - OnUpdate = actDebugPauseUpdate - end - object actDebugViewFont: TAction - Category = 'Debug Control' - Caption = '&Font...' - OnExecute = actDebugViewFontExecute - OnUpdate = actDebugViewFontUpdate - end - object actDebugViewDefaultFont: TAction - Category = 'Debug Control' - Caption = 'Use Editor &Character Font' - OnExecute = actDebugViewDefaultFontExecute - OnUpdate = actDebugViewDefaultFontUpdate - end - object actDebugViewState: TAction - Category = 'Debug Control' - Caption = '&State' - OnExecute = actDebugViewStateExecute - OnUpdate = actDebugViewStateUpdate - end - object actDebugSwitchToDebuggerWindow: TAction - Category = 'Debug Control' - Caption = 'Switch to debugger' - ShortCut = 117 - OnExecute = actDebugSwitchToDebuggerWindowExecute - OnUpdate = actDebugSwitchToDebuggerWindowUpdate - end - end - object dlgFont: TFontDialog - Font.Charset = DEFAULT_CHARSET - Font.Color = clWindowText - Font.Height = -11 - Font.Name = 'MS Sans Serif' - Font.Style = [] - Left = 256 - Top = 73 - end end diff --git a/developer/src/tike/actions/dmActionsKeyboardEditor.pas b/developer/src/tike/actions/dmActionsKeyboardEditor.pas index 4f6434d6937..33bcc399a95 100644 --- a/developer/src/tike/actions/dmActionsKeyboardEditor.pas +++ b/developer/src/tike/actions/dmActionsKeyboardEditor.pas @@ -40,8 +40,14 @@ interface uses - SysUtils, Classes, ActnList, - UfrmSelectSystemKeyboard, ImgList, Controls, Dialogs, System.Actions; + SysUtils, + Classes, + ActnList, + UfrmSelectSystemKeyboard, + ImgList, + Controls, + Dialogs, + System.Actions; type TmodActionsKeyboardEditor = class(TDataModule) @@ -50,86 +56,26 @@ TmodActionsKeyboardEditor = class(TDataModule) actKeyboardIncludeDebugInformation: TAction; actKeyboardInstall: TAction; actKeyboardUninstall: TAction; - actionsDebug: TActionList; - actDebugStartDebugger: TAction; - actDebugSetClearBreakpoint: TAction; - actDebugStopDebugger: TAction; - actDebugSingleStepMode: TAction; - actDebugStepForward: TAction; - actDebugRun: TAction; - actDebugSelectSystemKeyboard: TAction; - actDebugANSITestMode: TAction; - actDebugViewElements: TAction; - actDebugViewCallStack: TAction; - actDebugViewDeadkeys: TAction; - actDebugViewRegressionTesting: TAction; - actDebugTestMode: TAction; - actDebugDebuggerMode: TAction; - actDebugPause: TAction; - actDebugViewFont: TAction; - actDebugViewDefaultFont: TAction; - dlgFont: TFontDialog; actKeyboardTest: TAction; actKeyboardTestKeymanWeb: TAction; - actDebugViewState: TAction; actKeyboardFontHelper: TAction; actKeyboardFonts: TAction; - actDebugSwitchToDebuggerWindow: TAction; procedure actKeyboardCompileExecute(Sender: TObject); - procedure actionsKeyboardEditorUpdate(Action: TBasicAction; - var Handled: Boolean); procedure actKeyboardIncludeDebugInformationExecute(Sender: TObject); - procedure actDebugViewElementsExecute(Sender: TObject); - procedure actDebugSetClearBreakpointExecute(Sender: TObject); - procedure actDebugSetClearBreakpointUpdate(Sender: TObject); - procedure actDebugStartDebuggerExecute(Sender: TObject); - procedure actDebugStartDebuggerUpdate(Sender: TObject); - procedure actDebugStopDebuggerExecute(Sender: TObject); - procedure actDebugStopDebuggerUpdate(Sender: TObject); procedure actKeyboardInstallExecute(Sender: TObject); procedure actKeyboardUninstallExecute(Sender: TObject); procedure actKeyboardIncludeDebugInformationUpdate(Sender: TObject); - procedure actDebugSingleStepModeUpdate(Sender: TObject); - procedure actDebugSingleStepModeExecute(Sender: TObject); - procedure actDebugStepForwardUpdate(Sender: TObject); - procedure actDebugStepForwardExecute(Sender: TObject); - procedure actDebugRunUpdate(Sender: TObject); - procedure actDebugRunExecute(Sender: TObject); - procedure actDebugSelectSystemKeyboardExecute(Sender: TObject); - procedure actDebugDebuggerModeExecute(Sender: TObject); - procedure actDebugTestModeExecute(Sender: TObject); - procedure actDebugTestModeUpdate(Sender: TObject); - procedure actDebugDebuggerModeUpdate(Sender: TObject); - procedure actDebugPauseUpdate(Sender: TObject); - procedure actDebugPauseExecute(Sender: TObject); - procedure actDebugSelectSystemKeyboardUpdate(Sender: TObject); - procedure actDebugViewDefaultFontExecute(Sender: TObject); - procedure actDebugViewDefaultFontUpdate(Sender: TObject); - procedure actDebugViewFontExecute(Sender: TObject); - procedure actDebugViewFontUpdate(Sender: TObject); procedure actKeyboardTestExecute(Sender: TObject); procedure actKeyboardTestKeymanWebExecute(Sender: TObject); - procedure actDebugViewCallStackExecute(Sender: TObject); - procedure actDebugViewDeadkeysExecute(Sender: TObject); - procedure actDebugViewRegressionTestingExecute(Sender: TObject); - procedure actDebugViewElementsUpdate(Sender: TObject); - procedure actDebugViewCallStackUpdate(Sender: TObject); - procedure actDebugViewDeadkeysUpdate(Sender: TObject); - procedure actDebugViewRegressionTestingUpdate(Sender: TObject); - procedure actDebugViewStateExecute(Sender: TObject); - procedure actDebugViewStateUpdate(Sender: TObject); procedure actKeyboardFontHelperExecute(Sender: TObject); procedure actKeyboardFontsExecute(Sender: TObject); procedure actKeyboardCompileUpdate(Sender: TObject); - procedure actDebugSwitchToDebuggerWindowUpdate(Sender: TObject); - procedure actDebugSwitchToDebuggerWindowExecute(Sender: TObject); - private - function IsDebuggerVisible: Boolean; - function IsDebuggerInTestMode: Boolean; - { Private declarations } - public - { Public declarations } - procedure SelectDebugSystemKeyboard(k: TSystemKeyboardItem); + procedure actKeyboardInstallUpdate(Sender: TObject); + procedure actKeyboardUninstallUpdate(Sender: TObject); + procedure actKeyboardTestUpdate(Sender: TObject); + procedure actKeyboardTestKeymanWebUpdate(Sender: TObject); + procedure actKeyboardFontHelperUpdate(Sender: TObject); + procedure actKeyboardFontsUpdate(Sender: TObject); end; var @@ -153,10 +99,10 @@ implementation Keyman.Developer.System.Project.ProjectFile, Keyman.Developer.UI.Project.ProjectFileUI, Keyman.Developer.UI.Project.UfrmProject, + Keyman.Developer.UI.UfrmLdmlKeyboardEditor, + Keyman.Developer.System.Project.xmlLdmlProjectFile, UframeTextEditor, UfrmKeymanWizard, - UfrmDebug, - UfrmDebugStatus, UfrmMessages, UfrmMain, UfrmPackageEditor, @@ -164,7 +110,14 @@ implementation {$R *.dfm} -function ActiveEditor: TfrmKeymanWizard; +function ActiveLdmlKeyboardEditor: TfrmLdmlKeyboardEditor; +begin + if Assigned(frmKeymanDeveloper.ActiveChild) and (frmKeymanDeveloper.ActiveChild is TfrmLdmlKeyboardEditor) + then Result := frmKeymanDeveloper.ActiveChild as TfrmLdmlKeyboardEditor + else Result := nil; +end; + +function ActiveKmnKeyboardEditor: TfrmKeymanWizard; begin if Assigned(frmKeymanDeveloper.ActiveChild) and (frmKeymanDeveloper.ActiveChild is TfrmKeymanWizard) then Result := frmKeymanDeveloper.ActiveChild as TfrmKeymanWizard @@ -178,363 +131,189 @@ function ActivePackageEditor: TfrmPackageEditor; else Result := nil; end; -function ActiveProjectFile: TkmnProjectFile; -begin - Result := ActiveEditor.ProjectFile as TkmnProjectFile; -end; - -function ActivePackageProjectFile: TkpsProjectFile; -begin - Result := ActivePackageEditor.ProjectFile as TkpsProjectFile; -end; - -function ActiveMemo: IKMDTextEditorActions; +function ActiveLdmlKeyboardProjectFile: TxmlLdmlProjectFile; begin - Result := GetTextEditorController(Screen.ActiveControl); + if ActiveLdmlKeyboardEditor = nil + then Result := nil + else Result := ActiveLdmlKeyboardEditor.ProjectFile as TxmlLdmlProjectFile; end; -procedure TmodActionsKeyboardEditor.actionsKeyboardEditorUpdate(Action: TBasicAction; var Handled: Boolean); +function ActiveKmnKeyboardProjectFile: TkmnProjectFile; begin - with Action as TAction do - begin - if not Assigned(Action.OnUpdate) then - begin - Enabled := ActiveEditor <> nil; - if not Enabled then Handled := True; - end; - end; + if ActiveKmnKeyboardEditor = nil + then Result := nil + else Result := ActiveKmnKeyboardEditor.ProjectFile as TkmnProjectFile; end; -{ ---- Debug Menu ---- } - -procedure TmodActionsKeyboardEditor.actDebugDebuggerModeExecute(Sender: TObject); +function ActivePackageProjectFile: TkpsProjectFile; begin - if ActiveEditor.DebugForm.UIStatus = duiTest then - ActiveEditor.DebugForm.UIStatus := duiReadyForInput; + if ActivePackageEditor = nil + then Result := nil + else Result := ActivePackageEditor.ProjectFile as TkpsProjectFile; end; -procedure TmodActionsKeyboardEditor.actDebugDebuggerModeUpdate(Sender: TObject); +function ActiveProjectFileUI: TProjectFileUI; begin - actDebugDebuggerMode.Checked := (ActiveEditor <> nil) and (ActiveEditor.DebugForm.UIStatus <> duiTest); + if ActiveKmnKeyboardProjectFile <> nil then + Result := ActiveKmnKeyboardProjectFile.UI as TProjectFileUI + else if ActiveLdmlKeyboardProjectFile <> nil then + Result := ActiveLdmlKeyboardProjectFile.UI as TProjectFileUI + else if ActivePackageProjectFile <> nil then + Result := ActivePackageProjectFile.UI as TProjectFileUI + else + Result := nil; end; -procedure TmodActionsKeyboardEditor.actDebugPauseExecute(Sender: TObject); -begin - if ActiveEditor.DebugForm.UIStatus = duiPaused - then ActiveEditor.DebugForm.Unpause - else ActiveEditor.DebugForm.Pause; -end; +{ ---- Keyboard Menu ---- } -procedure TmodActionsKeyboardEditor.actDebugPauseUpdate(Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardCompileUpdate(Sender: TObject); begin - actDebugPause.Enabled := - (ActiveEditor <> nil) and - IsDebuggerVisible and - not IsDebuggerInTestMode and - (ActiveEditor.DebugForm.UIStatus in [duiPaused, duiFocusedForInput, duiReadyForInput]); - - if actDebugPause.Enabled - then actDebugPause.Checked := ActiveEditor.DebugForm.UIStatus = duiPaused - else actDebugPause.Checked := False; -end; + // TODO: Split Keyboard menu and package editor functions + actKeyboardCompile.Enabled := + (ActiveKmnKeyboardEditor <> nil) or + (ActiveLdmlKeyboardEditor <> nil) or + (ActivePackageEditor <> nil) or + ((frmKeymanDeveloper.ActiveChild is TfrmProject) and (FGlobalProject <> nil)); -procedure TmodActionsKeyboardEditor.actDebugRunExecute(Sender: TObject); -begin - ActiveEditor.DebugForm.Run; -end; + if actKeyboardCompile.Enabled + then actKeyboardCompile.ShortCut := Vcl.Menus.Shortcut(VK_F7, []) + else actKeyboardCompile.ShortCut := scNone; -procedure TmodActionsKeyboardEditor.actDebugRunUpdate(Sender: TObject); -begin - actDebugRun.Enabled := IsDebuggerVisible and not IsDebuggerInTestMode and - (ActiveEditor.DebugForm.UIStatus = duiDebugging); + // This is a little side-effecty + frmKeymanDeveloper.mnuKeyboard.Visible := True; + frmKeymanDeveloper.mnuDebug.Visible := ActiveKmnKeyboardEditor <> nil; end; -procedure TmodActionsKeyboardEditor.actDebugSelectSystemKeyboardExecute(Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardCompileExecute(Sender: TObject); // I4504 var - FKeyboardID: string; + DebugReset: Boolean; begin - if SelectSystemKeyboard(frmKeymanDeveloper, FKeyboardID) then - begin - //ActiveEditor.DebugForm.SetSystemKeyboardID(FKeyboardID); // I3655 - end; -end; + DebugReset := False; -procedure TmodActionsKeyboardEditor.actDebugSelectSystemKeyboardUpdate(Sender: TObject); -begin - actDebugSelectSystemKeyboard.Enabled := IsDebuggerVisible; -end; + frmMessages.Clear; // I4686 -procedure TmodActionsKeyboardEditor.actDebugSetClearBreakpointExecute(Sender: TObject); -begin - if ActiveEditor.DebugForm.IsBreakPointLine(ActiveMemo.SelectedRow) - then ActiveEditor.DebugForm.ClearBreakpoint(ActiveMemo.SelectedRow) - else ActiveEditor.DebugForm.SetBreakpoint(ActiveMemo.SelectedRow); -end; + if ActiveKmnKeyboardEditor <> nil then + begin + if not ActiveKmnKeyboardEditor.PrepareForBuild(DebugReset) then + Exit; -procedure TmodActionsKeyboardEditor.actDebugSetClearBreakpointUpdate(Sender: TObject); -begin - if (ActiveEditor <> nil) and (ActiveMemo <> nil) and Assigned(ActiveEditor.DebugForm) then + if ActiveProjectFileUI.DoAction(pfaCompile, False) and DebugReset then // I4686 + ActiveKmnKeyboardEditor.StartDebugging; + end + else if ActiveLdmlKeyboardEditor <> nil then begin - actDebugSetClearBreakpoint.Enabled := True; - actDebugSetClearBreakpoint.Visible := True; - if ActiveEditor.DebugForm.IsBreakPointLine(ActiveMemo.SelectedRow) - then actDebugSetClearBreakpoint.Caption := 'Clear &Breakpoint' - else actDebugSetClearBreakpoint.Caption := 'Set &Breakpoint'; + ActiveProjectFileUI.DoAction(pfaCompile, False); end - else + else if ActivePackageEditor <> nil then + begin + ActiveProjectFileUI.DoAction(pfaCompile, False); + end + else if frmKeymanDeveloper.ActiveChild is TfrmProject then begin - actDebugSetClearBreakpoint.Enabled := False; - actDebugSetClearBreakpoint.Visible := False; + (frmKeymanDeveloper.ActiveChild as TfrmProject).CompileAll; end; end; -procedure TmodActionsKeyboardEditor.actDebugSingleStepModeExecute( - Sender: TObject); -begin - ActiveEditor.DebugForm.SingleStepMode := not ActiveEditor.DebugForm.SingleStepMode; -end; - -procedure TmodActionsKeyboardEditor.actDebugSingleStepModeUpdate(Sender: TObject); -begin - actDebugSingleStepMode.Enabled := IsDebuggerVisible and not IsDebuggerInTestMode; - if actDebugSingleStepMode.Enabled - then actDebugSingleStepMode.Checked := ActiveEditor.DebugForm.SingleStepMode - else actDebugSingleStepMode.Checked := False; -end; - -procedure TmodActionsKeyboardEditor.actDebugStartDebuggerExecute(Sender: TObject); -begin - ActiveEditor.StartDebugging; -end; - -procedure TmodActionsKeyboardEditor.actDebugStartDebuggerUpdate(Sender: TObject); -begin - actDebugStartDebugger.Enabled := (ActiveEditor <> nil) and not ActiveEditor.IsDebugVisible; -end; - -procedure TmodActionsKeyboardEditor.actDebugStepForwardExecute(Sender: TObject); -begin - ActiveEditor.DebugForm.StepForward; -end; - -procedure TmodActionsKeyboardEditor.actDebugStepForwardUpdate(Sender: TObject); -begin - actDebugStepForward.Enabled := IsDebuggerVisible and not IsDebuggerInTestMode and - (ActiveEditor.DebugForm.UIStatus = duiDebugging); -end; +//------------------------------------------------------------------------------ -procedure TmodActionsKeyboardEditor.actDebugStopDebuggerExecute( +procedure TmodActionsKeyboardEditor.actKeyboardFontHelperUpdate( Sender: TObject); begin - ActiveEditor.StopDebugging; -end; - -procedure TmodActionsKeyboardEditor.actDebugStopDebuggerUpdate(Sender: TObject); -begin - actDebugStopDebugger.Enabled := (ActiveEditor <> nil) and ActiveEditor.IsDebugVisible; + actKeyboardFontHelper.Enabled := (ActiveKmnKeyboardProjectFile <> nil); end; -procedure TmodActionsKeyboardEditor.actDebugSwitchToDebuggerWindowExecute( +procedure TmodActionsKeyboardEditor.actKeyboardFontHelperExecute( Sender: TObject); begin - ActiveEditor.DebugForm.SetFocus; + ActiveProjectFileUI.DoAction(pfaFontHelper, False) // I4687 end; -procedure TmodActionsKeyboardEditor.actDebugSwitchToDebuggerWindowUpdate( - Sender: TObject); -begin - actDebugSwitchToDebuggerWindow.Enabled := (ActiveEditor <> nil) and ActiveEditor.IsDebugVisible and - ((Screen.ActiveControl = nil) or (Screen.ActiveControl.Owner <> ActiveEditor.DebugForm)); -end; +//------------------------------------------------------------------------------ -procedure TmodActionsKeyboardEditor.actDebugTestModeExecute(Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardFontsUpdate(Sender: TObject); begin - ActiveEditor.DebugForm.UIStatus := duiTest; + actKeyboardFonts.Enabled := (ActiveKmnKeyboardProjectFile <> nil); end; -procedure TmodActionsKeyboardEditor.actDebugTestModeUpdate(Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardFontsExecute(Sender: TObject); // I4057 begin - actDebugTestMode.Enabled := (ActiveEditor <> nil); - actDebugTestMode.Checked := actDebugTestMode.Enabled and (ActiveEditor.DebugForm.UIStatus = duiTest); + ActiveProjectFileUI.DoAction(pfaFontDialog, False); // I4687 end; -procedure TmodActionsKeyboardEditor.actDebugViewCallStackExecute(Sender: TObject); -begin - ActiveEditor.DebugStatusForm.Visible := True; - ActiveEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveEditor.DebugStatusForm.tabDebugCallStack; -end; +//------------------------------------------------------------------------------ -procedure TmodActionsKeyboardEditor.actDebugViewCallStackUpdate( +procedure TmodActionsKeyboardEditor.actKeyboardIncludeDebugInformationUpdate( Sender: TObject); begin - actDebugViewCallStack.Enabled := (ActiveEditor <> nil) and ActiveEditor.IsDebugVisible; -end; - -procedure TmodActionsKeyboardEditor.actDebugViewDeadkeysExecute(Sender: TObject); -begin - ActiveEditor.DebugStatusForm.Visible := True; - ActiveEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveEditor.DebugStatusForm.tabDebugDeadkeys; -end; - -procedure TmodActionsKeyboardEditor.actDebugViewDeadkeysUpdate(Sender: TObject); -begin - actDebugViewDeadkeys.Enabled := (ActiveEditor <> nil) and ActiveEditor.IsDebugVisible; -end; - -procedure TmodActionsKeyboardEditor.actDebugViewDefaultFontExecute(Sender: TObject); -begin - ActiveEditor.DebugForm.UpdateFont(nil); -end; - -procedure TmodActionsKeyboardEditor.actDebugViewDefaultFontUpdate(Sender: TObject); -begin - actDebugViewDefaultFont.Enabled := IsDebuggerVisible; - actDebugViewDefaultFont.Checked := actDebugViewDefaultFont.Enabled and - ActiveEditor.DebugForm.DefaultFont; -end; - -procedure TmodActionsKeyboardEditor.actDebugViewFontExecute(Sender: TObject); -begin - dlgFont.Font := ActiveEditor.DebugForm.memo.Font; - if dlgFont.Execute then - ActiveEditor.DebugForm.UpdateFont(dlgFont.Font); + actKeyboardIncludeDebugInformation.Enabled := ActiveKmnKeyboardEditor <> nil; + if (ActiveKmnKeyboardEditor <> nil) and (ActiveKmnKeyboardProjectFile <> nil) + then actKeyboardIncludeDebugInformation.Checked := (ActiveProjectFileUI as TkmnProjectFileUI).Debug // I4687 + else actKeyboardIncludeDebugInformation.Checked := False; end; -procedure TmodActionsKeyboardEditor.actDebugViewFontUpdate(Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardIncludeDebugInformationExecute(Sender: TObject); begin - actDebugViewFont.Enabled := IsDebuggerVisible; - actDebugViewFont.Checked := actDebugViewFont.Enabled and not ActiveEditor.DebugForm.DefaultFont; + (ActiveProjectFileUI as TkmnProjectFileUI).Debug := not (ActiveProjectFileUI as TkmnProjectFileUI).Debug; // I4687 end; -procedure TmodActionsKeyboardEditor.actDebugViewRegressionTestingExecute(Sender: TObject); -begin - ActiveEditor.DebugStatusForm.Visible := True; - ActiveEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveEditor.DebugStatusForm.tabDebugRegressionTesting; -end; +//------------------------------------------------------------------------------ -procedure TmodActionsKeyboardEditor.actDebugViewRegressionTestingUpdate(Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardInstallUpdate(Sender: TObject); begin - actDebugViewRegressionTesting.Enabled := (ActiveEditor <> nil) and ActiveEditor.IsDebugVisible; + actKeyboardInstall.Enabled := ActiveProjectFileUI <> nil; end; -procedure TmodActionsKeyboardEditor.actDebugViewStateExecute(Sender: TObject); -begin - ActiveEditor.DebugStatusForm.Visible := True; - ActiveEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveEditor.DebugStatusForm.tabDebugKey; -end; - -procedure TmodActionsKeyboardEditor.actDebugViewStateUpdate(Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardInstallExecute(Sender: TObject); begin - actDebugViewState.Enabled := (ActiveEditor <> nil) and ActiveEditor.IsDebugVisible; + try + ActiveProjectFileUI.DoAction(pfaInstall, False); // I4687 + except + on E:EOleException do // I654 + begin + ShowMessage(E.Message); + end; + end; end; -procedure TmodActionsKeyboardEditor.actDebugViewElementsExecute(Sender: TObject); -begin - ActiveEditor.DebugStatusForm.Visible := True; - ActiveEditor.DebugStatusForm.pagesDebug.ActivePage := ActiveEditor.DebugStatusForm.tabDebugStores; -end; +//------------------------------------------------------------------------------ -procedure TmodActionsKeyboardEditor.actDebugViewElementsUpdate(Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardTestUpdate(Sender: TObject); begin - actDebugViewElements.Enabled := (ActiveEditor <> nil) and ActiveEditor.IsDebugVisible; + actKeyboardTest.Enabled := (ActiveKmnKeyboardEditor <> nil); + // TODO: test xml keyboards in editor end; -{ ---- Keyboard Menu ---- } - -procedure TmodActionsKeyboardEditor.actKeyboardCompileExecute(Sender: TObject); // I4504 -var - DebugReset: Boolean; +procedure TmodActionsKeyboardEditor.actKeyboardTestExecute(Sender: TObject); begin - DebugReset := False; - - frmMessages.Clear; // I4686 - - if ActiveEditor <> nil then - begin - if not ActiveEditor.PrepareForBuild(DebugReset) then - Exit; - - if (ActiveProjectFile.UI as TProjectFileUI).DoAction(pfaCompile, False) and DebugReset then // I4686 - ActiveEditor.StartDebugging; - end - else if ActivePackageEditor <> nil then - begin - (ActivePackageProjectFile.UI as TProjectFileUI).DoAction(pfaCompile, False); - end - else if frmKeymanDeveloper.ActiveChild is TfrmProject then - (frmKeymanDeveloper.ActiveChild as TfrmProject).CompileAll; + ActiveKmnKeyboardEditor.DebugForm.UIStatus := duiTest; + ActiveKmnKeyboardEditor.StartDebugging(True); end; -procedure TmodActionsKeyboardEditor.actKeyboardCompileUpdate(Sender: TObject); -begin - // TODO: Split Keyboard menu and package editor functions - actKeyboardCompile.Enabled := - (ActiveEditor <> nil) or - (ActivePackageEditor <> nil) or - ((frmKeymanDeveloper.ActiveChild is TfrmProject) and (FGlobalProject <> nil)); - - if actKeyboardCompile.Enabled - then actKeyboardCompile.ShortCut := Vcl.Menus.Shortcut(VK_F7, []) - else actKeyboardCompile.ShortCut := scNone; - - frmKeymanDeveloper.mnuKeyboard.Visible := True; - frmKeymanDeveloper.mnuDebug.Visible := ActiveEditor <> nil; -end; +//------------------------------------------------------------------------------ -procedure TmodActionsKeyboardEditor.actKeyboardFontHelperExecute( +procedure TmodActionsKeyboardEditor.actKeyboardTestKeymanWebUpdate( Sender: TObject); begin - (ActiveProjectFile.UI as TProjectFileUI).DoAction(pfaFontHelper, False) // I4687 + actKeyboardTestKeymanWeb.Enabled := (ActiveKmnKeyboardEditor <> nil); end; -procedure TmodActionsKeyboardEditor.actKeyboardFontsExecute(Sender: TObject); // I4057 -begin - (ActiveProjectFile.UI as TProjectFileUI).DoAction(pfaFontDialog, False); // I4687 -end; - -procedure TmodActionsKeyboardEditor.actKeyboardIncludeDebugInformationExecute(Sender: TObject); -begin - (ActiveProjectFile.UI as TkmnProjectFileUI).Debug := not (ActiveProjectFile.UI as TkmnProjectFileUI).Debug; // I4687 -end; - -procedure TmodActionsKeyboardEditor.actKeyboardIncludeDebugInformationUpdate( +procedure TmodActionsKeyboardEditor.actKeyboardTestKeymanWebExecute( Sender: TObject); begin - actKeyboardIncludeDebugInformation.Enabled := ActiveEditor <> nil; - if (ActiveEditor <> nil) and (ActiveProjectFile <> nil) - then actKeyboardIncludeDebugInformation.Checked := (ActiveProjectFile.UI as TkmnProjectFileUI).Debug // I4687 - else actKeyboardIncludeDebugInformation.Checked := False; -end; - -procedure TmodActionsKeyboardEditor.actKeyboardInstallExecute(Sender: TObject); -begin - try - (ActiveProjectFile.UI as TProjectFileUI).DoAction(pfaInstall, False); // I4687 - //WideShowMessage('Keyboard installed successfully.'); - except - on E:EOleException do // I654 - begin - ShowMessage(E.Message); - end; - end; + ActiveProjectFileUI.DoAction(pfaTestKeymanWeb, False); // I4687 end; -procedure TmodActionsKeyboardEditor.actKeyboardTestExecute(Sender: TObject); -begin - ActiveEditor.DebugForm.UIStatus := duiTest; - ActiveEditor.StartDebugging(True); -end; +//------------------------------------------------------------------------------ -procedure TmodActionsKeyboardEditor.actKeyboardTestKeymanWebExecute( - Sender: TObject); +procedure TmodActionsKeyboardEditor.actKeyboardUninstallUpdate(Sender: TObject); begin - (ActiveProjectFile.UI as TProjectFileUI).DoAction(pfaTestKeymanWeb, False); // I4687 + actKeyboardUninstall.Enabled := ActiveProjectFileUI <> nil; end; procedure TmodActionsKeyboardEditor.actKeyboardUninstallExecute(Sender: TObject); begin try - if (ActiveProjectFile.UI as TProjectFileUI).DoAction(pfaUninstall, False) + if ActiveProjectFileUI.DoAction(pfaUninstall, False) then ShowMessage('Keyboard uninstalled successfully.') else ShowMessage('Failed to uninstall keyboard.'); except @@ -545,21 +324,4 @@ procedure TmodActionsKeyboardEditor.actKeyboardUninstallExecute(Sender: TObject) end; end; -function TmodActionsKeyboardEditor.IsDebuggerInTestMode: Boolean; -begin - Result := (ActiveEditor <> nil) and (ActiveEditor.DebugForm.UIStatus = duiTest); -end; - -function TmodActionsKeyboardEditor.IsDebuggerVisible: Boolean; -begin - Result := (ActiveEditor <> nil) and ActiveEditor.IsDebugVisible; -end; - -procedure TmodActionsKeyboardEditor.SelectDebugSystemKeyboard(k: TSystemKeyboardItem); -begin -//TODO: #1225, #1074 (note both menu item and toolbar combo are hidden while this is not working) -// if IsDebuggerVisible then // I3655 -// ActiveEditor.DebugForm.SetSystemKeyboardID(k.KeyboardID); -end; - end. diff --git a/developer/src/tike/actions/dmActionsMain.dfm b/developer/src/tike/actions/dmActionsMain.dfm index 8b53150dfe5..09890f90613 100644 --- a/developer/src/tike/actions/dmActionsMain.dfm +++ b/developer/src/tike/actions/dmActionsMain.dfm @@ -32,6 +32,17 @@ object modActionsMain: TmodActionsMain OnExecute = actViewCharacterIdentifierExecute OnUpdate = actViewCharacterIdentifierUpdate end + object actProjectOpenFolder: TBrowseForFolder + Category = 'Project' + Caption = 'Open Project Folder...' + DialogCaption = 'Open Project Folder' + BrowseOptions = [] + BrowseOptionsEx = [] + Hint = 'Open Project Folder|Opens an existing project folder' + ShortCut = 24655 + UseFileDialog = True + OnAccept = actProjectOpenFolderAccept + end object actFileOpen: TFileOpen Category = 'File' Caption = '&Open...' @@ -248,22 +259,8 @@ object modActionsMain: TmodActionsMain Dialog.Options = [ofHideReadOnly, ofPathMustExist, ofFileMustExist, ofEnableSizing] Hint = 'Open Project|Opens an existing project' ImageIndex = 28 - ShortCut = 16463 OnAccept = actProjectOpenAccept end - object actProjectSaveAs: TFileSaveAs - Category = 'Project' - Caption = 'Save Project &As...' - Dialog.DefaultExt = 'kpj' - Dialog.Filter = 'Keyman Developer Project Files (*.kpj)|*.kpj|All Files (*.*)|*.*' - Dialog.Options = [ofOverwritePrompt, ofHideReadOnly, ofPathMustExist, ofEnableSizing] - Dialog.Title = 'Save Project As' - Dialog.OnCanClose = actProjectSaveAsSaveDialogCanClose - Hint = 'Save Project As|Saves the current project with a new name' - BeforeExecute = actProjectSaveAsBeforeExecute - OnAccept = actProjectSaveAsAccept - OnUpdate = actProjectSaveAsUpdate - end object actProjectAddCurrentEditorFile: TAction Category = 'Project' Caption = '&Current Editor File' diff --git a/developer/src/tike/actions/dmActionsMain.pas b/developer/src/tike/actions/dmActionsMain.pas index f13b57e29f1..2230d89c58b 100644 --- a/developer/src/tike/actions/dmActionsMain.pas +++ b/developer/src/tike/actions/dmActionsMain.pas @@ -102,7 +102,6 @@ TmodActionsMain = class(TDataModule) actViewStatusBar: TAction; actProjectNew: TAction; actProjectOpen: TFileOpen; - actProjectSaveAs: TFileSaveAs; actProjectAddCurrentEditorFile: TAction; actProjectAddFiles: TFileOpen; actProjectSettings: TAction; @@ -139,6 +138,7 @@ TmodActionsMain = class(TDataModule) actToolsWebConfigure: TAction; actToolsWebStartServer: TAction; actToolsWebStopServer: TAction; + actProjectOpenFolder: TBrowseForFolder; procedure actFileNewExecute(Sender: TObject); procedure DataModuleCreate(Sender: TObject); procedure actFileOpenAccept(Sender: TObject); @@ -169,11 +169,9 @@ TmodActionsMain = class(TDataModule) procedure actToolsOptionsExecute(Sender: TObject); procedure actToolsVirtualKeyIdentifierExecute(Sender: TObject); procedure actProjectNewExecute(Sender: TObject); - procedure actProjectSaveAsBeforeExecute(Sender: TObject); procedure actProjectAddCurrentEditorFileExecute(Sender: TObject); procedure actProjectSettingsExecute(Sender: TObject); procedure actProjectOpenAccept(Sender: TObject); - procedure actProjectSaveAsAccept(Sender: TObject); procedure actProjectAddFilesAccept(Sender: TObject); procedure actProjectAddCurrentEditorFileUpdate(Sender: TObject); procedure actHelpContentsExecute(Sender: TObject); @@ -224,12 +222,9 @@ TmodActionsMain = class(TDataModule) procedure actViewCharacterIdentifierExecute(Sender: TObject); // I4807 procedure actViewCharacterIdentifierUpdate(Sender: TObject); procedure actFileSaveAsSaveDialogCanClose(Sender: TObject; - var CanClose: Boolean); - procedure actProjectSaveAsSaveDialogCanClose(Sender: TObject; var CanClose: Boolean); // I4807 procedure actProjectCloseExecute(Sender: TObject); procedure actProjectCloseUpdate(Sender: TObject); - procedure actProjectSaveAsUpdate(Sender: TObject); procedure actProjectAddFilesUpdate(Sender: TObject); procedure actProjectSettingsUpdate(Sender: TObject); procedure actFileNewUpdate(Sender: TObject); @@ -243,12 +238,12 @@ TmodActionsMain = class(TDataModule) procedure actToolsWebStartServerUpdate(Sender: TObject); procedure actToolsWebStopServerExecute(Sender: TObject); procedure actToolsWebStopServerUpdate(Sender: TObject); + procedure actProjectOpenFolderAccept(Sender: TObject); private function CheckFilenameConventions(FileName: string): Boolean; function SaveAndCloseAllFiles: Boolean; procedure CloseProject; public - procedure NewProject(pt: TProjectType); procedure OpenProject(FileName: WideString); end; @@ -270,10 +265,14 @@ implementation Keyman.System.KeyboardUtils, Keyman.Developer.System.Project.Project, Keyman.Developer.System.Project.ProjectFileType, + Keyman.Developer.System.Project.ProjectLoader, Keyman.Developer.System.ServerAPI, Keyman.Developer.UI.Project.ProjectFileUI, Keyman.Developer.UI.Project.ProjectUI, Keyman.Developer.UI.Project.UfrmNewProject, + Keyman.Developer.UI.Project.UfrmProject, + Keyman.Developer.UI.Project.UfrmProjectSettings, + Keyman.Developer.UI.Project.UfrmProjectSettings20, GlobalProxySettings, RegistryKeys, TextFileFormat, @@ -296,8 +295,6 @@ implementation UfrmOSKEditor, UfrmPackageEditor, UmodWebHttpServer, - Keyman.Developer.UI.Project.UfrmProject, - Keyman.Developer.UI.Project.UfrmProjectSettings, Upload_Settings, utilexecute, UfrmMDIChild; @@ -540,8 +537,12 @@ procedure TmodActionsMain.actProjectAddCurrentEditorFileExecute( procedure TmodActionsMain.actProjectAddCurrentEditorFileUpdate(Sender: TObject); begin + actProjectAddCurrentEditorFile.Visible := + not IsGlobalProjectUIReady or + (FGlobalProject.Options.Version = pv10); actProjectAddCurrentEditorFile.Enabled := IsGlobalProjectUIReady and + (FGlobalProject.Options.Version = pv10) and Assigned(frmKeymanDeveloper.ActiveEditor) and not frmKeymanDeveloper.ActiveEditor.Untitled and (not Assigned(frmKeymanDeveloper.ActiveEditor.ProjectFile) or @@ -561,7 +562,11 @@ procedure TmodActionsMain.actProjectAddFilesAccept(Sender: TObject); procedure TmodActionsMain.actProjectAddFilesUpdate(Sender: TObject); begin - actProjectAddFiles.Enabled := IsGlobalProjectUIReady; + actProjectAddFiles.Visible := + not IsGlobalProjectUIReady or + (FGlobalProject.Options.Version = pv10); + actProjectAddFiles.Enabled := IsGlobalProjectUIReady and + (FGlobalProject.Options.Version = pv10); end; procedure TmodActionsMain.actProjectCloseExecute(Sender: TObject); @@ -588,10 +593,17 @@ procedure TmodActionsMain.actProjectOpenAccept(Sender: TObject); OpenProject(actProjectOpen.Dialog.FileName); end; +procedure TmodActionsMain.actProjectOpenFolderAccept(Sender: TObject); +begin + if not frmKeymanDeveloper.BeforeOpenProject then + Exit; + OpenProject(actProjectOpenFolder.Folder); +end; + procedure TmodActionsMain.OpenProject(FileName: WideString); begin FileName := ExpandUNCFileName(FileName); - if (FileName <> '') and not FileExists(FileName) then + if (FileName <> '') and not FileExists(FileName) and not DirectoryExists(FileName) then begin ShowMessage('The project '+FileName+' does not exist.'); Exit; @@ -602,20 +614,19 @@ procedure TmodActionsMain.OpenProject(FileName: WideString); if not SaveAndCloseAllFiles then Exit; FreeGlobalProjectUI; end; - LoadGlobalProjectUI(ptUnknown, FileName); // I4687 - frmKeymanDeveloper.ProjectMRU.Add(FGlobalProject.FileName); - frmKeymanDeveloper.ShowProject; - frmKeymanDeveloper.UpdateCaption; -end; - -procedure TmodActionsMain.NewProject(pt: TProjectType); -begin - if IsGlobalProjectUIReady then - begin - if not SaveAndCloseAllFiles then Exit; - FreeGlobalProjectUI; + try + LoadGlobalProjectUI(ptUnknown, FileName); // I4687 + except + on E:EProjectLoader do + begin + // Message will be displayed by LoadGlobalProjectUI + FreeGlobalProjectUI; + frmKeymanDeveloper.ShowProject; + frmKeymanDeveloper.UpdateCaption; + Exit; + end; end; - NewGlobalProjectUI(pt); + frmKeymanDeveloper.ProjectMRU.Add(FGlobalProject.FileName); frmKeymanDeveloper.ShowProject; frmKeymanDeveloper.UpdateCaption; end; @@ -631,36 +642,17 @@ procedure TmodActionsMain.CloseProject; frmKeymanDeveloper.UpdateCaption; end; -procedure TmodActionsMain.actProjectSaveAsAccept(Sender: TObject); -begin - FGlobalProject.FileName := actProjectSaveAs.Dialog.FileName; - FGlobalProject.Save; - frmKeymanDeveloper.ProjectMRU.Add(FGlobalProject.FileName); -end; - -procedure TmodActionsMain.actProjectSaveAsBeforeExecute(Sender: TObject); -begin - actProjectSaveAs.Dialog.FileName := FGlobalProject.FileName; -end; - -procedure TmodActionsMain.actProjectSaveAsSaveDialogCanClose(Sender: TObject; - var CanClose: Boolean); -begin - CanClose := CheckFilenameConventions((Sender as TSaveDialog).FileName); -end; - -procedure TmodActionsMain.actProjectSaveAsUpdate(Sender: TObject); -begin - actProjectSaveAs.Enabled := IsGlobalProjectUIReady; -end; - procedure TmodActionsMain.actProjectSettingsExecute(Sender: TObject); +var + frm: TForm; begin - with TfrmProjectSettings.Create(Screen.ActiveForm) do // I4688 + if FGlobalProject.Options.Version = pv10 + then frm := TfrmProjectSettings.Create(Screen.ActiveForm) // I4688 + else frm := TfrmProjectSettings20.Create(Screen.ActiveForm); try - ShowModal; + frm.ShowModal; finally - Free; + frm.Free; end; end; diff --git a/developer/src/tike/child/Keyman.Developer.UI.UfrmLdmlKeyboardEditor.dfm b/developer/src/tike/child/Keyman.Developer.UI.UfrmLdmlKeyboardEditor.dfm new file mode 100644 index 00000000000..9420597382e --- /dev/null +++ b/developer/src/tike/child/Keyman.Developer.UI.UfrmLdmlKeyboardEditor.dfm @@ -0,0 +1,12 @@ +inherited frmLdmlKeyboardEditor: TfrmLdmlKeyboardEditor + Left = 0 + Top = 0 + Caption = 'frmLdmlKeyboardEditor' + ClientHeight = 247 + ClientWidth = 480 + Font.Name = 'Tahoma' + ExplicitWidth = 480 + ExplicitHeight = 247 + PixelsPerInch = 96 + TextHeight = 13 +end diff --git a/developer/src/tike/child/Keyman.Developer.UI.UfrmLdmlKeyboardEditor.pas b/developer/src/tike/child/Keyman.Developer.UI.UfrmLdmlKeyboardEditor.pas new file mode 100644 index 00000000000..e000dabbbac --- /dev/null +++ b/developer/src/tike/child/Keyman.Developer.UI.UfrmLdmlKeyboardEditor.pas @@ -0,0 +1,33 @@ +unit Keyman.Developer.UI.UfrmLdmlKeyboardEditor; + +interface + +uses + System.Classes, + System.ImageList, + System.SysUtils, + System.Variants, + Vcl.Controls, + Vcl.Dialogs, + Vcl.Forms, + Vcl.Graphics, + Vcl.ImgList, + Winapi.Messages, + Winapi.Windows, + + MenuImgList, + UfrmEditor; + +type + TfrmLdmlKeyboardEditor = class(TfrmEditor) + private + { Private declarations } + public + { Public declarations } + end; + +implementation + +{$R *.dfm} + +end. diff --git a/developer/src/tike/child/UfrmDebug.dfm b/developer/src/tike/child/UfrmDebug.dfm index 1d8bca104b9..362488d5f33 100644 --- a/developer/src/tike/child/UfrmDebug.dfm +++ b/developer/src/tike/child/UfrmDebug.dfm @@ -67,10 +67,10 @@ inherited frmDebug: TfrmDebug Left = 228 Top = 73 object mnuPopupFont: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewFont + Action = modActionsDebugger.actDebugViewFont end object mnuPopupResetToQuotedFont: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewDefaultFont + Action = modActionsDebugger.actDebugViewDefaultFont end object N1: TMenuItem Caption = '-' diff --git a/developer/src/tike/child/UfrmDebug.pas b/developer/src/tike/child/UfrmDebug.pas index c6a04b45017..1d271e62a05 100644 --- a/developer/src/tike/child/UfrmDebug.pas +++ b/developer/src/tike/child/UfrmDebug.pas @@ -207,7 +207,7 @@ implementation Keyman.Developer.System.HelpTopics, ActiveX, - dmActionsKeyboardEditor, + dmActionsDebugger, dmActionsMain, Glossary, Keyman.Developer.System.Project.ProjectLog, @@ -1386,7 +1386,7 @@ procedure TfrmDebug.UpdateCharacterGrid; // I4808 procedure TfrmDebug.cmdStopClick(Sender: TObject); begin - modActionsKeyboardEditor.actDebugStopDebugger.Execute; + modActionsDebugger.actDebugStopDebugger.Execute; end; { TDebugBreakpoint } diff --git a/developer/src/tike/child/UfrmKeymanWizard.dfm b/developer/src/tike/child/UfrmKeymanWizard.dfm index 13d710fed6e..e3e6dcffc89 100644 --- a/developer/src/tike/child/UfrmKeymanWizard.dfm +++ b/developer/src/tike/child/UfrmKeymanWizard.dfm @@ -1338,7 +1338,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Top = 33 Width = 137 Height = 25 - Action = modActionsKeyboardEditor.actDebugStartDebugger + Action = modActionsDebugger.actDebugStartDebugger TabOrder = 1 end object cmdCompile: TButton diff --git a/developer/src/tike/child/UfrmKeymanWizard.pas b/developer/src/tike/child/UfrmKeymanWizard.pas index 9d511388d66..a4a276e145d 100644 --- a/developer/src/tike/child/UfrmKeymanWizard.pas +++ b/developer/src/tike/child/UfrmKeymanWizard.pas @@ -132,6 +132,7 @@ interface UframeTextEditor, UfrmDebug, ExtShiftState, ImgList, MenuImgList, KeyboardParser, TextFileFormat, dmActionsKeyboardEditor, + dmActionsDebugger, VisualKeyboard, UframeOnScreenKeyboardEditor, KeymanDeveloperUtils, OnScreenKeyboard, KMDActionInterfaces, diff --git a/developer/src/tike/dialogs/UfrmNew.pas b/developer/src/tike/dialogs/UfrmNew.pas index ecb0adea702..e00ed26fb74 100644 --- a/developer/src/tike/dialogs/UfrmNew.pas +++ b/developer/src/tike/dialogs/UfrmNew.pas @@ -217,9 +217,7 @@ procedure TfrmNew.FormCreate(Sender: TObject); i: Integer; begin inherited; - if FGlobalProject.Untitled - then FRootPath := FKeymanDeveloperOptions.DefaultProjectPath - else FRootPath := ExtractFilePath(FGlobalProject.FileName); + FRootPath := ExtractFilePath(FGlobalProject.FileName); lvItems.Selected := lvItems.Items[0]; lvItems.ItemFocused := lvItems.Items[0]; diff --git a/developer/src/tike/http/Keyman.Developer.System.HttpServer.App.pas b/developer/src/tike/http/Keyman.Developer.System.HttpServer.App.pas index eef85cd3af7..1af9aafb976 100644 --- a/developer/src/tike/http/Keyman.Developer.System.HttpServer.App.pas +++ b/developer/src/tike/http/Keyman.Developer.System.HttpServer.App.pas @@ -104,7 +104,7 @@ procedure TAppHttpResponder.RespondProject(doc: string; AContext: TIdContext; path := CrackUTF8ZeroExtendedString(ARequestInfo.CommandType, ARequestInfo.Params.Values['path']); - if (Path <> '') and (not FileExists(path) or not SameText(ExtractFileExt(path), Ext_ProjectSource)) then + if (Path <> '') and (not DirectoryExists(ExtractFileDir(path)) or not SameText(ExtractFileExt(path), Ext_ProjectSource)) then begin AResponseInfo.ResponseNo := 404; AResponseInfo.ResponseText := 'Project file '+path+' does not exist.'; @@ -191,7 +191,7 @@ procedure TAppHttpResponder.RespondProject(doc: string; AContext: TIdContext; // Saving state - if (Path <> '') and (not FileExists(path) or not SameText(ExtractFileExt(path), Ext_ProjectSource)) then + if (Path <> '') and (not DirectoryExists(ExtractFileDir(path)) or not SameText(ExtractFileExt(path), Ext_ProjectSource)) then begin AResponseInfo.ResponseNo := 404; AResponseInfo.ResponseText := 'Project file '+path+' does not exist.'; diff --git a/developer/src/tike/main/UfrmMain.dfm b/developer/src/tike/main/UfrmMain.dfm index 436cc9d2477..311c1fd67d5 100644 --- a/developer/src/tike/main/UfrmMain.dfm +++ b/developer/src/tike/main/UfrmMain.dfm @@ -75,7 +75,7 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper Top = 0 Width = 82 Height = 22 - Action = modActionsKeyboardEditor.actDebugTestMode + Action = modActionsDebugger.actDebugTestMode AllowAllUp = True GroupIndex = 1 end @@ -84,7 +84,7 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper Top = 0 Width = 98 Height = 22 - Action = modActionsKeyboardEditor.actDebugDebuggerMode + Action = modActionsDebugger.actDebugDebuggerMode AllowAllUp = True GroupIndex = 2 end @@ -120,7 +120,7 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper object ToolButton21: TToolButton Left = 8 Top = 0 - Action = modActionsKeyboardEditor.actDebugStopDebugger + Action = modActionsDebugger.actDebugStopDebugger end object ToolButton23: TToolButton Left = 31 @@ -133,7 +133,7 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper object ToolButton22: TToolButton Left = 39 Top = 0 - Action = modActionsKeyboardEditor.actDebugSingleStepMode + Action = modActionsDebugger.actDebugSingleStepMode end object ToolButton24: TToolButton Left = 62 @@ -146,17 +146,17 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper object ToolButton25: TToolButton Left = 70 Top = 0 - Action = modActionsKeyboardEditor.actDebugStepForward + Action = modActionsDebugger.actDebugStepForward end object ToolButton26: TToolButton Left = 93 Top = 0 - Action = modActionsKeyboardEditor.actDebugRun + Action = modActionsDebugger.actDebugRun end object ToolButton27: TToolButton Left = 116 Top = 0 - Action = modActionsKeyboardEditor.actDebugPause + Action = modActionsDebugger.actDebugPause end object ToolButton28: TToolButton Left = 139 @@ -169,7 +169,7 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper object ToolButton29: TToolButton Left = 147 Top = 0 - Action = modActionsKeyboardEditor.actDebugSetClearBreakpoint + Action = modActionsDebugger.actDebugSetClearBreakpoint end end end @@ -2935,8 +2935,8 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper object OpenProject1: TMenuItem Action = modActionsMain.actProjectOpen end - object SaveProjectAs1: TMenuItem - Action = modActionsMain.actProjectSaveAs + object OpenProjectFolder1: TMenuItem + Action = modActionsMain.actProjectOpenFolder end object CloseProject1: TMenuItem Action = modActionsMain.actProjectClose @@ -2950,7 +2950,7 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper object N26: TMenuItem Caption = '-' end - object Addtoproject1: TMenuItem + object mnuProjectAddToProject: TMenuItem Caption = 'A&dd to project' object CurrentEditorFile1: TMenuItem Action = modActionsMain.actProjectAddCurrentEditorFile @@ -3014,40 +3014,40 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper object mnuDebug: TMenuItem Caption = '&Debug' object SetBreakpoint1: TMenuItem - Action = modActionsKeyboardEditor.actDebugSetClearBreakpoint + Action = modActionsDebugger.actDebugSetClearBreakpoint end object N32: TMenuItem Caption = '-' end object StartDebugging1: TMenuItem - Action = modActionsKeyboardEditor.actDebugStartDebugger + Action = modActionsDebugger.actDebugStartDebugger end object StopDebugger1: TMenuItem - Action = modActionsKeyboardEditor.actDebugStopDebugger + Action = modActionsDebugger.actDebugStopDebugger end object N33: TMenuItem Caption = '-' end object ANSITestMode1: TMenuItem - Action = modActionsKeyboardEditor.actDebugANSITestMode + Action = modActionsDebugger.actDebugANSITestMode end object N34: TMenuItem Caption = '-' end object SingleStepMode1: TMenuItem - Action = modActionsKeyboardEditor.actDebugSingleStepMode + Action = modActionsDebugger.actDebugSingleStepMode end object N35: TMenuItem Caption = '-' end object StepForward1: TMenuItem - Action = modActionsKeyboardEditor.actDebugStepForward + Action = modActionsDebugger.actDebugStepForward end object Run1: TMenuItem - Action = modActionsKeyboardEditor.actDebugRun + Action = modActionsDebugger.actDebugRun end object Pause1: TMenuItem - Action = modActionsKeyboardEditor.actDebugPause + Action = modActionsDebugger.actDebugPause end object N36: TMenuItem Caption = '-' @@ -3055,35 +3055,35 @@ inherited frmKeymanDeveloper: TfrmKeymanDeveloper object View2: TMenuItem Caption = '&View' object State1: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewState + Action = modActionsDebugger.actDebugViewState end object Elements1: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewElements + Action = modActionsDebugger.actDebugViewElements end object CallStack1: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewCallStack + Action = modActionsDebugger.actDebugViewCallStack end object Deadkeys1: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewDeadkeys + Action = modActionsDebugger.actDebugViewDeadkeys end object RegressionTesting1: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewRegressionTesting + Action = modActionsDebugger.actDebugViewRegressionTesting end object N38: TMenuItem Caption = '-' end object Font1: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewFont + Action = modActionsDebugger.actDebugViewFont end object UseEditorCharacterFont1: TMenuItem - Action = modActionsKeyboardEditor.actDebugViewDefaultFont + Action = modActionsDebugger.actDebugViewDefaultFont end end object N37: TMenuItem Caption = '-' end object SelectSystemKeyboard1: TMenuItem - Action = modActionsKeyboardEditor.actDebugSelectSystemKeyboard + Action = modActionsDebugger.actDebugSelectSystemKeyboard end end object mnuTools: TMenuItem diff --git a/developer/src/tike/main/UfrmMain.pas b/developer/src/tike/main/UfrmMain.pas index 352a96c3508..b8b8d526ab9 100644 --- a/developer/src/tike/main/UfrmMain.pas +++ b/developer/src/tike/main/UfrmMain.pas @@ -88,22 +88,52 @@ interface uses System.UITypes, - Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, - ComCtrls, ImgList, Menus, StdCtrls, ExtCtrls, ToolWin, Buttons, - KeymanDeveloperUtils, UfrmMDIChild, UfrmMDIEditor, + Windows, + Messages, + SysUtils, + Classes, + Graphics, + Controls, + Forms, + ComCtrls, + ImgList, + Menus, + StdCtrls, + ExtCtrls, + ToolWin, + Buttons, + KeymanDeveloperUtils, + UfrmMDIChild, + UfrmMDIEditor, MenuImgList, Keyman.Developer.System.Project.Project, - Keyman.Developer.UI.Project.UfrmProject, CharacterMapSettings, + Keyman.Developer.UI.Project.UfrmProject, + CharacterMapSettings, mrulist, UfrmUnicodeDataStatus, CharacterDragObject, Keyman.Developer.UI.dmActionsModelEditor, - dmActionsMain, UnicodeData, UserMessages, webhelp, - dmActionsKeyboardEditor, Dialogs, UfrmTike, AppEvnts, + UnicodeData, + UserMessages, + webhelp, + dmActionsDebugger, + dmActionsKeyboardEditor, + dmActionsMain, + Dialogs, + UfrmTike, + AppEvnts, DropTarget, - System.ImageList, Winapi.ActiveX, CloseButtonPageControl, JvComponentBase, - JvDockControlForm, JvDockTree, JvDockVIDStyle, JvDockVSNetStyle, - JvAppRegistryStorage, Vcl.ActnMan, Vcl.ActnCtrls; + System.ImageList, + Winapi.ActiveX, + CloseButtonPageControl, + JvComponentBase, + JvDockControlForm, + JvDockTree, + JvDockVIDStyle, + JvDockVSNetStyle, + JvAppRegistryStorage, + Vcl.ActnMan, + Vcl.ActnCtrls; type TfrmKeymanDeveloper = class(TTikeForm, IUnicodeDataUIManager, IDragDrop) @@ -208,12 +238,11 @@ TfrmKeymanDeveloper = class(TTikeForm, IUnicodeDataUIManager, IDragDrop) CodeFont1: TMenuItem; NewProject1: TMenuItem; OpenProject1: TMenuItem; - SaveProjectAs1: TMenuItem; N25: TMenuItem; mnuProjectsRecent: TMenuItem; N26: TMenuItem; N27: TMenuItem; - Addtoproject1: TMenuItem; + mnuProjectAddToProject: TMenuItem; CurrentEditorFile1: TMenuItem; OtherFiles1: TMenuItem; ProjectSettings1: TMenuItem; @@ -285,6 +314,7 @@ TfrmKeymanDeveloper = class(TTikeForm, IUnicodeDataUIManager, IDragDrop) Stopserver1: TMenuItem; ToolButton13: TToolButton; ToolButton16: TToolButton; + OpenProjectFolder1: TMenuItem; procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); procedure mnuFileClick(Sender: TObject); @@ -448,9 +478,12 @@ implementation Keyman.Developer.System.Project.ProjectFile, Keyman.Developer.System.Project.ProjectFileType, Keyman.Developer.System.Project.WelcomeRenderer, + Keyman.Developer.System.Project.ProjectLoader, Keyman.Developer.System.Project.ProjectLog, + Keyman.Developer.System.Project.XmlLdmlProjectFile, Keyman.Developer.UI.Project.ProjectFileUI, Keyman.Developer.UI.Project.ProjectUI, + Keyman.Developer.UI.UfrmLdmlKeyboardEditor, Keyman.Developer.UI.UfrmWordlistEditor, Keyman.Developer.UI.UfrmModelEditor, TextFileFormat, @@ -511,6 +544,7 @@ procedure TfrmKeymanDeveloper.FormCreate(Sender: TObject); modActionsTextEditor := TmodActionsTextEditor.Create(Self); modActionsKeyboardEditor := TmodActionsKeyboardEditor.Create(Self); + modActionsDebugger := TmodActionsDebugger.Create(Self); modActionsMain := TmodActionsMain.Create(Self); modActionsModelEditor := TmodActionsModelEditor.Create(Self); @@ -549,10 +583,21 @@ procedure TfrmKeymanDeveloper.FormCreate(Sender: TObject); RemoveOldestTikeTestFonts(False); if (FActiveProject <> '') and not FileExists(FActiveProject) then + // TODO: we need to support folder-based projects here FActiveProject := ''; if FActiveProject <> '' then - LoadGlobalProjectUI(ptUnknown, FActiveProject, True); + begin + try + LoadGlobalProjectUI(ptUnknown, FActiveProject); + except + on E:EProjectLoader do + begin + // Message will be displayed by LoadGlobalProjectUI + FreeGlobalProjectUI; + end; + end; + end; InitDock; @@ -628,10 +673,7 @@ procedure TfrmKeymanDeveloper.DoCloseCleanup; begin if IsGlobalProjectUIReady then begin - if FGlobalProject.Untitled - then FGlobalProject.PersistUntitledProject // I1010: Persist untitled project - else FGlobalProject.Save; // I4691 - + FGlobalProject.Save; // I4691 WriteString(SRegValue_ActiveProject, FGlobalProject.FileName); end else @@ -1024,7 +1066,7 @@ procedure TfrmKeymanDeveloper.cbDebugSystemKeyboardItemClick(Sender: TObject); begin if cbDebugSystemKeyboard.ItemIndex < 0 then cbDebugSystemKeyboard.ItemIndex := 0; - modActionsKeyboardEditor.SelectDebugSystemKeyboard(cbDebugSystemKeyboard.Items.Objects[cbDebugSystemKeyboard.ItemIndex] as TSystemKeyboardItem); + modActionsDebugger.SelectDebugSystemKeyboard(cbDebugSystemKeyboard.Items.Objects[cbDebugSystemKeyboard.ItemIndex] as TSystemKeyboardItem); //actDebugSystemKeyboard end; @@ -1133,8 +1175,16 @@ function TfrmKeymanDeveloper.OpenFile(FFileName: string; FCloseNewFile: Boolean) begin if not IsGlobalProjectUIReady then begin + // TODO: we need to open the parent folder as a project, if possible // This can happen if we get a file opened via Explorer. - modActionsMain.NewProject(ptKeyboard); + ShowMessage('TODO -- open parent folder as project'); + Exit(nil); + end; + + if DirectoryExists(FFileName) then + begin + // This is an attempt to open a project folder? + // TODO end; Result := nil; @@ -1163,6 +1213,8 @@ function TfrmKeymanDeveloper.OpenFile(FFileName: string; FCloseNewFile: Boolean) 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 (ext = '.xml') and TxmlLdmlProjectFile.IsFileTypeSupported(FFileName) then + Result := OpenEditor(FFileName, TfrmLdmlKeyboardEditor) else if FileHasModelTsExt(FFileName) then Result := OpenModelEditor(FFileName) else Result := OpenEditor(FFileName, TfrmEditor); @@ -1400,6 +1452,9 @@ procedure TfrmKeymanDeveloper.mnuProjectClick(Sender: TObject); end; mnuProjectsRecent.Enabled := FProjectMRU.FileCount > 0; + + mnuProjectAddToProject.Visible := not IsGlobalProjectUIReady or + (FGlobalProject.Options.Version = pv10); end; procedure TfrmKeymanDeveloper.mnuProjectRecentFileClick(Sender: TObject); @@ -1494,9 +1549,8 @@ procedure TfrmKeymanDeveloper.UpdateCaption; begin if not IsGlobalProjectUIReady then Caption := 'Keyman Developer' - else if FGlobalProject.Untitled - then Caption := '(Untitled project) - Keyman Developer' - else Caption := ChangeFileExt(ExtractFileName(FGlobalProject.FileName), '') + ' - Keyman Developer'; + else + Caption := ChangeFileExt(ExtractFileName(FGlobalProject.FileName), '') + ' - Keyman Developer'; end; procedure TfrmKeymanDeveloper.UpdateChildCaption(Window: TfrmTikeChild); diff --git a/developer/src/tike/main/UfrmMessages.pas b/developer/src/tike/main/UfrmMessages.pas index 21d11fe6921..ade6b1d1b8b 100644 --- a/developer/src/tike/main/UfrmMessages.pas +++ b/developer/src/tike/main/UfrmMessages.pas @@ -182,6 +182,12 @@ procedure TfrmMessages.memoMessageDblClick(Sender: TObject); mi := FMessageItems[line] as TMessageItem; FFilename := mi.FileName; + if FFileName = FGlobalProject.FileName then + begin + frmKeymanDeveloper.ShowProject; + Exit; + end; + frm := frmKeymanDeveloper.FindEditorByFileName(FFileName); if not Assigned(frm) then begin diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas index 66b858a3ba7..e5ddbf9e316 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas @@ -82,6 +82,68 @@ interface TempFileManager, utilfiletypes; +type + TProjectState = (psCreating, psReady, psLoading, psSaving, psDestroying); + TProjectType = (ptUnknown, ptKeyboard, ptLexicalModel); // distinct from utilfiletypes.TKeymanProjectType + + TProjectVersion = (pvUnknown, pv10, pv20); + + TProjectOptionsRecord = record + BuildPath: string; + SourcePath: string; + CompilerWarningsAsErrors: Boolean; + WarnDeprecatedCode: Boolean; + CheckFilenameConventions: Boolean; + SkipMetadataFiles: Boolean; + ProjectType: TProjectType; + Version: TProjectVersion; + end; + + TProjectOptions = class + BuildPath: string; + SourcePath: string; + CompilerWarningsAsErrors: Boolean; + WarnDeprecatedCode: Boolean; + CheckFilenameConventions: Boolean; + SkipMetadataFiles: Boolean; + ProjectType: TProjectType; + Version: TProjectVersion; + public + procedure Assign(source: TProjectOptions); overload; + procedure Assign(source: TProjectOptionsRecord); overload; + function EqualsRecord(source: TProjectOptionsRecord): Boolean; + end; + +const DefaultProjectOptions: array[TProjectVersion] of TProjectOptionsRecord = ( +( // unknown version, this is unused + BuildPath: ''; + SourcePath: ''; + CompilerWarningsAsErrors: False; + WarnDeprecatedCode: False; + CheckFilenameConventions: False; + SkipMetadatafiles: False; + ProjectType: ptKeyboard; + Version: pvUnknown +), ( // 1.0 + BuildPath: ''; + SourcePath: ''; + CompilerWarningsAsErrors: False; + WarnDeprecatedCode: True; + CheckFilenameConventions: False; + SkipMetadatafiles: True; + ProjectType: ptKeyboard; + Version: pv10 +), ( // 2.0 + BuildPath: '$PROJECTPATH/build'; + SourcePath: '$PROJECTPATH/source'; + CompilerWarningsAsErrors: False; + WarnDeprecatedCode: True; + CheckFilenameConventions: False; + SkipMetadatafiles: False; + ProjectType: ptKeyboard; + Version: pv20 +)); + type { Forward declarations } @@ -90,10 +152,6 @@ interface TProjectFileList = class; TProjectFile = class; TProjectFileStates = class; - TProjectOptions = class; - - TProjectState = (psCreating, psReady, psLoading, psSaving, psDestroying); - TProjectType = (ptUnknown, ptKeyboard, ptLexicalModel); // distinct from utilfiletypes.TKeymanProjectType { TProject } @@ -111,51 +169,55 @@ TProject = class FDisplayState: WideString; FMRU: TMRUList; FOptions: TProjectOptions; + FUpgradeMessages: TStrings; - procedure SetFileName(Value: string); function ImportFromIni(FileName: string): Boolean; function LoadFromXML(FileName: string): Boolean; function ExpandMemberFileName(Root, FileName: string): string; - function GetSavedFileName: string; - function GetUntitled: Boolean; //procedure ChildRefresh(Sender: TObject); procedure ListNotify(Item: TProjectFile; Action: TListNotification); procedure ChildRefresh(Sender: TObject); procedure MRUChange(Sender: TObject); procedure UpdateFileParameters; - procedure LoadPersistedUntitledProject; - function GetSavedUserFileName: string; + function GetUserFileName: string; + function ResolveProjectPath(APath: string): string; + procedure PopulateFolder(const path: string); + function GetTargetFilename10(ATargetFile, ASourceFile, + AVersion: string): string; + function GetTargetFilename20(ATargetFile, ASourceFile, + AVersion: string): string; protected procedure DoRefresh; virtual; procedure DoRefreshCaption; virtual; property State: TProjectState read FState; - property SavedFileName: string read GetSavedFileName; - property SavedUserFileName: string read GetSavedUserFileName; public procedure Log(AState: TProjectLogState; Filename, Msg: string; MsgCode, line: Integer); virtual; - constructor Create(AProjectType: TProjectType; AFileName: string; ALoadPersistedUntitledProject: Boolean = False); virtual; + constructor Create(AProjectType: TProjectType; AFileName: string); virtual; destructor Destroy; override; procedure Refresh; - procedure PersistUntitledProject; - function Render: WideString; + function IsDefaultProject(Version: TProjectVersion): Boolean; + function Load: Boolean; virtual; // I4694 function Save: Boolean; virtual; // I4694 function SaveUser: Boolean; virtual; // I4694 + function PopulateFiles: Boolean; class function StandardTemplatePath: string; class function StringsTemplatePath: string; - class function GetUntitledProjectFilename(CurrentProcess: Boolean): string; - function GetTargetFilename(ATargetFile, ASourceFile, AVersion: string): string; // I4688 + function CanUpgrade: Boolean; + function Upgrade: Boolean; + property UpgradeMessages: TStrings read FUpgradeMessages; + //procedure AddMRU(const FFileName: string); property MRU: TMRUList read FMRU; @@ -163,9 +225,8 @@ TProject = class function FindFile(AFileName: string): TProjectFile; - //property StandardTemplatePath: string read GetStandardTemplatePath; - property FileName: string read FFileName write SetFileName; - property Untitled: Boolean read GetUntitled; + property FileName: string read FFileName; + property UserFileName: string read GetUserFileName; property Files: TProjectFileList read FFiles; property DisplayState: WideString read FDisplayState write FDisplayState; @@ -214,9 +275,13 @@ TProjectFile = class constructor Create(AProject: TProject; AFileName: string; AParent: TProjectFile); virtual; destructor Destroy; override; - procedure Load(node: IXMLNode; LoadState: Boolean); virtual; // I4698 + function IsCompilable: Boolean; virtual; + function IsSourceFile: Boolean; virtual; + class function IsFileTypeSupported(const Filename: string): Boolean; virtual; + + procedure Load(node: IXMLNode); virtual; // I4698 procedure LoadState(node: IXMLNode); virtual; // I4698 - procedure Save(node: IXMLNode; SaveState: Boolean); virtual; // I4698 + procedure Save(node: IXMLNode); virtual; // I4698 procedure SaveState(node: IXMLNode); virtual; // I4698 procedure AddFreeNotification(AClient: IProjectFileFreeNotification); @@ -288,22 +353,6 @@ TProjectFileStates = class(TObjectList) procedure ProjectFileDestroying(ProjectFile: TProjectFile); end; - TProjectOptions = class // I4688 - private - FBuildPath: string; - FWarnDeprecatedCode: Boolean; // I4866 - FCompilerWarningsAsErrors: Boolean; // I4865 - FCheckFilenameConventions: Boolean; - FProjectType: TProjectType; - public - constructor Create; - property BuildPath: string read FBuildPath write FBuildPath; - property WarnDeprecatedCode: Boolean read FWarnDeprecatedCode write FWarnDeprecatedCode; // I4866 - property CompilerWarningsAsErrors: Boolean read FCompilerWarningsAsErrors write FCompilerWarningsAsErrors; // I4865 - property CheckFilenameConventions: Boolean read FCheckFilenameConventions write FCheckFilenameConventions; - property ProjectType: TProjectType read FProjectType write FProjectType; - end; - const WM_USER_ProjectUpdateDisplayState = WM_USER; @@ -311,6 +360,8 @@ function GlobalProjectStateWndHandle: THandle; function ProjectTypeFromString(s: string): TProjectType; function ProjectTypeToString(pt: TProjectType): string; +function ProjectVersionFromString(s: string): TProjectVersion; +function ProjectVersionToString(pv: TProjectVersion): string; implementation @@ -534,25 +585,53 @@ function TProjectFile.GetOwnerProject: TProject; end; end; -procedure TProjectFile.Load(node: IXMLNode; LoadState: Boolean); // I4698 +function TProjectFile.IsCompilable: Boolean; +begin + Result := False; +end; + +class function TProjectFile.IsFileTypeSupported( + const Filename: string): Boolean; +begin + // assumes that if we are registered for the file type extension, then we can + // handle the file. For example, .xml LDML keyboards + Result := True; +end; + +function TProjectFile.IsSourceFile: Boolean; var - i: Integer; + FilePath, SourcePath: string; +begin + // An file that is a sub-file of another one is never a source file + if Assigned(FParent) then + Exit(False); + + // If no project is assigned, this is an unsupported mode (loading without + // project is no longer supported in 17.0+) but we don't want to crash so + // we'll just treat this as a source file + if not Assigned(FProject) then + Exit(True); + + // We don't restrict builds to sourcepath files for v1.0 projects + if FProject.Options.Version = pv10 then + Exit(True); + + // If no sourcepath is defined, then we'll build any files + if FProject.Options.SourcePath = '' then + Exit(True); + + // Only return true if the file is directly in the ProjectOptions.SourcePath folder + SourcePath := ReplaceStr(IncludeTrailingPathDelimiter(FProject.ResolveProjectPath(FProject.Options.SourcePath)), '/', '\'); + FilePath := ReplaceStr(ExtractFilePath(FFileName), '/', '\'); + Result := SameFileName(SourcePath, FilePath); +end; + +procedure TProjectFile.Load(node: IXMLNode); // I4698 begin if node.ChildNodes.IndexOf('ID') >= 0 then FID := CleanID(VarToWideStr(node.ChildValues['ID'])); if node.ChildNodes.IndexOf('ParentFileID') >= 0 then FParentFileID := CleanID(VarToWideStr(node.ChildValues['ParentFileID'])); - - if LoadState then - begin - FIDEState.Clear; - if node.ChildNodes.IndexOf('IDEState') >= 0 then - begin - node := node.ChildNodes.Nodes['IDEState']; - for i := 0 to node.ChildNodes.Count - 1 do - FIDEState[node.ChildNodes[i].NodeName] := node.ChildNodes[i].NodeValue; - end; - end; end; procedure TProjectFile.LoadState(node: IXMLNode); // I4698 @@ -579,32 +658,18 @@ procedure TProjectFile.RemoveFreeNotification( FNotifiers.Remove(AClient); end; -procedure TProjectFile.Save(node: IXMLNode; SaveState: Boolean); // I4698 -var - I: Integer; +procedure TProjectFile.Save(node: IXMLNode); // I4698 begin node.AddChild('ID').NodeValue := FID; node.AddChild('Filename').NodeValue := ExtractFileName(FFileName); node.AddChild('Filepath').NodeValue := ExtractRelativePath(FProject.FileName, FFileName); node.AddChild('FileVersion').NodeValue := FFileVersion; // I4701 - node.AddChild('FileType').NodeValue := ExtractFileExt(FFileName);; + + // Note: FileType is only ever written in Delphi code; it is used by xsl + // transforms for rendering + node.AddChild('FileType').NodeValue := ExtractFileExt(FFileName); if Assigned(FParent) then node.AddChild('ParentFileID').NodeValue := FParent.ID; - - if SaveState then - begin - node.AddChild('FullPath').NodeValue := FFileName; - - if FIDEState.Count > 0 then - begin - node := node.AddChild('IDEState'); - for I := 0 to FIDEState.Count - 1 do - begin - with node.AddChild(TProjectFileState(FIDEState.Get(I)).Name) do - NodeValue := TProjectFileState(FIDEState.Get(I)).Value; - end; - end; - end; end; procedure TProjectFile.SaveState(node: IXMLNode); // I4698 @@ -680,11 +745,17 @@ procedure TProject.ChildRefresh(Sender: TObject); end; end; -constructor TProject.Create(AProjectType: TProjectType; AFileName: string; ALoadPersistedUntitledProject: Boolean = False); +constructor TProject.Create(AProjectType: TProjectType; AFileName: string); var i: Integer; begin - FOptions := TProjectOptions.Create; // I4688 + Assert(AFileName <> ''); + + FUpgradeMessages := TStringList.Create; + + // Assumes v1.0 by default + FOptions := TProjectOptions.Create; + FOptions.Assign(DefaultProjectOptions[pv10]); if AProjectType = ptUnknown then FOptions.ProjectType := ptKeyboard @@ -702,19 +773,9 @@ constructor TProject.Create(AProjectType: TProjectType; AFileName: string; ALoad FMustSave := False; - if (FFileName = '') and (ALoadPersistedUntitledProject) then - begin - FMustSave := True; - LoadPersistedUntitledProject; // I1010: Persist untitled project - end - else if not Load then // I4703 + if not Load then // I4703 begin - FFileName := ''; - if not Load then - begin - FMustSave := True; - LoadPersistedUntitledProject; // I4703 - end; + raise EProjectLoader.Create('Unable to load project '+FFileName); end; FBusy := True; @@ -734,6 +795,7 @@ destructor TProject.Destroy; FState := psDestroying; FFiles.Free; FMRU.Free; + FreeandNil(FUpgradeMessages); FreeAndNil(FOptions); // I4688 inherited Destroy; end; @@ -746,12 +808,6 @@ procedure TProject.DoRefreshCaption; // I4687 begin end; -procedure TProject.SetFileName(Value: string); -begin - FFileName := Value; - DoRefreshCaption; // I4687 -end; - procedure TProject.ListNotify(Item: TProjectFile; Action: TListNotification); begin if FState = psReady then @@ -776,9 +832,9 @@ function TProject.Load: Boolean; FState := psLoading; try SetLength(buf, 32); - if FileExists(SavedFileName) then + if FileExists(FileName) then begin - with TFileStream.Create(SavedFileName, fmOpenRead) do + with TFileStream.Create(FileName, fmOpenRead) do try Read(buf[0], 32); // I3310 finally @@ -786,11 +842,12 @@ function TProject.Load: Boolean; end; if (buf[0] = Ord('<')) or ((TEncoding.GetBufferEncoding(buf, encoding) > 0) and (encoding = TEncoding.UTF8)) // I3310, I3473 - then Result := LoadFromXML(SavedFileName) - else Result := ImportFromIni(SavedFileName); + then Result := LoadFromXML(FileName) + else Result := ImportFromIni(FileName); end - else if DirectoryExists(ExtractFilePath(SavedFileName)) then - Result := Save { Create a temporary project file } + else if DirectoryExists(ExtractFilePath(FileName)) then + // This will fall back to a 2.0 folder load + Result := LoadFromXML(FileName) else begin Result := False; @@ -803,40 +860,11 @@ function TProject.Load: Boolean; end; end; -// I1010: Persist untitled project - begin - -procedure TProject.LoadPersistedUntitledProject; -begin - FFileName := TProject.GetUntitledProjectFilename(False); - try - Load; - finally - FFileName := ''; - end; -end; - procedure TProject.Log(AState: TProjectLogState; Filename, Msg: string; MsgCode, line: Integer); begin // Do nothing end; -procedure TProject.PersistUntitledProject; -var - path: string; -begin - path := TProject.GetUntitledProjectFilename(False); - - FState := psSaving; - with TProjectSaver.Create(Self, path) do - try - Execute; - finally - Free; - end; - - FState := psReady; -end; - procedure TProject.Refresh; // I4687 begin DoRefresh; @@ -847,8 +875,22 @@ function TProject.Render: WideString; doc, userdoc, xsl: IXMLDomDocument; FLastDir: string; i: Integer; + saver: TProjectSaver; + xml, xmluser: string; begin - if not FileExists(SavedFileName) then Save; + FState := psSaving; + try + saver := TProjectSaver.Create(Self, ''); + try + saver.Execute; + xml := saver.XML; + xmluser := saver.XMLUser; + finally + saver.Free; + end; + finally + FState := psReady; + end; Result := ''; FLastDir := GetCurrentDir; @@ -857,24 +899,21 @@ function TProject.Render: WideString; doc := MSXMLDOMDocumentFactory.CreateDOMDocument; try doc.async := False; - doc.load(SavedFileName); + doc.loadXML(xml); // // Inject the user settings to the loaded file // - if FileExists(SavedUserFileName) then // I4698 - begin - userdoc := MSXMLDOMDocumentFactory.CreateDOMDocument; - try - userdoc.async := False; - userdoc.load(SavedUserFileName); - - for i := 0 to userdoc.documentElement.childNodes.length - 1 do - doc.documentElement.appendChild(userdoc.documentElement.childNodes.item[i].cloneNode(true)); - finally - userdoc := nil; - end; + userdoc := MSXMLDOMDocumentFactory.CreateDOMDocument; + try + userdoc.async := False; + userdoc.loadXML(xmluser); + + for i := 0 to userdoc.documentElement.childNodes.length - 1 do + doc.documentElement.appendChild(userdoc.documentElement.childNodes.item[i].cloneNode(true)); + finally + userdoc := nil; end; // @@ -901,7 +940,190 @@ function TProject.Render: WideString; end; end; -// I1010: Persist untitled project - end +function ReadUtf8FileText(const filename: string): string; +var + ss: TStringStream; +begin + ss := TStringStream.Create('', TEncoding.UTF8); + try + ss.LoadFromFile(filename); + Result := ss.DataString; + finally + ss.Free; + end; +end; + +function IsKeymanFile(filename: string): Boolean; +begin + filename := filename.ToLower; + Result := + filename.EndsWith('.model.ts') or +// filename.EndsWith('.kpj') or + filename.EndsWith('.kmn') or + filename.EndsWith('.xml') or + filename.EndsWith('.kps') or + filename.EndsWith('.kvks') or + filename.EndsWith('.keyman-touch-layout'); +end; + +function TProject.IsDefaultProject(Version: TProjectVersion): Boolean; +begin + Result := FOptions.EqualsRecord(DefaultProjectOptions[Version]); +end; + +/// +/// Adds all files in project folder to the in-memory project data +/// @param projectFilename Full path to project.kpj (even if the file doesn't exist) +/// +/// +function TProject.PopulateFiles: Boolean; +var + ProjectPath: string; +begin + if FOptions.Version <> pv20 then + raise EProjectLoader.Create('PopulateFiles can only be called on a v2.0 project'); + + FFiles.Clear; + + ProjectPath := ExtractFilePath(FileName); + if not DirectoryExists(ProjectPath) then + Exit(False); + + PopulateFolder(ProjectPath); + + Result := True; +end; + +procedure TProject.PopulateFolder(const path: string); +var + ff: string; + f: TSearchRec; +begin + if FindFirst(path + '*', faDirectory, f) = 0 then + begin + repeat + ff := path + f.Name; + + if (f.Name = '.') or (f.Name = '..') then + begin + Continue; + end; + + if (f.Attr and faDirectory) = faDirectory then + begin + PopulateFolder(ff + '\'); + Continue; + end; + + CreateProjectFile(Self, ff, nil); + until FindNext(f) <> 0; + System.SysUtils.FindClose(f); + end; +end; + +function TProject.CanUpgrade: Boolean; +var + i: Integer; + Path: string; + SourcePath: string; +begin + if FOptions.Version = pv20 then + begin + Exit(False); + end; + + FUpgradeMessages.Clear; + Result := True; + + // Things that block upgrade: + // 1. invalid paths in Options + // 2. contained file paths outside the project folder (primary files only) + + if Options.BuildPath.Contains('$SOURCEPATH') then + begin + Result := False; + FUpgradeMessages.Add('The BuildPath project setting contains the "$SOURCEPATH" tag, which is no longer supported'); + end; + if Options.BuildPath.Contains('$VERSION') then + begin + Result := False; + FUpgradeMessages.Add('The BuildPath project setting contains the "$VERSION" tag, which is no longer supported'); + end; + + SourcePath := '?'; + + for i := 0 to Files.Count - 1 do + begin + if Assigned(Files[i].Parent) then + begin + Continue; + end; + + Path := ExtractRelativePath(FileName, Files[i].FileName); + + // Ensure all compileable files + if Files[i].IsCompilable then + begin + if SourcePath = '?' then + begin + SourcePath := ExtractFileDir(Path) + end + else if not SameFileName(SourcePath, ExtractFileDir(Path)) then + begin + FUpgradeMessages.Add('File '+Files[i].FileName+' is not in the same folder as at least one other source file. All primary source files must be in the same folder.'); + Result := False; + end; + end; + + if IsRelativePath(Path) and not Path.StartsWith('..') then + begin + // Path is in same folder or a subfolder of the project + Continue; + end; + + FUpgradeMessages.Add('File '+Files[i].FileName+' is outside the project folder. All primary source files must be in the same folder as the project file, or in a subfolder.'); + Result := False; + end; +end; + +function TProject.Upgrade: Boolean; +var + i: Integer; +begin + if Options.Version = pv20 then + raise Exception.Create('Unexpected: Upgrade was called when already version 2.0'); + + if not CanUpgrade then + raise Exception.Create('Unexpected: Upgrade was called when CanUpgrade=False'); + + Options.Version := pv20; + + // Set location of all source files, default to 'source' if no source files + // are present in the project + Options.SourcePath := '$PROJECTPATH\source'; + for i := 0 to Files.Count - 1 do + begin + if Files[i].IsCompilable then + begin + Options.SourcePath := '$PROJECTPATH\' + ExtractFileDir(ExtractRelativePath(FFileName, Files[i].FileName)); + Break; + end; + end; + + for i := Files.Count - 1 downto 0 do + begin + if Assigned(Files[i].Parent) then + begin + Files.Delete(i); + end; + end; + + Save; + + PopulateFiles; + + Result := True; +end; function TProject.LoadFromXML(FileName: string): Boolean; begin @@ -972,7 +1194,7 @@ function TProject.Save: Boolean; begin FState := psSaving; try - with TProjectSaver.Create(Self, SavedFileName) do + with TProjectSaver.Create(Self, FileName) do try Execute; finally @@ -989,7 +1211,7 @@ function TProject.SaveUser: Boolean; begin FState := psSaving; try - with TProjectSaver.Create(Self, SavedFileName) do + with TProjectSaver.Create(Self, FileName) do try SaveUser; finally @@ -1002,19 +1224,17 @@ function TProject.SaveUser: Boolean; Result := True; end; -function TProject.GetSavedFileName: string; +function TProject.GetUserFileName: string; begin - if FFileName = '' - then Result := TProject.GetUntitledProjectFilename(True) // I4181 - else Result := FFileName; + Result := ChangeFileExt(FileName, Ext_ProjectSourceUser); end; -function TProject.GetSavedUserFileName: string; +function TProject.ResolveProjectPath(APath: string): string; begin - Result := ChangeFileExt(SavedFileName, Ext_ProjectSourceUser); + Result := ReplaceText(APath, '$PROJECTPATH', ExtractFileDir(ExpandFileName(FFileName))); end; -function TProject.GetTargetFilename(ATargetFile, ASourceFile, AVersion: string): string; // I4688 +function TProject.GetTargetFilename10(ATargetFile, ASourceFile, AVersion: string): string; // I4688 begin Result := Trim(Options.BuildPath); if Result = '' then Result := '$SOURCEPATH'; @@ -1022,14 +1242,32 @@ function TProject.GetTargetFilename(ATargetFile, ASourceFile, AVersion: string): // Replace placeholders in the target path Result := ReplaceText(Result, '$SOURCEPATH', ExtractFileDir(ExpandFileName(ASourceFile))); - if FFileName = '' // if we have an unsaved project, use the source path for project path - then Result := ReplaceText(Result, '$PROJECTPATH', ExtractFileDir(ExpandFileName(ASourceFile))) - else Result := ReplaceText(Result, '$PROJECTPATH', ExtractFileDir(ExpandFileName(FFileName))); + Result := ReplaceText(Result, '$PROJECTPATH', ExtractFileDir(ExpandFileName(FFileName))); Result := ReplaceText(Result, '$VERSION', AVersion); Result := Result + ExtractFileName(ATargetFile); end; +function TProject.GetTargetFilename20(ATargetFile, ASourceFile, AVersion: string): string; // I4688 +begin + Result := Trim(Options.BuildPath); + if Result = '' then + begin + Exit(ExtractFilePath(ExpandFileName(ASourceFile)) + ExtractFileName(ATargetFile)); + end; + + Result := IncludeTrailingPathDelimiter(Result); + Result := ResolveProjectPath(Result); + Result := Result + ExtractFileName(ATargetFile); +end; + +function TProject.GetTargetFilename(ATargetFile, ASourceFile, AVersion: string): string; // I4688 +begin + if Options.Version = pv10 + then Result := GetTargetFilename10(ATargetFile, ASourceFile, AVersion) + else Result := GetTargetFilename20(ATargetFile, ASourceFile, AVersion); +end; + class function TProject.StandardTemplatePath: string; //(const FileName: string): string; begin Result := StringsTemplatePath; // GetXMLTemplatePath + 'project\'; @@ -1040,13 +1278,6 @@ class function TProject.StringsTemplatePath: string; Result := GetXMLTemplatePath + 'project\'; end; -class function TProject.GetUntitledProjectFilename(CurrentProcess: Boolean): string; -begin - if CurrentProcess - then Result := GetFolderPath(CSIDL_APPDATA) + SFolderKeymanDeveloper + '\Untitled.' + IntToStr(GetCurrentProcessId) + Ext_ProjectSource - else Result := GetFolderPath(CSIDL_APPDATA) + SFolderKeymanDeveloper + '\Untitled' + Ext_ProjectSource; -end; - procedure TProject.UpdateFileParameters; // I4688 // I4710 var i: Integer; @@ -1070,20 +1301,6 @@ procedure TProject.UpdateFileParameters; // I4688 // I4710 end; end; -function TProject.GetUntitled: Boolean; -begin - Result := FFileName = ''; -end; - -{procedure TProject.ChildRefresh(Sender: TObject); -begin - if FState = psReady then - begin - Save; - Refresh; - end; -end;} - function TProject.FindFile(AFileName: string): TProjectFile; var i: Integer; @@ -1149,12 +1366,41 @@ procedure TProjectFileStates.SetItem(Index, Value: WideString); { TProjectOptions } -constructor TProjectOptions.Create; +procedure TProjectOptions.Assign(source: TProjectOptions); begin - WarnDeprecatedCode := True; // I4866 - CompilerWarningsAsErrors := False; // I4865 - CheckFilenameConventions := True; // default to TRUE for new projects - ProjectType := ptKeyboard; + Self.BuildPath := source.BuildPath; + Self.SourcePath := source.SourcePath; + Self.CompilerWarningsAsErrors := source.CompilerWarningsAsErrors; + Self.WarnDeprecatedCode := source.WarnDeprecatedCode; + Self.CheckFilenameConventions := source.CheckFilenameConventions; + Self.SkipMetadataFiles := source.SkipMetadataFiles; + Self.ProjectType := source.ProjectType; + Self.Version := Source.Version; +end; + +procedure TProjectOptions.Assign(source: TProjectOptionsRecord); +begin + Self.BuildPath := source.BuildPath; + Self.SourcePath := source.SourcePath; + Self.CompilerWarningsAsErrors := source.CompilerWarningsAsErrors; + Self.WarnDeprecatedCode := source.WarnDeprecatedCode; + Self.CheckFilenameConventions := source.CheckFilenameConventions; + Self.SkipMetadataFiles := source.SkipMetadataFiles; + Self.ProjectType := source.ProjectType; + Self.Version := Source.Version; +end; + +function TProjectOptions.EqualsRecord(source: TProjectOptionsRecord): Boolean; +begin + Result := + (Self.BuildPath = source.BuildPath) and + (Self.SourcePath = source.SourcePath) and + (Self.CompilerWarningsAsErrors = source.CompilerWarningsAsErrors) and + (Self.WarnDeprecatedCode = source.WarnDeprecatedCode) and + (Self.CheckFilenameConventions = source.CheckFilenameConventions) and + (Self.SkipMetadataFiles = source.SkipMetadataFiles) and + (Self.ProjectType = source.ProjectType) and + (Self.Version = Source.Version); end; type @@ -1232,14 +1478,25 @@ function ProjectTypeToString(pt: TProjectType): string; end; end; +function ProjectVersionFromString(s: string): TProjectVersion; +begin + if SameText(s, '1.0') then Result := pv10 + else if SameText(s, '2.0') then Result := pv20 + else Result := pvUnknown; +end; + +function ProjectVersionToString(pv: TProjectVersion): string; +begin + case pv of + pvUnknown: Result := ''; + pv10: Result := '1.0'; + pv20: Result := '2.0'; + end; +end; + initialization FGlobalProjectStateWnd := TGlobalProjectStateWnd.Create; finalization // Deletes temporary session-local project - if FileExists(TProject.GetUntitledProjectFilename(True)) then - System.SysUtils.DeleteFile(TProject.GetUntitledProjectFilename(True)); - if FileExists(ChangeFileExt(TProject.GetUntitledProjectFilename(True),Ext_ProjectSourceUser)) then - System.SysUtils.DeleteFile(ChangeFileExt(TProject.GetUntitledProjectFilename(True),Ext_ProjectSourceUser)); - FGlobalProjectStateWnd.Free; end. diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFileType.pas b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFileType.pas index 819a5491d52..c0633dc34e5 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFileType.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFileType.pas @@ -97,7 +97,8 @@ function CreateProjectFile(AProject: TProject; AFileName: string; AParent: TProj ni := -1; for i := 0 to FRegisteredFileTypes.Count - 1 do if FRegisteredFileTypes[i].Extension = '*' then ni := i - else if FRegisteredFileTypes[i].Extension = Ext then + else if (FRegisteredFileTypes[i].Extension = Ext) and + FRegisteredFileTypes[i].ProjectFileClass.IsFileTypeSupported(AFileName) then begin Result := FRegisteredFileTypes[i].ProjectFileClass.Create(AProject, AFileName, AParent); diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectLoader.pas b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectLoader.pas index aff2b243d41..c68339ab861 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectLoader.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectLoader.pas @@ -1,18 +1,18 @@ (* Name: Keyman.Developer.System.Project.ProjectLoader Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 1 Aug 2006 Modified Date: 24 Aug 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 01 Aug 2006 - mcdurdin - Initial version 19 Mar 2007 - mcdurdin - I708 - Files disappearing from project 19 Nov 2007 - mcdurdin - I1157 - const string parameters @@ -22,7 +22,7 @@ 05 May 2015 - mcdurdin - I4698 - V9.0 - Split project and user preferences files 24 Aug 2015 - mcdurdin - I4865 - Add treat hints and warnings as errors into project 24 Aug 2015 - mcdurdin - I4866 - Add warn on deprecated features to project and compile - + *) unit Keyman.Developer.System.Project.ProjectLoader; @@ -38,6 +38,8 @@ interface Keyman.Developer.System.Project.ProjectFile, utilsystem; +// Corresponds to kmc/projectLoader.ts; TypeScript implementation is master version + type EProjectLoader = class(Exception); @@ -45,7 +47,9 @@ TProjectLoader = class private FFileName: string; FProject: TProject; - procedure LoadUser; // I4698 + procedure LoadUser; + procedure LoadDefaultProjectFromFolder; + procedure LoadProjectFromFile; // I4698 public constructor Create(AProject: TProject; AFileName: string); procedure Execute; @@ -73,13 +77,30 @@ constructor TProjectLoader.Create(AProject: TProject; AFileName: string); FFileName := AFileName; end; - procedure TProjectLoader.Execute; // I4698 +begin + if FileExists(FFileName) or (FFileName = '') + then LoadProjectFromFile + else LoadDefaultProjectFromFolder; +end; + +procedure TProjectLoader.LoadDefaultProjectFromFolder; +begin + FProject.Options.Assign(DefaultProjectOptions[pv20]); + if not FProject.PopulateFiles then + // TODO: This seems somewhat arbitrary and troublesome. Better to load the + // folder and give warnings about file layout + raise EProjectLoader.Create('Not a Keyman Developer project folder'); +end; + + +procedure TProjectLoader.LoadProjectFromFile; var n, i: Integer; doc: IXMLDocument; node, root: IXMLNode; pf: TProjectFile; + hasUserState: Boolean; begin try doc := LoadXMLDocument(FFileName); @@ -88,6 +109,8 @@ procedure TProjectLoader.Execute; // I4698 raise EProjectLoader.Create('Error loading project file: '+E.Message); end; + hasUserState := FileExists(ChangeFileExt(FFileName, Ext_ProjectSourceUser)); + root := doc.DocumentElement; if root.NodeName <> 'KeymanDeveloperProject' then raise EProjectLoader.Create('Not a Keyman Developer project file'); @@ -97,70 +120,96 @@ procedure TProjectLoader.Execute; // I4698 node := root.ChildNodes.FindNode('Options'); // I4688 if node <> nil then begin - FProject.Options.BuildPath := VarToStr(node.ChildValues['BuildPath']); + if not VarIsNull(node.ChildValues['Version']) then + begin + FProject.Options.Version := ProjectVersionFromString(VarToStr(node.ChildValues['Version'])); + if FProject.Options.Version = pvUnknown then + raise EProjectLoader.Create('This project format is version '+VarToStr(node.ChildValues['Version'])+', which is not supported by this version of Keyman Developer.'); + end; + + // Set default project options based on what we learned above + FProject.Options.Assign(DefaultProjectOptions[FProject.Options.Version]); - if VarIsNull(node.ChildValues['CompilerWarningsAsErrors']) // I4865 - then FProject.Options.CompilerWarningsAsErrors := False - else FProject.Options.CompilerWarningsAsErrors := node.ChildValues['CompilerWarningsAsErrors']; + if not VarIsNull(node.ChildValues['BuildPath']) then + FProject.Options.BuildPath := VarToStr(node.ChildValues['BuildPath']); - if VarIsNull(node.ChildValues['WarnDeprecatedCode']) // I4866 - then FProject.Options.WarnDeprecatedCode := True - else FProject.Options.WarnDeprecatedCode := node.ChildValues['WarnDeprecatedCode']; + if not VarIsNull(node.ChildValues['SourcePath']) then + FProject.Options.SourcePath := VarToStr(node.ChildValues['SourcePath']); - if VarIsNull(node.ChildValues['CheckFilenameConventions']) - then FProject.Options.CheckFilenameConventions := False // existing projects default to FALSE (new projects default to TRUE) - else FProject.Options.CheckFilenameConventions := node.ChildValues['CheckFilenameConventions']; + if not VarIsNull(node.ChildValues['CompilerWarningsAsErrors']) then + FProject.Options.CompilerWarningsAsErrors := node.ChildValues['CompilerWarningsAsErrors']; - FProject.Options.ProjectType := ProjectTypeFromString(VarToStr(node.ChildValues['ProjectType'])); - if FProject.Options.ProjectType = ptUnknown then - // Support projects without a defined projecttype - FProject.Options.ProjectType := ptKeyboard; + if not VarIsNull(node.ChildValues['WarnDeprecatedCode']) then + FProject.Options.WarnDeprecatedCode := node.ChildValues['WarnDeprecatedCode']; + + if not VarIsNull(node.ChildValues['CheckFilenameConventions']) then + FProject.Options.CheckFilenameConventions := node.ChildValues['CheckFilenameConventions']; + + if not VarIsNull(node.ChildValues['SkipMetadataFiles']) then + FProject.Options.SkipMetadataFiles := node.ChildValues['SkipMetadataFiles']; + + if not VarIsNull(node.ChildValues['ProjectType']) then + begin + FProject.Options.ProjectType := ProjectTypeFromString(VarToStr(node.ChildValues['ProjectType'])); + if FProject.Options.ProjectType = ptUnknown then + // Support projects without a defined projecttype + FProject.Options.ProjectType := ptKeyboard; + end; end; { Load root nodes first - I708 } - for i := 0 to root.ChildNodes['Files'].ChildNodes.Count - 1 do + if FProject.Options.Version = pv10 then begin - node := root.ChildNodes['Files'].ChildNodes[i]; - if node.NodeName <> 'File' then Continue; - if node.ChildNodes.FindNode('ParentFileID') = nil then // ChildValues['ParentFileID'] then + for i := 0 to root.ChildNodes['Files'].ChildNodes.Count - 1 do begin - if not VarIsNull(node.ChildValues['Filepath']) then + node := root.ChildNodes['Files'].ChildNodes[i]; + if node.NodeName <> 'File' then Continue; + if node.ChildNodes.FindNode('ParentFileID') = nil then // ChildValues['ParentFileID'] then begin - // I1152 - Avoid crashes when .kpj file is invalid - pf := CreateProjectFile(FProject, ExpandFileNameClean(FFileName, node.ChildValues['Filepath']), nil); - pf.Load(node, True); + if not VarIsNull(node.ChildValues['Filepath']) then + begin + // I1152 - Avoid crashes when .kpj file is invalid + pf := CreateProjectFile(FProject, ExpandFileNameClean(FFileName, node.ChildValues['Filepath']), nil); + pf.Load(node); + if not hasUserState then + pf.LoadState(node); + end; end; end; - end; - { Load child nodes } + { Load child nodes } - for i := 0 to root.ChildNodes['Files'].ChildNodes.Count - 1 do - begin - node := root.ChildNodes['Files'].ChildNodes[i]; - if node.NodeName <> 'File' then Continue; - - if node.ChildNodes.FindNode('ParentFileID') <> nil then // ChildValues['ParentFileID'] then + for i := 0 to root.ChildNodes['Files'].ChildNodes.Count - 1 do begin - n := FProject.Files.IndexOfID(node.ChildValues['ParentFileID']); - if n < 0 then Continue; - pf := CreateProjectFile(FProject, ExpandFileNameClean(FFileName, node.ChildValues['Filepath']), FProject.Files[n]); - pf.Load(node, True); + node := root.ChildNodes['Files'].ChildNodes[i]; + if node.NodeName <> 'File' then Continue; + + if node.ChildNodes.FindNode('ParentFileID') <> nil then // ChildValues['ParentFileID'] then + begin + n := FProject.Files.IndexOfID(node.ChildValues['ParentFileID']); + if n < 0 then Continue; + pf := CreateProjectFile(FProject, ExpandFileNameClean(FFileName, node.ChildValues['Filepath']), FProject.Files[n]); + pf.Load(node); + if not hasUserState then + pf.LoadState(node); + end; end; - end; - { Load MRU from old project files } + { Load MRU from old project files } - node := root.ChildNodes['MRU']; - if Assigned(node) then - begin - for i := 0 to node.ChildNodes.Count - 1 do + node := root.ChildNodes['MRU']; + if Assigned(node) then begin - if node.ChildNodes[i].NodeName <> 'File' then Continue; - FProject.MRU.Append(node.ChildNodes[i].ChildNodes['FullPath'].NodeValue); + for i := 0 to node.ChildNodes.Count - 1 do + begin + if node.ChildNodes[i].NodeName <> 'File' then Continue; + FProject.MRU.Append(node.ChildNodes[i].ChildNodes['FullPath'].NodeValue); + end; end; - end; + end + else + FProject.PopulateFiles; LoadUser; end; diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectSaver.pas b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectSaver.pas index 8e5e77a4bd7..20b85e3cfdc 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectSaver.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectSaver.pas @@ -37,7 +37,7 @@ interface uses System.Classes, - SysUtils, + System.SysUtils, Winapi.Windows, Xml.XMLDoc, Xml.XMLIntf, @@ -52,10 +52,14 @@ TProjectSaver = class private FFileName: string; FProject: TProject; + FXML: string; + FXMLUser: string; public constructor Create(AProject: TProject; AFileName: string); procedure Execute; procedure SaveUser; // I4698 + property XML: string read FXML; + property XMLUser: string read FXMLUser; end; implementation @@ -79,8 +83,18 @@ procedure TProjectSaver.Execute; // I4698 var i: Integer; doc: IXMLDocument; - node, root: IXMLNode; + filenode, node, root: IXMLNode; + defopts: TProjectOptionsRecord; begin + if FProject.IsDefaultProject(pv20) and (FFileName <> '') then + begin + if FileExists(FFileName) then + System.SysUtils.DeleteFile(FFileName); + Exit; + end; + + defopts := DefaultProjectOptions[FProject.Options.Version]; + doc := NewXMLDocument(); doc.Options := doc.Options + [doNodeAutoIndent]; // I4704 doc.Encoding := 'utf-8'; @@ -91,19 +105,63 @@ procedure TProjectSaver.Execute; // I4698 // options node := root.AddChild('Options'); // I4688 - node.AddChild('BuildPath').NodeValue := FProject.Options.BuildPath; - node.AddChild('CompilerWarningsAsErrors').NodeValue := FProject.Options.CompilerWarningsAsErrors; // I4866 - node.AddChild('WarnDeprecatedCode').NodeValue := FProject.Options.WarnDeprecatedCode; // I4865 - node.AddChild('CheckFilenameConventions').NodeValue := FProject.Options.CheckFilenameConventions; // I4866 - node.AddChild('ProjectType').NodeValue := ProjectTypeToString(FProject.Options.ProjectType); + if FProject.Options.Version = pv20 then + // Only v2.0 projects have a version number + node.AddChild('Version').NodeValue := ProjectVersionToString(FProject.Options.Version); + + if FProject.Options.BuildPath <> defopts.BuildPath then + node.AddChild('BuildPath').NodeValue := FProject.Options.BuildPath; + + if FProject.Options.SourcePath <> defopts.SourcePath then + node.AddChild('SourcePath').NodeValue := FProject.Options.SourcePath; + + if FProject.Options.CompilerWarningsAsErrors <> defopts.CompilerWarningsAsErrors then + node.AddChild('CompilerWarningsAsErrors').NodeValue := FProject.Options.CompilerWarningsAsErrors; // I4866 + + if FProject.Options.WarnDeprecatedCode <> defopts.WarnDeprecatedCode then + node.AddChild('WarnDeprecatedCode').NodeValue := FProject.Options.WarnDeprecatedCode; // I4865 + + if FProject.Options.CheckFilenameConventions <> defopts.CheckFilenameConventions then + node.AddChild('CheckFilenameConventions').NodeValue := FProject.Options.CheckFilenameConventions; // I4866 + + if FProject.Options.SkipMetadataFiles <> defopts.SkipMetadataFiles then + node.AddChild('SkipMetadataFiles').NodeValue := FProject.Options.SkipMetadataFiles; + + if FProject.Options.ProjectType <> defopts.ProjectType then + node.AddChild('ProjectType').NodeValue := ProjectTypeToString(FProject.Options.ProjectType); // files - node := root.AddChild('Files'); - for i := 0 to FProject.Files.Count - 1 do - FProject.Files[i].Save(node.AddChild('File'), False); + if (FProject.Options.Version = pv10) or (FFileName = '') then + begin + node := root.AddChild('Files'); + for i := 0 to FProject.Files.Count - 1 do + begin + filenode := node.AddChild('File'); + + // For xsl renderer, we have additional metadata we provide for v2.0 + // projects, at least until we replace the project view with a tree + // structure + if (FFileName = '') then + begin + if FProject.Files[i].IsSourceFile + then filenode.AddChild('IsInSourcePath').NodeValue := 'true' + else filenode.AddChild('IsInSourcePath').NodeValue := 'false'; + if FProject.Files[i].IsCompilable + then filenode.AddChild('IsCompilable').NodeValue := 'true' + else filenode.AddChild('IsCompilable').NodeValue := 'false'; + end; + + FProject.Files[i].Save(filenode); + end; + end; - doc.SaveToFile(FFileName); + if FFileName <> '' then + begin + doc.SaveToFile(FFileName); + end + else + doc.SaveToXML(FXML); SaveUser; end; @@ -153,7 +211,12 @@ procedure TProjectSaver.SaveUser; // I4698 end; end; - doc.SaveToFile(ChangeFileExt(FFileName, Ext_ProjectSourceUser)); + if FFileName <> '' then + begin + doc.SaveToFile(ChangeFileExt(FFileName, Ext_ProjectSourceUser)); + end + else + doc.SaveToXML(FXMLUser); end; end. diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.kmnProjectFile.pas b/developer/src/tike/project/Keyman.Developer.System.Project.kmnProjectFile.pas index db149fe8166..55ef1992a59 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.kmnProjectFile.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.kmnProjectFile.pas @@ -1,18 +1,18 @@ (* Name: Keyman.Developer.System.Project.kmnProjectFile Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 1 Aug 2006 Modified Date: 24 Aug 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 01 Aug 2006 - mcdurdin - Add loading and saving from XML 23 Aug 2006 - mcdurdin - Add CompileKeyboardToWeb function 28 Sep 2006 - mcdurdin - Editions @@ -47,7 +47,7 @@ 03 Aug 2015 - mcdurdin - I4823 - Note in compile log if symbols are included in build 24 Aug 2015 - mcdurdin - I4865 - Add treat hints and warnings as errors into project 24 Aug 2015 - mcdurdin - I4866 - Add warn on deprecated features to project and compile - + *) unit Keyman.Developer.System.Project.kmnProjectFile; // I3306 // I4687 // I4688 // I4692 @@ -73,7 +73,6 @@ TkmnProjectFile = class(TOpenableProjectFile) FHeader_Copyright: WideString; FTargets: TKeymanTargets; FKVKFileName: string; - FWarnAsError: Boolean; // I4706 function GetOutputFilename: string; function GetTargetFilename: string; @@ -85,13 +84,13 @@ TkmnProjectFile = class(TOpenableProjectFile) property KVKFileName: string read FKVKFileName; property IsDebug: Boolean read FDebug; public - procedure Load(node: IXMLNode; LoadState: Boolean); override; // I4698 - procedure Save(node: IXMLNode; SaveState: Boolean); override; // I4698 + function IsCompilable: Boolean; override; + procedure Load(node: IXMLNode); override; // I4698 + procedure Save(node: IXMLNode); override; // I4698 procedure LoadState(node: IXMLNode); override; // I4698 procedure SaveState(node: IXMLNode); override; // I4698 property Debug: Boolean read FDebug write FDebug; - property WarnAsError: Boolean read FWarnAsError write FWarnAsError; // I4706 property OutputFilename: string read GetOutputFilename; property TargetFilename: string read GetTargetFilename; @@ -119,15 +118,13 @@ implementation - TkmnProjectFile - -------------------------------------------------------------------------------} -procedure TkmnProjectFile.Save(node: IXMLNode; SaveState: Boolean); // I4698 +procedure TkmnProjectFile.Save(node: IXMLNode); // I4698 begin - inherited Save(node, SaveState); // I4698 + inherited Save(node); // I4698 node := node.AddChild('Details'); if FHeader_Name <> '' then node.AddChild('Name').NodeValue := FHeader_Name; if FHeader_Copyright <> '' then node.AddChild('Copyright').NodeValue := FHeader_Copyright; if FHeader_Message <> '' then node.AddChild('Message').NodeValue := FHeader_Message; - - if SaveState then node.AddChild('Debug').NodeValue := FDebug; // I4698 end; procedure TkmnProjectFile.SaveState(node: IXMLNode); // I4698 @@ -136,22 +133,15 @@ procedure TkmnProjectFile.SaveState(node: IXMLNode); // I4698 node.AddChild('Debug').NodeValue := FDebug; end; -procedure TkmnProjectFile.Load(node: IXMLNode; LoadState: Boolean); // I4698 +procedure TkmnProjectFile.Load(node: IXMLNode); // I4698 begin - inherited Load(node, LoadState); + inherited Load(node); if node.ChildNodes.IndexOf('Details') < 0 then Exit; node := node.ChildNodes['Details']; if node.ChildNodes.IndexOf('Name') >= 0 then FHeader_Name := VarToWideStr(node.ChildValues['Name']); if node.ChildNodes.IndexOf('Copyright') >= 0 then FHeader_Copyright := VarToWideStr(node.ChildValues['Copyright']); if node.ChildNodes.IndexOf('Message') >= 0 then FHeader_Message := VarToWideStr(node.ChildValues['Message']); - - if LoadState then - try - if node.ChildNodes.IndexOf('Debug') >= 0 then FDebug := node.ChildValues['Debug']; - except - FDebug := False; - end; end; procedure TkmnProjectFile.LoadState(node: IXMLNode); // I4698 @@ -181,6 +171,11 @@ function TkmnProjectFile.GetTargetFilename: string; Result := OwnerProject.GetTargetFilename(OutputFileName, FileName, FTempFileVersion); end; +function TkmnProjectFile.IsCompilable: Boolean; +begin + Result := True; +end; + procedure TkmnProjectFile.GetFileParameters; var j: Integer; diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFile.pas b/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFile.pas index 8c3ec46f8a0..cbba63398df 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFile.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.kpsProjectFile.pas @@ -1,18 +1,18 @@ (* Name: Keyman.Developer.System.Project.kpsProjectFile Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 20 Jun 2006 Modified Date: 6 Jun 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 20 Jun 2006 - mcdurdin - Initial version 01 Aug 2006 - mcdurdin - Add loading and saving from XML 23 Aug 2006 - mcdurdin - Refactor to use actions @@ -54,7 +54,6 @@ TkpsProjectFile = class(TOpenableProjectFile) FHeader_Copyright: string; FHeader_Version: string; FMSIFileName: WideString; - FWarnAsError: Boolean; // I4706 function GetOutputFilename: string; function GetTargetFilename: string; function GetTargetInstallerFilename: string; @@ -62,10 +61,9 @@ TkpsProjectFile = class(TOpenableProjectFile) function GetRelativeOrder: Integer; override; procedure GetFileParameters; override; public - procedure Load(node: IXMLNode; LoadState: Boolean); override; // I4698 - procedure Save(node: IXMLNode; SaveState: Boolean); override; // I4698 - - property WarnAsError: Boolean read FWarnAsError write FWarnAsError; // I4706 + function IsCompilable: Boolean; override; + procedure Load(node: IXMLNode); override; // I4698 + procedure Save(node: IXMLNode); override; // I4698 property OutputFilename: string read GetOutputFilename; property TargetFilename: string read GetTargetFilename; @@ -163,9 +161,14 @@ function TkpsProjectFile.GetTargetInstallerFilename: string; Result := OwnerProject.GetTargetFilename(ChangeFileExt(ExtractFileName(FMSIFileName),'') + '-' + ChangeFileExt(ExtractFileName(OutputFilename), '') + '.exe', Filename, FileVersion); end; -procedure TkpsProjectFile.Load(node: IXMLNode; LoadState: Boolean); // I4698 +function TkpsProjectFile.IsCompilable: Boolean; +begin + Result := True; +end; + +procedure TkpsProjectFile.Load(node: IXMLNode); // I4698 begin - inherited Load(node, LoadState); + inherited Load(node); if node.ChildNodes.IndexOf('Details') < 0 then Exit; node := node.ChildNodes['Details']; @@ -174,9 +177,9 @@ procedure TkpsProjectFile.Load(node: IXMLNode; LoadState: Boolean); // I4698 if node.ChildNodes.IndexOf('Version') >= 0 then FHeader_Version := VarToWideStr(node.ChildValues['Version']); end; -procedure TkpsProjectFile.Save(node: IXMLNode; SaveState: Boolean); // I4698 +procedure TkpsProjectFile.Save(node: IXMLNode); // I4698 begin - inherited Save(node, SaveState); + inherited Save(node); node := node.AddChild('Details'); if FHeader_Name <> '' then node.AddChild('Name').NodeValue := FHeader_Name; if FHeader_Copyright <> '' then node.AddChild('Copyright').NodeValue := FHeader_Copyright; diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.modelTsProjectFile.pas b/developer/src/tike/project/Keyman.Developer.System.Project.modelTsProjectFile.pas index d04488df532..464409df518 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.modelTsProjectFile.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.modelTsProjectFile.pas @@ -34,7 +34,6 @@ TmodelTsProjectFile = class; TmodelTsProjectFile = class(TOpenableProjectFile) private FDebug: Boolean; - FWarnAsError: Boolean; FTestKeyboard: string; // I4706 function GetTargetFilename: string; @@ -44,16 +43,13 @@ TmodelTsProjectFile = class(TOpenableProjectFile) property IsDebug: Boolean read FDebug; public - procedure Load(node: IXMLNode; LoadState: Boolean); override; // I4698 - procedure Save(node: IXMLNode; SaveState: Boolean); override; // I4698 + function IsCompilable: Boolean; override; procedure LoadState(node: IXMLNode); override; // I4698 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 TargetFilename: string read GetTargetFilename; end; @@ -70,13 +66,6 @@ implementation - TmodelTsProjectFile - -------------------------------------------------------------------------------} -procedure TmodelTsProjectFile.Save(node: IXMLNode; SaveState: Boolean); // I4698 -begin - inherited Save(node, SaveState); // I4698 - if SaveState then - Self.SaveState(node); // I4698 -end; - procedure TmodelTsProjectFile.SaveState(node: IXMLNode); // I4698 begin inherited SaveState(node); @@ -84,14 +73,6 @@ procedure TmodelTsProjectFile.SaveState(node: IXMLNode); // I4698 node.AddChild('TestKeyboard').NodeValue := FTestKeyboard; end; -procedure TmodelTsProjectFile.Load(node: IXMLNode; LoadState: Boolean); // I4698 -begin - inherited Load(node, LoadState); - - if LoadState then - Self.LoadState(node); -end; - procedure TmodelTsProjectFile.LoadState(node: IXMLNode); // I4698 begin inherited LoadState(node); @@ -126,6 +107,11 @@ function TmodelTsProjectFile.GetTargetFilename: string; Result := OwnerProject.GetTargetFilename(OutputFileName, FileName, FTempFileVersion); end; +function TmodelTsProjectFile.IsCompilable: Boolean; +begin + Result := True; +end; + procedure TmodelTsProjectFile.GetFileParameters; begin SetFileVersion('1.0'); // I4701 diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.modelTsProjectFileAction.pas b/developer/src/tike/project/Keyman.Developer.System.Project.modelTsProjectFileAction.pas index 834045aa177..1015c455a0f 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.modelTsProjectFileAction.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.modelTsProjectFileAction.pas @@ -64,7 +64,7 @@ function TmodelTsProjectFileAction.CompileModel: Boolean; ForceDirectories(ExtractFileDir(TargetFileName)); Result := CompileModelFile(Self, FileName, TargetFileName, IsDebug); - if HasCompileWarning and (WarnAsError or OwnerProject.Options.CompilerWarningsAsErrors) then Result := False; // I4706 + if HasCompileWarning and OwnerProject.Options.CompilerWarningsAsErrors then Result := False; // I4706 if Result then Log(plsSuccess, Format('''%s'' was compiled successfully to ''%s''.', [FileName, TargetFileName]), 0, 0) // I4504 diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.xmlLdmlProjectFile.pas b/developer/src/tike/project/Keyman.Developer.System.Project.xmlLdmlProjectFile.pas new file mode 100644 index 00000000000..9609b899282 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.System.Project.xmlLdmlProjectFile.pas @@ -0,0 +1,187 @@ +{ + * Keyman is copyright (C) SIL International. MIT License. + * + * xmlLdmlProjectFile: LDML keyboard files +} +unit Keyman.Developer.System.Project.xmlLdmlProjectFile; + +interface + +uses + System.SysUtils, + Xml.XMLIntf, + + Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.System.Project.ProjectFiles, + Keyman.Developer.System.Project.ProjectFileType, + UKeymanTargets; + +type + TxmlLdmlProjectFile = class; + + TxmlLdmlProjectFile = class(TOpenableProjectFile) + private + FDebug: Boolean; + FHeader_Name: WideString; + FTargets: TKeymanTargets; + FKVKFileName: string; + + function GetOutputFilename: string; + function GetTargetFilename: string; + function GetJSTargetFilename: string; + protected + function GetRelativeOrder: Integer; override; + procedure GetFileParameters; override; + + property KVKFileName: string read FKVKFileName; + property IsDebug: Boolean read FDebug; + public + class function IsFileTypeSupported(const Filename: string): Boolean; override; + function IsCompilable: Boolean; override; + procedure Load(node: IXMLNode); override; // I4698 + procedure Save(node: IXMLNode); override; // I4698 + procedure LoadState(node: IXMLNode); override; // I4698 + procedure SaveState(node: IXMLNode); override; // I4698 + + property Debug: Boolean read FDebug write FDebug; + + property OutputFilename: string read GetOutputFilename; + property TargetFilename: string read GetTargetFilename; + property JSTargetFilename: string read GetJSTargetFilename; + property Header_Name: WideString read FHeader_Name; + property Targets: TKeymanTargets read FTargets; + end; + +implementation + +uses + System.Classes, + System.Variants, + Winapi.Windows, + + KeyboardParser, + kmxfileconsts, + KeyboardFonts, + Keyman.System.KeyboardUtils, + utilsystem; + +{------------------------------------------------------------------------------- + - TxmlLdmlProjectFile - + -------------------------------------------------------------------------------} + +function TxmlLdmlProjectFile.IsCompilable: Boolean; +begin + Result := True; +end; + +class function TxmlLdmlProjectFile.IsFileTypeSupported(const Filename: string): Boolean; +var + ss: TStringStream; +begin + // Look for DTD in plain text as an adequate heuristic + ss := TStringStream.Create('', TEncoding.UTF8); + try + ss.LoadFromFile(Filename); + Result := ss.DataString.IndexOf('ldmlKeyboard3.dtd') > 0; + finally + ss.Free; + end; +end; + +procedure TxmlLdmlProjectFile.Save(node: IXMLNode); // I4698 +begin + inherited Save(node); // I4698 + + // We override the FileType set by TProjectFile so that we + // can distinguish .xml keyboard files in the project view + // from other arbitrary .xml files that may be in the project + // FileType is _only_ used by the project view xsl files + node.ChildValues['FileType'] := '.xml-ldml-keyboard'; + + node := node.AddChild('Details'); + if FHeader_Name <> '' then node.AddChild('Name').NodeValue := FHeader_Name; +end; + +procedure TxmlLdmlProjectFile.SaveState(node: IXMLNode); // I4698 +begin + inherited SaveState(node); + node.AddChild('Debug').NodeValue := FDebug; +end; + +procedure TxmlLdmlProjectFile.Load(node: IXMLNode); // I4698 +begin + inherited Load(node); + + if node.ChildNodes.IndexOf('Details') < 0 then Exit; + node := node.ChildNodes['Details']; + if node.ChildNodes.IndexOf('Name') >= 0 then FHeader_Name := VarToWideStr(node.ChildValues['Name']); +end; + +procedure TxmlLdmlProjectFile.LoadState(node: IXMLNode); // I4698 +begin + inherited LoadState(node); + try + if node.ChildNodes.IndexOf('Debug') >= 0 then FDebug := node.ChildValues['Debug']; + except + FDebug := False; + end; +end; + +function TxmlLdmlProjectFile.GetRelativeOrder: Integer; +begin + Result := 20; +end; + +function TxmlLdmlProjectFile.GetTargetFilename: string; +var + FTempFileVersion: string; +begin + // https://github.com/keymanapp/keyman/issues/631 + // This appears to be a Delphi compiler bug (RSP-20457) + // Workaround is to make a copy of the parameter locally + // which fixes the reference counting. + FTempFileVersion := FileVersion; + Result := OwnerProject.GetTargetFilename(OutputFileName, FileName, FTempFileVersion); +end; + +procedure TxmlLdmlProjectFile.GetFileParameters; +begin + FHeader_Name := ''; + FKVKFileName := ''; + SetFileVersion('1.0'); // I4701 + FTargets := AllKeymanTargets; + + if not FileExists(FileName) then Exit; + + // TODO: Load from XML? +end; + +function TxmlLdmlProjectFile.GetJSTargetFilename: string; +begin + if FTargets = [] then + GetFileParameters; + + // There is no JS target if no target is specified + if FTargets * KMWKeymanTargets = [] then + Exit(''); + + Result := OwnerProject.GetTargetFilename(TKeyboardUtils.GetKeymanWebCompiledFileName(FileName), FileName, FileVersion); +end; + +function TxmlLdmlProjectFile.GetOutputFilename: string; +begin + if FTargets = [] then + GetFileParameters; + + // If no target is specified, we'll fall back to .kmx + // so we always have at least one target filename + if (FTargets <> []) and (FTargets * KMXKeymanTargets = []) then + Exit(''); + Result := ChangeFileExt(FileName, '.kmx'); +end; + + +initialization + RegisterProjectFileType('.xml', TxmlLdmlProjectFile); +end. + diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.xmlLdmlProjectFileAction.pas b/developer/src/tike/project/Keyman.Developer.System.Project.xmlLdmlProjectFileAction.pas new file mode 100644 index 00000000000..36c9c85f7cb --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.System.Project.xmlLdmlProjectFileAction.pas @@ -0,0 +1,93 @@ +{ + * Keyman is copyright (C) SIL International. MIT License. + * + * xmlLdmlProjectFileAction: actions for LDML keyboard files +} +unit Keyman.Developer.System.Project.xmlLdmlProjectFileAction; + +interface + +uses + System.SysUtils, + + Keyman.Developer.System.Project.xmlLdmlProjectFile, + Keyman.Developer.System.Project.ProjectFile, + Keyman.Developer.System.Project.ProjectFiles, + Keyman.Developer.System.Project.ProjectFileType, + UKeymanTargets; + +type + TxmlLdmlProjectFileAction = class(TxmlLdmlProjectFile) + private + procedure CheckFilenameConventions; + public + function CompileKeyboard: Boolean; + function Clean: Boolean; + end; + +implementation + +uses + System.Classes, + System.StrUtils, + System.Variants, + Winapi.Windows, + + Keyman.Developer.System.Project.ProjectLog, + Keyman.System.KeyboardUtils, + Keyman.Developer.System.KmcWrapper; + +function TxmlLdmlProjectFileAction.Clean: Boolean; +var + FJS: string; +begin + CleanFile(OutputFileName); + + if Targets * KMXKeymanTargets <> [] then + CleanFile(ExtractFilePath(FileName) + ExtractFileName(ChangeFileExt(KVKFileName, '.kvk')), True); + + if Targets * KMWKeymanTargets <> [] then + begin + FJS := TKeyboardUtils.GetKeymanWebCompiledFileName(FileName); + CleanFile(FJS); // keyboard.js + end; + + Result := True; +end; + +procedure TxmlLdmlProjectFileAction.CheckFilenameConventions; +begin + if not OwnerProject.Options.CheckFilenameConventions then + Exit; + + if not TKeyboardUtils.DoesKeyboardFilenameFollowConventions(FileName) then + begin + HasCompileWarning := True; + Log(plsWarning, Format(TKeyboardUtils.SKeyboardNameDoesNotFollowConventions_Message, [ExtractFileName(FileName)]), CERR_WARNING, 0); + end; + + if KVKFileName <> '' then + begin + if not TKeyboardUtils.DoesKeyboardFilenameFollowConventions(KVKFileName) then + begin + HasCompileWarning := True; + Log(plsWarning, Format(TKeyboardUtils.SKeyboardNameDoesNotFollowConventions_Message, [ExtractFileName(KVKFileName)]), CERR_WARNING, 0); + end; + end; +end; + +function TxmlLdmlProjectFileAction.CompileKeyboard: Boolean; +var + w: TKmcWrapper; +begin + w := TKmcWrapper.Create; + try + Result := w.Compile(Self, FileName, TargetFilename, Debug); + finally + w.Free; + end; +end; + +initialization + RegisterProjectFileType('.xml', TxmlLdmlProjectFileAction); +end. diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas index 6026fb2377e..f850f9a9dad 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectFileUI.pas @@ -47,7 +47,7 @@ TProjectUI = class(TProject) procedure DoRefreshCaption; override; public - constructor Create(AProjectType: TProjectType; AFileName: string; ALoadPersistedUntitledProject: Boolean = False); override; + constructor Create(AProjectType: TProjectType; AFileName: string); override; destructor Destroy; override; procedure Log(AState: TProjectLogState; Filename, Msg: string; MsgCode, line: Integer); override; // I4706 @@ -129,8 +129,7 @@ procedure TProjectUI.DoRefreshCaption; { TProjectUI } -constructor TProjectUI.Create(AProjectType: TProjectType; AFileName: string; - ALoadPersistedUntitledProject: Boolean); +constructor TProjectUI.Create(AProjectType: TProjectType; AFileName: string); begin inherited; FRenderFileName := TTempFileManager.Get('.html'); // I4181 @@ -144,9 +143,7 @@ destructor TProjectUI.Destroy; function TProjectUI.DisplayFileName: string; begin - if Untitled - then Result := '(untitled project)' - else Result := ExtractFileName(FileName); + Result := ExtractFileName(FileName); end; function TProjectUI.Load: Boolean; // I4694 @@ -185,7 +182,7 @@ function TProjectUI.Render: WideString; FLastDir: string; i: Integer; begin - if not FileExists(SavedFileName) then Save; + if not FileExists(FileName) then Save; Result := FRenderFileName.Name; // I4181 FLastDir := GetCurrentDir; @@ -194,18 +191,18 @@ function TProjectUI.Render: WideString; doc := MSXMLDOMDocumentFactory.CreateDOMDocument; try doc.async := False; - doc.load(SavedFileName); + doc.load(FileName); // // Inject the user settings to the loaded file // - if FileExists(SavedUserFileName) then // I4698 + if FileExists(UserFileName) then // I4698 begin userdoc := MSXMLDOMDocumentFactory.CreateDOMDocument; try userdoc.async := False; - userdoc.load(SavedUserFileName); + userdoc.load(UserFileName); for i := 0 to userdoc.documentElement.childNodes.length - 1 do doc.documentElement.appendChild(userdoc.documentElement.childNodes.item[i].cloneNode(true)); finally diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas index a62ae6ec13a..97e0fb24116 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.ProjectUI.pas @@ -25,8 +25,7 @@ interface Keyman.Developer.UI.Project.ProjectFileUI; function GetGlobalProjectUI: TProjectUI; -function LoadGlobalProjectUI(pt: TProjectType; AFilename: string; ALoadPersistedUntitledProject: Boolean = False): TProjectUI; -function NewGlobalProjectUI(pt: TProjectType): TProjectUI; +function LoadGlobalProjectUI(pt: TProjectType; AFilename: string): TProjectUI; procedure FreeGlobalProjectUI; function IsGlobalProjectUIReady: Boolean; @@ -52,22 +51,17 @@ procedure FreeGlobalProjectUI; FreeAndNil(FGlobalProject); end; -function LoadGlobalProjectUI(pt: TProjectType; AFilename: string; ALoadPersistedUntitledProject: Boolean = False): TProjectUI; +function LoadGlobalProjectUI(pt: TProjectType; AFilename: string): TProjectUI; begin Assert(not Assigned(FGlobalProject)); - Result := TProjectUI.Create(pt, AFilename, ALoadPersistedUntitledProject); // I4687 - FGlobalProject := Result; -end; - -function NewGlobalProjectUI(pt: TProjectType): TProjectUI; -var - FSessionUntitledProjectFilename: string; -begin - Assert(not Assigned(FGlobalProject)); - FSessionUntitledProjectFilename := TProject.GetUntitledProjectFilename(True); - if FileExists(FSessionUntitledProjectFilename) then - DeleteFile(FSessionUntitledProjectFilename); - Result := TProjectUI.Create(pt, '', False); // I4687 + if DirectoryExists(AFilename) then + begin + // Load a directory-based project + if AFilename.EndsWith('\') then + AFilename := AFilename.Substring(0, AFilename.Length-1); + AFilename := AFilename + '\' + ExtractFileName(AFilename) + '.kpj'; + end; + Result := TProjectUI.Create(pt, AFilename); // I4687 FGlobalProject := Result; end; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.dfm index cd122cb0087..9c4dc677248 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.dfm +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.dfm @@ -2,19 +2,18 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters BorderIcons = [biSystemMenu] BorderStyle = bsDialog Caption = 'New Wordlist Lexical Model Project' - ClientHeight = 510 - ClientWidth = 412 + ClientHeight = 402 + ClientWidth = 756 OldCreateOrder = True Position = poScreenCenter OnDestroy = FormDestroy - ExplicitTop = -8 - ExplicitWidth = 418 - ExplicitHeight = 539 + ExplicitWidth = 762 + ExplicitHeight = 431 PixelsPerInch = 96 TextHeight = 13 object lblFileName: TLabel Left = 8 - Top = 423 + Top = 311 Width = 46 Height = 13 Caption = 'Model &ID:' @@ -22,7 +21,7 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters end object lblPath: TLabel Left = 8 - Top = 287 + Top = 149 Width = 26 Height = 13 Caption = '&Path:' @@ -30,7 +29,7 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters end object lblAuthorID: TLabel Left = 8 - Top = 342 + Top = 230 Width = 51 Height = 13 Caption = 'Aut&hor ID:' @@ -61,8 +60,8 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters FocusControl = editAuthor end object lblLanguages: TLabel - Left = 9 - Top = 146 + Left = 348 + Top = 114 Width = 52 Height = 13 Caption = '&Languages' @@ -70,14 +69,14 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters end object lblBCP47: TLabel Left = 8 - Top = 369 + Top = 257 Width = 90 Height = 13 Caption = '&Primary Language:' end object lblUniq: TLabel Left = 8 - Top = 396 + Top = 284 Width = 67 Height = 13 Caption = 'Uni&que Name:' @@ -93,13 +92,13 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters end object Bevel1: TBevel Left = 8 - Top = 317 - Width = 397 - Height = 2 + Top = 205 + Width = 317 + Height = 4 end object lblProjectFilename: TLabel Left = 8 - Top = 450 + Top = 338 Width = 77 Height = 13 Caption = 'Project &filename' @@ -113,40 +112,48 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters Caption = 'Fu&ll copyright:' FocusControl = editFullCopyright end + object Label2: TLabel + Left = 347 + Top = 10 + Width = 53 + Height = 13 + Caption = '&Description' + FocusControl = memoDescription + end object editModelID: TEdit Left = 120 - Top = 420 + Top = 308 Width = 205 Height = 21 TabStop = False ParentColor = True ReadOnly = True - TabOrder = 14 + TabOrder = 15 OnChange = editModelIDChange end object cmdBrowse: TButton - Left = 332 - Top = 284 + Left = 120 + Top = 173 Width = 73 Height = 21 Caption = '&Browse...' - TabOrder = 10 + TabOrder = 6 OnClick = cmdBrowseClick end object editPath: TEdit Left = 120 - Top = 284 + Top = 146 Width = 205 Height = 21 - TabOrder = 9 + TabOrder = 5 OnChange = editPathChange end object editAuthorID: TEdit Left = 120 - Top = 339 + Top = 227 Width = 205 Height = 21 - TabOrder = 11 + TabOrder = 12 OnChange = editModelIDComponentChange end object editCopyright: TEdit @@ -175,28 +182,28 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters OnChange = editAuthorChange end object cmdOK: TButton - Left = 252 - Top = 479 + Left = 302 + Top = 367 Width = 73 Height = 25 Caption = 'OK' Default = True - TabOrder = 16 + TabOrder = 17 OnClick = cmdOKClick end object cmdCancel: TButton - Left = 331 - Top = 479 + Left = 381 + Top = 367 Width = 73 Height = 25 Cancel = True Caption = 'Cancel' ModalResult = 2 - TabOrder = 17 + TabOrder = 18 end object gridLanguages: TStringGrid - Left = 8 - Top = 167 + Left = 347 + Top = 135 Width = 317 Height = 102 ColCount = 2 @@ -204,7 +211,7 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters FixedCols = 0 RowCount = 9 Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goRowSelect] - TabOrder = 5 + TabOrder = 8 OnClick = gridLanguagesClick OnDblClick = gridLanguagesDblClick ColWidths = ( @@ -212,47 +219,47 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters 64) end object cmdAddLanguage: TButton - Left = 332 - Top = 167 + Left = 671 + Top = 135 Width = 73 Height = 25 Caption = '&Add...' - TabOrder = 6 + TabOrder = 9 OnClick = cmdAddLanguageClick end object cmdEditLanguage: TButton - Left = 332 - Top = 198 + Left = 671 + Top = 166 Width = 73 Height = 25 Caption = '&Edit...' - TabOrder = 7 + TabOrder = 10 OnClick = cmdEditLanguageClick end object cmdRemoveLanguage: TButton - Left = 332 - Top = 229 + Left = 671 + Top = 197 Width = 73 Height = 25 Caption = '&Remove' - TabOrder = 8 + TabOrder = 11 OnClick = cmdRemoveLanguageClick end object editUniq: TEdit Left = 120 - Top = 393 + Top = 281 Width = 205 Height = 21 - TabOrder = 13 + TabOrder = 14 OnChange = editModelIDComponentChange end object cbBCP47: TComboBox Left = 120 - Top = 366 + Top = 254 Width = 205 Height = 21 Style = csDropDownList - TabOrder = 12 + TabOrder = 13 OnClick = editModelIDComponentChange end object editModelName: TEdit @@ -265,13 +272,13 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters end object editProjectFilename: TEdit Left = 120 - Top = 447 + Top = 335 Width = 284 Height = 21 TabStop = False ParentColor = True ReadOnly = True - TabOrder = 15 + TabOrder = 16 end object editFullCopyright: TEdit Left = 120 @@ -281,4 +288,12 @@ inherited frmNewModelProjectParameters: TfrmNewModelProjectParameters TabOrder = 3 OnChange = editFullCopyrightChange end + object memoDescription: TMemo + Left = 347 + Top = 31 + Width = 398 + Height = 79 + TabOrder = 7 + OnChange = memoDescriptionChange + end end diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas index 052c71d7fe5..a8f3e02ec4e 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewModelProjectParameters.pas @@ -58,6 +58,8 @@ TfrmNewModelProjectParameters = class(TTikeForm) editProjectFilename: TEdit; Label1: TLabel; editFullCopyright: TEdit; + Label2: TLabel; + memoDescription: TMemo; procedure cmdOKClick(Sender: TObject); procedure editModelIDComponentChange(Sender: TObject); procedure FormCreate(Sender: TObject); @@ -75,6 +77,7 @@ TfrmNewModelProjectParameters = class(TTikeForm) procedure cmdBrowseClick(Sender: TObject); procedure editModelNameChange(Sender: TObject); procedure editFullCopyrightChange(Sender: TObject); + procedure memoDescriptionChange(Sender: TObject); private pack: TKPSFile; FSetup: Integer; @@ -100,6 +103,7 @@ TfrmNewModelProjectParameters = class(TTikeForm) procedure UpdateModelIDFromComponents; procedure UpdateProjectFilename; function GetFullCopyright: string; + function GetDescription: string; protected function GetHelpTopic: string; override; property AuthorID: string read GetAuthorID; @@ -110,6 +114,7 @@ TfrmNewModelProjectParameters = class(TTikeForm) property FullCopyright: string read GetFullCopyright; property Version: string read GetVersion; property Author: string read GetAuthor; + property Description: string read GetDescription; property ModelName: string read GetModelName; property BCP47Tags: string read GetBCP47Tags; property BasePath: string read GetBasePath; @@ -153,6 +158,7 @@ function ShowNewModelProjectParameters(Owner: TComponent): Boolean; pt.Name := f.ModelName; pt.Copyright := f.Copyright; pt.FullCopyright := f.FullCopyright; + pt.Description := f.Description; pt.Author := f.Author; pt.Version := f.Version; pt.BCP47Tags := f.BCP47Tags; @@ -352,6 +358,7 @@ procedure TfrmNewModelProjectParameters.EnableControls; begin e := not Author.IsEmpty and + not Description.IsEmpty and not ModelName.IsEmpty and not AuthorID.IsEmpty and not PrimaryBCP47.IsEmpty and @@ -390,6 +397,11 @@ function TfrmNewModelProjectParameters.GetCopyright: string; Result := Trim(editCopyright.Text); end; +function TfrmNewModelProjectParameters.GetDescription: string; +begin + Result := memoDescription.Text; +end; + function TfrmNewModelProjectParameters.GetFullCopyright: string; begin Result := editFullCopyright.Text; @@ -474,8 +486,8 @@ function TfrmNewModelProjectParameters.Validate: Boolean; procedure TfrmNewModelProjectParameters.UpdateAuthorIDFromAuthor; begin editAuthorID.Text := TLexicalModelUtils.CleanLexicalModelIDComponent(Author); - editCopyright.Text := Char($00A9 {copyright})+' '+Author; - editFullCopyright.Text := Char($00A9 {copyright})+' '+FormatDateTime('yyyy', Now)+' '+Author; + editCopyright.Text := 'Copyright ' + Char($00A9 {copyright})+' '+Author; + editFullCopyright.Text := 'Copyright ' + Char($00A9 {copyright})+' '+FormatDateTime('yyyy', Now)+' '+Author; end; procedure TfrmNewModelProjectParameters.UpdateUniqFromModelName; @@ -556,6 +568,11 @@ procedure TfrmNewModelProjectParameters.LanguageGrid_Fill; end; end; +procedure TfrmNewModelProjectParameters.memoDescriptionChange(Sender: TObject); +begin + EnableControls; +end; + procedure TfrmNewModelProjectParameters.BCP47_Fill; var lm: TPackageLexicalModel; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm index 9d3c9b9ad6c..d14234be724 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.dfm @@ -42,15 +42,12 @@ inherited frmNewProject: TfrmNewProject FullDrag = True HideSelection = False Items.ItemData = { - 05160100000500000001000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000 - 00054200610073006900630000000000FFFFFFFFFFFFFFFF00000000FFFFFFFF - 000000000542006C0061006E006B0002000000FFFFFFFFFFFFFFFF00000000FF - FFFFFF000000001657006F00720064006C0069007300740020004C0065007800 - 6900630061006C0020004D006F00640065006C0000000000FFFFFFFFFFFFFFFF - 00000000FFFFFFFF000000001342006C0061006E006B0020004C006500780069 - 00630061006C0020004D006F00640065006C0000000000FFFFFFFFFFFFFFFF00 - 000000FFFFFFFF000000001749006D0070006F00720074002000570069006E00 - 64006F007700730020004B006500790062006F00610072006400} + 05B20000000300000001000000FFFFFFFFFFFFFFFF00000000FFFFFFFF000000 + 00054200610073006900630002000000FFFFFFFFFFFFFFFF00000000FFFFFFFF + 000000001657006F00720064006C0069007300740020004C0065007800690063 + 0061006C0020004D006F00640065006C0000000000FFFFFFFFFFFFFFFF000000 + 00FFFFFFFF000000001749006D0070006F00720074002000570069006E006400 + 6F007700730020004B006500790062006F00610072006400} LargeImages = ilLarge SmallImages = ilSmall TabOrder = 0 @@ -86,7 +83,7 @@ inherited frmNewProject: TfrmNewProject Left = 56 Top = 292 Bitmap = { - 494C010103000800400020002000FFFFFF002110FFFFFFFFFFFFFFFF424D3600 + 494C010103000800040020002000FFFFFF002110FFFFFFFFFFFFFFFF424D3600 0000000000003600000028000000800000002000000001002000000000000040 0000000000000000000000000000000000000000000000000000000000000000 00004D4D4DAFB2B2B1F9B2B2B1F9B1B1B0F8B1B1B0F8B1B1B0F8B1B1B0F8B1B1 @@ -628,7 +625,7 @@ inherited frmNewProject: TfrmNewProject Left = 18 Top = 292 Bitmap = { - 494C010103000800400010001000FFFFFF002110FFFFFFFFFFFFFFFF424D3600 + 494C010103000800040010001000FFFFFF002110FFFFFFFFFFFFFFFF424D3600 0000000000003600000028000000400000001000000001002000000000000010 000000000000000000000000000000000000000000003F3F3F97C8C8C8FBC8C8 C8FBC8C8C8FBC8C8C8FBC8C8C8FBC8C8C8FBC8C8C8FBC8C8C8FBC8C8C8FBC8C8 diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas index f1dfe559d0e..39fb79eeaa5 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProject.pas @@ -110,16 +110,6 @@ function ShowNewProjectForm(Owner: TComponent): Boolean; Result := ShowNewProjectParameters(Owner); kptWordlistLexicalModel: Result := ShowNewModelProjectParameters(Owner); - kptBlankLexicalModel: - begin - modActionsMain.NewProject(ptLexicalModel); - Result := True; - end; - kptBlank: - begin - modActionsMain.NewProject(ptKeyboard); - Result := True; - end; kptImportWindowsKeyboard: Result := ShowImportWindowsKeyboard(Owner); end; @@ -152,12 +142,8 @@ function TfrmNewProject.GetProjectType: TKeymanProjectType; Result := kptUnknown else if lvItems.Selected.Caption = 'Basic' then Result := kptBasic - else if lvItems.Selected.Caption = 'Blank' then - Result := kptBlank else if lvItems.Selected.Caption = 'Import Windows Keyboard' then Result := kptImportWindowsKeyboard - else if lvItems.Selected.Caption = 'Blank Lexical Model' then - Result := kptBlankLexicalModel else if lvItems.Selected.Caption = 'Wordlist Lexical Model' then Result := kptWordlistLexicalModel else @@ -190,13 +176,9 @@ procedure TfrmNewProject.UpdateDescription; 'Creates a keyboard project that matches the folder structure used in '+ 'the Keyman keyboards repository and includes all basic files '+ 'needed for a keyboard project.'; - kptBlank: lblDescription.Caption := - 'Creates a blank, untitled keyboard project.'; kptImportWindowsKeyboard: lblDescription.Caption := 'Creates a new keyboard project, importing from a Windows system keyboard '+ 'and generating all the basic files needed for a keyboard project.'; - kptBlankLexicalModel: lblDescription.Caption := - 'Creates a blank, untitled lexical model project.'; kptWordlistLexicalModel: lblDescription.Caption := 'Creates a new lexical model project that matches the folder structure used in '+ 'the Keyman lexical-models repository and includes all basic files '+ diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.dfm index 16c40a10e68..c172f52cdc4 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.dfm +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.dfm @@ -2,17 +2,17 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters BorderIcons = [biSystemMenu] BorderStyle = bsDialog Caption = 'New Basic Keyboard Project' - ClientHeight = 330 + ClientHeight = 444 ClientWidth = 625 Position = poScreenCenter OnDestroy = FormDestroy ExplicitWidth = 631 - ExplicitHeight = 359 + ExplicitHeight = 473 PixelsPerInch = 96 TextHeight = 13 object lblFileName: TLabel Left = 12 - Top = 275 + Top = 390 Width = 64 Height = 13 Caption = '&Keyboard ID:' @@ -20,7 +20,7 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object lblPath: TLabel Left = 12 - Top = 248 + Top = 363 Width = 26 Height = 13 Caption = '&Path:' @@ -36,7 +36,7 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object lblCoypright: TLabel Left = 12 - Top = 65 + Top = 180 Width = 51 Height = 13 Caption = '&Copyright:' @@ -44,7 +44,7 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object lblVersion: TLabel Left = 12 - Top = 118 + Top = 233 Width = 39 Height = 13 Caption = '&Version:' @@ -52,7 +52,7 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object lblAuthor: TLabel Left = 12 - Top = 38 + Top = 153 Width = 37 Height = 13 Caption = 'A&uthor:' @@ -60,7 +60,7 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object lblTargets: TLabel Left = 12 - Top = 145 + Top = 260 Width = 41 Height = 13 Caption = '&Targets:' @@ -68,7 +68,7 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object lblKeyboardLanguages: TLabel Left = 339 - Top = 11 + Top = 152 Width = 52 Height = 13 Caption = '&Languages' @@ -76,7 +76,7 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object lblProjectFilename: TLabel Left = 12 - Top = 302 + Top = 417 Width = 77 Height = 13 Caption = 'Project &filename' @@ -84,35 +84,43 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object Label1: TLabel Left = 12 - Top = 91 + Top = 206 Width = 68 Height = 13 Caption = 'Fu&ll copyright:' FocusControl = editFullCopyright end + object Label2: TLabel + Left = 12 + Top = 38 + Width = 53 + Height = 13 + Caption = '&Description' + FocusControl = memoDescription + end object editKeyboardID: TEdit Left = 120 - Top = 272 + Top = 387 Width = 205 Height = 21 - TabOrder = 12 + TabOrder = 13 OnChange = editKeyboardIDChange end object cmdBrowse: TButton Left = 340 - Top = 245 + Top = 360 Width = 73 Height = 21 Caption = '&Browse...' - TabOrder = 11 + TabOrder = 12 OnClick = cmdBrowseClick end object editPath: TEdit Left = 120 - Top = 245 + Top = 360 Width = 205 Height = 21 - TabOrder = 10 + TabOrder = 11 OnChange = editPathChange end object editKeyboardName: TEdit @@ -125,70 +133,70 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object editCopyright: TEdit Left = 120 - Top = 62 + Top = 176 Width = 205 Height = 21 - TabOrder = 2 - Text = #169 + TabOrder = 3 + Text = 'Copyright '#169 OnChange = editCopyrightChange end object editVersion: TEdit Left = 120 - Top = 115 + Top = 230 Width = 205 Height = 21 - TabOrder = 4 + TabOrder = 5 Text = '1.0' OnChange = editVersionChange end object editAuthor: TEdit Left = 120 - Top = 35 + Top = 150 Width = 205 Height = 21 - TabOrder = 1 + TabOrder = 2 OnChange = editAuthorChange end object cmdOK: TButton Left = 463 - Top = 297 + Top = 412 Width = 73 Height = 25 Caption = 'OK' Default = True - TabOrder = 14 + TabOrder = 15 OnClick = cmdOKClick end object cmdCancel: TButton Left = 542 - Top = 297 + Top = 412 Width = 73 Height = 25 Cancel = True Caption = 'Cancel' ModalResult = 2 - TabOrder = 15 + TabOrder = 16 end object clbTargets: TCheckListBox Left = 120 - Top = 142 + Top = 257 Width = 205 Height = 97 OnClickCheck = clbTargetsClickCheck ItemHeight = 13 - TabOrder = 5 + TabOrder = 6 end object gridKeyboardLanguages: TStringGrid Left = 339 - Top = 32 + Top = 173 Width = 278 - Height = 153 + Height = 120 ColCount = 2 DefaultRowHeight = 16 FixedCols = 0 RowCount = 9 Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing, goRowSelect] - TabOrder = 6 + TabOrder = 7 OnClick = gridKeyboardLanguagesClick OnDblClick = gridKeyboardLanguagesDblClick ColWidths = ( @@ -197,49 +205,57 @@ inherited frmNewProjectParameters: TfrmNewProjectParameters end object cmdKeyboardAddLanguage: TButton Left = 340 - Top = 191 + Top = 299 Width = 73 Height = 25 Caption = '&Add...' - TabOrder = 7 + TabOrder = 8 OnClick = cmdKeyboardAddLanguageClick end object cmdKeyboardEditLanguage: TButton Left = 419 - Top = 191 + Top = 299 Width = 73 Height = 25 Caption = 'Ed&it...' - TabOrder = 8 + TabOrder = 9 OnClick = cmdKeyboardEditLanguageClick end object cmdKeyboardRemoveLanguage: TButton Left = 498 - Top = 191 + Top = 299 Width = 72 Height = 25 Caption = '&Remove' - TabOrder = 9 + TabOrder = 10 OnClick = cmdKeyboardRemoveLanguageClick end object editProjectFilename: TEdit Left = 120 - Top = 299 + Top = 414 Width = 293 Height = 21 TabStop = False ParentColor = True ReadOnly = True - TabOrder = 13 + TabOrder = 14 OnChange = editKeyboardIDChange end object editFullCopyright: TEdit Left = 120 - Top = 88 + Top = 203 Width = 205 Height = 21 - TabOrder = 3 - Text = #169' YYYY' + TabOrder = 4 + Text = 'Copyright '#169' YYYY' OnChange = editFullCopyrightChange end + object memoDescription: TMemo + Left = 120 + Top = 35 + Width = 497 + Height = 109 + TabOrder = 1 + OnChange = memoDescriptionChange + end end diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas index c9176f5d24c..0756f5604fd 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmNewProjectParameters.pas @@ -51,6 +51,8 @@ TfrmNewProjectParameters = class(TTikeForm) editProjectFilename: TEdit; Label1: TLabel; editFullCopyright: TEdit; + Label2: TLabel; + memoDescription: TMemo; procedure cmdOKClick(Sender: TObject); procedure editKeyboardNameChange(Sender: TObject); procedure FormCreate(Sender: TObject); @@ -68,6 +70,7 @@ TfrmNewProjectParameters = class(TTikeForm) procedure editKeyboardIDChange(Sender: TObject); procedure cmdBrowseClick(Sender: TObject); procedure editFullCopyrightChange(Sender: TObject); + procedure memoDescriptionChange(Sender: TObject); private dlgBrowse: TBrowse4Folder; pack: TKPSFile; // Used temporarily for storing language list @@ -90,6 +93,7 @@ TfrmNewProjectParameters = class(TTikeForm) procedure SetKeyboardName(const Value: string); procedure UpdateProjectFilename; function GetFullCopyright: string; + function GetDescription: string; protected function GetHelpTopic: string; override; public @@ -101,6 +105,8 @@ TfrmNewProjectParameters = class(TTikeForm) property BCP47Tags: string read GetBCP47Tags write SetBCP47Tags; property BasePath: string read GetBasePath; + property Description: string read GetDescription; + property KeyboardName: string read GetKeyboardName write SetKeyboardName; property KeyboardID: string read GetKeyboardID write SetKeyboardID; end; @@ -132,6 +138,7 @@ implementation // 6. author // 7. version // 8. bcp47 tags +// 9. description {$R *.dfm} @@ -153,6 +160,7 @@ function ShowNewProjectParameters(Owner: TComponent): Boolean; pt.FullCopyright := f.FullCopyright; pt.Author := f.Author; pt.Version := f.Version; + pt.Description := f.Description; pt.BCP47Tags := f.BCP47Tags; pt.IncludeIcon := True; @@ -185,7 +193,7 @@ procedure TfrmNewProjectParameters.FormCreate(Sender: TObject); begin inherited; editPath.Text := FKeymanDeveloperOptions.DefaultProjectPath; - editFullCopyright.Text := Char($00A9 {copyright})+' '+FormatDateTime('yyyy', Now)+' '; + editFullCopyright.Text := 'Copyright '+Char($00A9 {copyright})+' '+FormatDateTime('yyyy', Now)+' '; dlgBrowse := TBrowse4Folder.Create(Self); dlgBrowse.InitialDir := editPath.Text; @@ -307,9 +315,9 @@ procedure TfrmNewProjectParameters.editAuthorChange(Sender: TObject); begin EnableControls; if not editCopyright.Modified then - editCopyright.Text := Char($00A9 {copyright})+' '+Author; + editCopyright.Text := 'Copyright '+Char($00A9 {copyright})+' '+Author; if not editFullCopyright.Modified then - editFullCopyright.Text := Char($00A9 {copyright})+' '+FormatDateTime('yyyy', Now)+' '+Author; + editFullCopyright.Text := 'Copyright '+Char($00A9 {copyright})+' '+FormatDateTime('yyyy', Now)+' '+Author; end; procedure TfrmNewProjectParameters.editCopyrightChange(Sender: TObject); @@ -353,7 +361,8 @@ procedure TfrmNewProjectParameters.EnableControls; e := (Trim(editKeyboardName.Text) <> '') and (Trim(editPath.Text) <> '') and TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True) and - (GetTargets <> []); + (GetTargets <> []) and + (Trim(memoDescription.Text) <> ''); cmdOK.Enabled := e; @@ -380,6 +389,11 @@ function TfrmNewProjectParameters.GetCopyright: string; Result := Trim(editCopyright.Text); end; +function TfrmNewProjectParameters.GetDescription: string; +begin + Result := Trim(memoDescription.Text); +end; + function TfrmNewProjectParameters.GetFullCopyright: string; begin Result := Trim(editFullCopyright.Text); @@ -563,6 +577,11 @@ procedure TfrmNewProjectParameters.LanguageGrid_Fill; end; end; +procedure TfrmNewProjectParameters.memoDescriptionChange(Sender: TObject); +begin + EnableControls; +end; + procedure TfrmNewProjectParameters.UpdateProjectFilename; begin editProjectFilename.Text := diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProject.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProject.pas index 992a4491c36..ba73eaf68cf 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProject.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProject.pas @@ -109,9 +109,11 @@ implementation Keyman.Developer.System.Project.kmnProjectFile, Keyman.Developer.System.Project.kpsProjectFile, Keyman.Developer.System.Project.modelTsProjectFile, + Keyman.Developer.System.Project.xmlLdmlProjectFile, Keyman.Developer.System.Project.Project, Keyman.Developer.UI.Project.ProjectUI, Keyman.Developer.UI.Project.ProjectFileUI, + keyman.Developer.UI.Project.UpgradeProject, Keyman.Developer.System.Project.ProjectFileType, typinfo, ErrorControlledRegistry, @@ -286,14 +288,18 @@ procedure TfrmProject.CompileAll; ClearMessages; for i := 0 to FGlobalProject.Files.Count - 1 do begin - if (FGlobalProject.Files[i] is TkmnProjectFile) or - (FGlobalProject.Files[i] is TmodelTsProjectFile) then + if not FGlobalProject.Files[i].IsSourceFile then + Continue; + if FGlobalProject.Files[i].IsCompilable and + not (FGlobalProject.Files[i] is TkpsProjectFile) 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 not FGlobalProject.Files[i].IsSourceFile then + Continue; if FGlobalProject.Files[i] is TkpsProjectFile then if not (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaCompile, False) then Exit; // I4687 end; @@ -343,6 +349,8 @@ procedure TfrmProject.WebCommandWelcome(Command: WideString; Params: TStringList modActionsMain.actProjectNew.Execute else if Command = 'openproject' then modActionsMain.actProjectOpen.Execute + else if Command = 'openprojectfolder' then + modActionsMain.actProjectOpenFolder.Execute else if Command = 'editfile' then // MRU begin if SelectedMRUFileName <> '' then @@ -401,6 +409,7 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList if Command = 'fileaddnew' then begin { create a new file, add it to the project } + Assert(FGlobalProject.Options.Version = pv10); with TfrmNewFileDetails.Create(Self) do try BaseFileName := FGlobalProject.FileName; @@ -418,6 +427,7 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList else if Command = 'fileaddexisting' then begin { locate an existing file, add it to the project } + Assert(FGlobalProject.Options.Version = pv10); FFileType := FileTypeFromParamType; dlgOpenFile.Filter := GetFileTypeFilter(FFileType, FDefaultExtension); @@ -465,6 +475,8 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList pf := SelectedProjectFile; if Assigned(pf) and (pf is TkmnProjectFile) then OpenContainingFolder((pf as TkmnProjectFile).TargetFileName) + else if Assigned(pf) and (pf is TxmlLdmlProjectFile) then + OpenContainingFolder((pf as TxmlLdmlProjectFile).TargetFileName) else if Assigned(pf) and (pf is TkpsProjectFile) then OpenContainingFolder((pf as TkpsProjectFile).TargetFileName) else if Assigned(pf) and (pf is TmodelTsProjectFile) then @@ -476,6 +488,7 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList end else if Command = 'removefile' then begin + Assert(FGlobalProject.Options.Version = pv10); pf := SelectedProjectFile; if Assigned(pf) then begin @@ -500,6 +513,8 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList ClearMessages; for i := 0 to FGlobalProject.Files.Count - 1 do begin + if not FGlobalProject.Files[i].IsSourceFile then + Continue; (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaClean, False); // I4687 end; end @@ -508,7 +523,11 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList ClearMessages; for i := 0 to FGlobalProject.Files.Count - 1 do begin - if FGlobalProject.Files[i] is TkmnProjectFile then + if not FGlobalProject.Files[i].IsSourceFile then + Continue; + + if (FGlobalProject.Files[i] is TkmnProjectFile) or + (FGlobalProject.Files[i] is TxmlLdmlProjectFile) then (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaCompile, False); // I4687 end; end @@ -517,7 +536,10 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList ClearMessages; for i := 0 to FGlobalProject.Files.Count - 1 do begin - if FGlobalProject.Files[i] is TkmnProjectFile then + if not FGlobalProject.Files[i].IsSourceFile then + Continue; + if (FGlobalProject.Files[i] is TkmnProjectFile) or + (FGlobalProject.Files[i] is TxmlLdmlProjectFile) then (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaClean, False); end; end @@ -526,6 +548,8 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList ClearMessages; for i := 0 to FGlobalProject.Files.Count - 1 do begin + if not FGlobalProject.Files[i].IsSourceFile then + Continue; if FGlobalProject.Files[i] is TmodelTsProjectFile then (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaCompile, False); // I4687 end; @@ -535,6 +559,8 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList ClearMessages; for i := 0 to FGlobalProject.Files.Count - 1 do begin + if not FGlobalProject.Files[i].IsSourceFile then + Continue; if FGlobalProject.Files[i] is TmodelTsProjectFile then (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaClean, False); end; @@ -545,6 +571,8 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList ClearMessages; for i := 0 to FGlobalProject.Files.Count - 1 do begin + if not FGlobalProject.Files[i].IsSourceFile then + Continue; if FGlobalProject.Files[i] is TkpsProjectFile then (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaCompile, False); end; @@ -554,6 +582,8 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList ClearMessages; for i := 0 to FGlobalProject.Files.Count - 1 do begin + if not FGlobalProject.Files[i].IsSourceFile then + Continue; if FGlobalProject.Files[i] is TkpsProjectFile then (FGlobalProject.Files[i].UI as TProjectFileUI).DoAction(pfaClean, False); end; @@ -570,6 +600,11 @@ procedure TfrmProject.WebCommandProject(Command: WideString; Params: TStringList pf := SelectedProjectFile; if Assigned(pf) then (pf.UI as TProjectFileUI).DoAction(pfaCompile, False); end + else if Command = 'upgradeproject' then + begin + TryUpgradeProject(FGlobalProject); + ProjectRefresh(nil); + end else if Command = 'checkforupdates' then begin modActionsMain.actHelpCheckForUpdates.Execute; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.dfm b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.dfm new file mode 100644 index 00000000000..d36183334d3 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.dfm @@ -0,0 +1,122 @@ +object frmProjectSettings20: TfrmProjectSettings20 + Left = 0 + Top = 0 + BorderStyle = bsDialog + Caption = 'Project Settings' + ClientHeight = 219 + ClientWidth = 418 + Color = clBtnFace + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + OldCreateOrder = False + Position = poScreenCenter + OnCreate = FormCreate + PixelsPerInch = 96 + TextHeight = 13 + object lblOutputPath: TLabel + Left = 16 + Top = 19 + Width = 59 + Height = 13 + Caption = '&Output path' + FocusControl = editOutputPath + end + object Label2: TLabel + Left = 81 + Top = 73 + Width = 64 + Height = 13 + Caption = 'Placeholders:' + end + object Label3: TLabel + Left = 151 + Top = 73 + Width = 77 + Height = 13 + Caption = '$PROJECTPATH' + end + object Label6: TLabel + Left = 248 + Top = 73 + Width = 122 + Height = 13 + Caption = 'Path that the project is in' + end + object lblSourcePath: TLabel + Left = 16 + Top = 46 + Width = 58 + Height = 13 + Caption = '&Source path' + FocusControl = editSourcePath + end + object editOutputPath: TEdit + Left = 81 + Top = 16 + Width = 320 + Height = 21 + TabOrder = 0 + end + object cmdOK: TButton + Left = 131 + Top = 186 + Width = 75 + Height = 25 + Caption = 'OK' + Default = True + TabOrder = 6 + OnClick = cmdOKClick + end + object cmdCancel: TButton + Left = 212 + Top = 186 + Width = 75 + Height = 25 + Cancel = True + Caption = 'Cancel' + ModalResult = 2 + TabOrder = 7 + end + object chkCompilerWarningsAsErrors: TCheckBox + Left = 81 + Top = 114 + Width = 232 + Height = 17 + Caption = 'Treat compiler &hints and warnings as errors' + TabOrder = 3 + end + object chkWarnDeprecatedCode: TCheckBox + Left = 81 + Top = 137 + Width = 160 + Height = 17 + Caption = 'Warn on &deprecated code' + TabOrder = 4 + end + object chkCheckFilenameConventions: TCheckBox + Left = 81 + Top = 160 + Width = 160 + Height = 17 + Caption = '&Check filename conventions' + TabOrder = 5 + end + object editSourcePath: TEdit + Left = 81 + Top = 43 + Width = 320 + Height = 21 + TabOrder = 1 + end + object chkBuildMetadataFiles: TCheckBox + Left = 81 + Top = 92 + Width = 232 + Height = 17 + Caption = 'Build &metadata files for Keyman Cloud' + TabOrder = 2 + end +end diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.pas new file mode 100644 index 00000000000..2bda4a3bb66 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.pas @@ -0,0 +1,82 @@ +(* + Name: Keyman.Developer.UI.Project.UfrmProjectSettings + Copyright: Copyright (C) SIL International. + Documentation: + Description: + Create Date: 4 May 2015 + + Modified Date: 24 Aug 2015 + Authors: mcdurdin + Related Files: + Dependencies: + + Bugs: + Todo: + Notes: + History: 04 May 2015 - mcdurdin - I4688 - V9.0 - Add build path to project settings + 24 Aug 2015 - mcdurdin - I4865 - Add treat hints and warnings as errors into project + 24 Aug 2015 - mcdurdin - I4866 - Add warn on deprecated features to project and compile + +*) +unit Keyman.Developer.UI.Project.UfrmProjectSettings20; // I4688 + +interface + +uses + Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, + Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; + +type + TfrmProjectSettings20 = class(TForm) + lblOutputPath: TLabel; + editOutputPath: TEdit; + Label2: TLabel; + cmdOK: TButton; + cmdCancel: TButton; + Label3: TLabel; + Label6: TLabel; + chkCompilerWarningsAsErrors: TCheckBox; + chkWarnDeprecatedCode: TCheckBox; + chkCheckFilenameConventions: TCheckBox; + lblSourcePath: TLabel; + editSourcePath: TEdit; + chkBuildMetadataFiles: TCheckBox; + procedure FormCreate(Sender: TObject); + procedure cmdOKClick(Sender: TObject); + private + { Private declarations } + public + { Public declarations } + end; + +implementation + +{$R *.dfm} + +uses + Keyman.Developer.System.Project.Project; + +procedure TfrmProjectSettings20.cmdOKClick(Sender: TObject); +begin + FGlobalProject.Options.BuildPath := Trim(editOutputPath.Text); + FGlobalProject.Options.SourcePath := Trim(editSourcePath.Text); + FGlobalProject.Options.SkipMetadataFiles := not chkBuildMetadataFiles.Checked; + FGlobalProject.Options.CompilerWarningsAsErrors := chkCompilerWarningsAsErrors.Checked; // I4865 + FGlobalProject.Options.WarnDeprecatedCode := chkWarnDeprecatedCode.Checked; // I4866 + FGlobalProject.Options.CheckFilenameConventions := chkCheckFilenameConventions.Checked; + FGlobalProject.Save; + ModalResult := mrOk; +end; + +procedure TfrmProjectSettings20.FormCreate(Sender: TObject); +begin + editOutputPath.Text := FGlobalProject.Options.BuildPath; + editSourcePath.Text := FGlobalProject.Options.SourcePath; + chkBuildMetadataFiles.Checked := not FGlobalProject.Options.SkipMetadataFiles; + chkCompilerWarningsAsErrors.Checked := FGlobalProject.Options.CompilerWarningsAsErrors; // I4865 + chkWarnDeprecatedCode.Checked := FGlobalProject.Options.WarnDeprecatedCode; // I4866 + chkCheckFilenameConventions.Checked := FGlobalProject.Options.CheckFilenameConventions; + if editOutputPath.Text = '' then editOutputPath.Text := '$PROJECTPATH'; +end; + +end. diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UpgradeProject.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UpgradeProject.pas new file mode 100644 index 00000000000..fe4ae171378 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UpgradeProject.pas @@ -0,0 +1,76 @@ +unit Keyman.Developer.UI.Project.UpgradeProject; + +interface + +uses + Keyman.Developer.System.Project.ProjectFile; + +type + TUpgradeResult = (urNoAction, urUpgraded, urCancelled); + +function TryUpgradeProject(Project: TProject): TUpgradeResult; + +implementation + +uses + System.Classes, + System.UITypes, + Vcl.Controls, + Vcl.Dialogs, + + UfrmMessages, + Keyman.Developer.System.Project.ProjectLog, + KeymanDeveloperOptions; + +function TryUpgradeProject(Project: TProject): TUpgradeResult; +var + msg: string; +begin + Result := urNoAction; + + if Project.Options.Version = pv20 then + begin + // We are already up to date + Exit; + end; + +{ TODO: + if not FKeymanDeveloperOptions.PromptForProjectUpgrade then + begin + // User wishes to stick with v1.0 projects + Exit; + end; +} + + frmMessages.Clear; + + if not Project.CanUpgrade then + begin + // Project has restrictions, such as files in wrong folders, so + // we cannot upgrade. Show a message for the user + for msg in Project.UpgradeMessages do + begin + Project.Log(plsError, Project.FileName, msg, 0, 0); + end; + MessageDlg('Some issues must be addressed before the current project can '+ + 'be upgraded to Keyman Developer 17 format. '+ + 'Details of the issues are listed in the Messages panel.', mtError, [mbOk], 0); + Exit; + end; + + case MessageDlg('The current project can be upgraded to Keyman Developer 17.0 format. '+ + 'Do you wish to upgrade it (recommended)?'#13#10#13#10+ + 'Note: upgraded projects will not be readable by older versions of Keyman Developer.', + mtConfirmation, mbYesNoCancel, 0) of + mrNo: Exit; + mrCancel: Exit(urCancelled); + end; + + // .. do the upgrade + + Project.Upgrade; + + Result := urUpgraded; +end; + +end. diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.xmlLdmlProjectFileUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.xmlLdmlProjectFileUI.pas new file mode 100644 index 00000000000..e4abe152010 --- /dev/null +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.xmlLdmlProjectFileUI.pas @@ -0,0 +1,275 @@ +{ + * Keyman is copyright (C) SIL International. MIT License. + * + * xmlLdmlProjectFileUI: User Interface connections for LDML keyboard files +} +unit Keyman.Developer.UI.Project.xmlLdmlProjectFileUI; + +interface + +uses + System.UITypes, + Menus, + Keyman.Developer.UI.Project.ProjectFilesUI, + Keyman.Developer.UI.Project.ProjectFileUI, + Keyman.Developer.UI.Project.ProjectUIFileType, + UfrmMessages, + Keyman.Developer.System.Project.xmlLdmlProjectFile, + Keyman.Developer.System.Project.xmlLdmlProjectFileAction; + +type + TxmlLdmlProjectFileUI = class(TOpenableProjectFileUI) + private + function TestKeymanWeb(FSilent: Boolean): Boolean; + function InstallKeyboard: Boolean; + function UninstallKeyboard: Boolean; + function GetProjectFile: TxmlLdmlProjectFileAction; + + function GetDebug: Boolean; + procedure SetDebug(const Value: Boolean); + function CompileKeyboard(FSilent: Boolean): Boolean; + function TestKeyboardState(FCompiledName: string; FSilent: Boolean): Boolean; + public + function DoAction(action: TProjectFileAction; FSilent: Boolean): Boolean; override; + property Debug: Boolean read GetDebug write SetDebug; + property ProjectFile: TxmlLdmlProjectFileAction read GetProjectFile; + end; + +implementation + +uses + Winapi.Windows, + System.Classes, + System.StrUtils, + System.SysUtils, + System.Variants, + Vcl.Dialogs, + Vcl.Graphics, + Vcl.Controls, + dmActionsMain, + + Keyman.Developer.UI.UfrmMessageDlgWithSave, + UfrmMain, + Keyman.Developer.UI.UfrmLdmlKeyboardEditor, + UfrmMDIEditor, + UKeymanTargets, + UmodWebHttpServer, + Keyman.Developer.System.ServerAPI, + Keyman.Developer.UI.ServerUI, + KeyboardFonts, + KeymanDeveloperUtils, + KeymanDeveloperOptions, + utilsystem; + +function TxmlLdmlProjectFileUI.DoAction(action: TProjectFileAction; FSilent: Boolean): Boolean; +begin + case action of + pfaCompile: Result := CompileKeyboard(FSilent); + pfaTestKeymanWeb: Result := TestKeymanWeb(FSilent); + pfaInstall: Result := InstallKeyboard; + pfaUninstall: Result := UninstallKeyboard; +// pfaDebug: Result := DebugKeyboard(FSilent); + pfaClean: Result := ProjectFile.Clean; + else + Result := False; + end; +end; + +function TxmlLdmlProjectFileUI.CompileKeyboard(FSilent: Boolean): Boolean; +var + FSave: Boolean; +begin + Result := False; + + if ProjectFile.Modified then + begin + if not FSilent then + begin + if not FKeymanDeveloperOptions.AutoSaveBeforeCompiling then + begin + if TfrmMessageDlgWithSave.Execute( + 'The keyboard file has been modified. You must save before compiling.'+#13#10#13#10+ + 'Save the keyboard and continue?', + 'Always save automatically before compiling', + '', True, FSave) in [mrNo, mrCancel] then + Exit(False); + if FSave then + begin + FKeymanDeveloperOptions.AutoSaveBeforeCompiling := True; + FKeymanDeveloperOptions.Write; + end; + end; + + if not modActionsMain.actFileSave.Execute then Exit; + end + else + Exit; + end; + + if not FSilent then + frmMessages.DoShowForm; + + Result := + ProjectFile.CompileKeyboard; + + if Result and + TServerDebugAPI.Running and + TServerDebugAPI.IsKeyboardRegistered(ProjectFile.TargetFileName) and + (ProjectFile.Targets * KMWKeymanTargets <> []) then + TestKeymanWeb(True); +end; + +function TxmlLdmlProjectFileUI.GetDebug: Boolean; +begin + Result := ProjectFile.Debug; +end; + +function TxmlLdmlProjectFileUI.GetProjectFile: TxmlLdmlProjectFileAction; +begin + Result := FOwner as TxmlLdmlProjectFileAction; +end; + +procedure TxmlLdmlProjectFileUI.SetDebug(const Value: Boolean); +begin + ProjectFile.Debug := Value; +end; + +function TxmlLdmlProjectFileUI.TestKeymanWeb(FSilent: Boolean): Boolean; // I4409 +(*var + FCompiledName: string; + editor: TfrmTikeEditor; + wizard: TfrmEditor; + i: TKeyboardFont; + j: TKeyboardFont; + Found: Boolean; + + function IsStandardFont(const FontName: string): Boolean; // I4448 + const + StandardFontNames: array[0..9] of string = ( + 'Arial', 'Calibri', 'Consolas', 'Courier New', 'Lucida Console', 'Lucida Sans Unicode', 'Segoe UI', 'Tahoma', 'Times New Roman', 'Verdana' + ); + begin + Result := AnsiIndexText(FontName, StandardFontNames) >= 0; + end; + + procedure RegisterFont(const fontname: string); + var + strm: TMemoryStream; + begin + if (fontname <> '') and not IsStandardFont(fontname) then + begin + strm := TMemoryStream.Create; + try + if TFontLoadUtil.LoadFontData(fontname, strm) and + TServerDebugAPI.Running then + TServerDebugAPI.RegisterFont(strm, fontname); + finally + strm.Free; + end; + end; + end; +begin + editor := frmKeymanDeveloper.FindEditorByFileName(ProjectFile.FileName); // I4021 + if not Assigned(editor) or not (editor is TfrmKeymanWizard) then + Exit(False); + wizard := editor as TfrmKeymanWizard; + + if ProjectFile.Targets * KMWKeymanTargets = [] then + Exit(False); + + FCompiledName := ProjectFile.JSTargetFilename; + if FCompiledName = '' then + Exit(False); + + if not TestKeyboardState(FCompiledName, FSilent) then + Exit(False); + + // We register all fonts that are used by the layout, + // but just once for each reference! + for i := kfontChar to kfontTouchLayoutDesktop do + begin + Found := False; + for j := kfontChar to TKeyboardFont(Ord(i)-1) do + if Wizard.FontInfo[j].Name = Wizard.FontInfo[i].Name then + begin + Found := True; + Break; + end; + if not Found then + RegisterFont(Wizard.FontInfo[i].Name); + end; + + if TServerUI.VerifyServerRunning then + begin + TServerDebugAPI.RegisterKeyboard( + FCompiledName, + ProjectFile.FileVersion, + // We only need to specify the char + osk fonts here + // as the others are referenced in the touch layout definition directly + Wizard.FontInfo[kfontChar].Name, + Wizard.FontInfo[kfontOSK].Name + ); + + wizard.NotifyStartedWebDebug; // I4021 + end; +*) +begin + Result := True; +end; + +function TxmlLdmlProjectFileUI.InstallKeyboard: Boolean; +var + FCompiledName: string; +begin + Result := False; + FCompiledName := ProjectFile.TargetFilename; + if not TestKeyboardState(FCompiledName, False) then Exit; + KeymanDeveloperUtils.InstallKeyboard(FCompiledName, True); + Result := True; +end; + +function TxmlLdmlProjectFileUI.UninstallKeyboard: Boolean; +begin + Result := KeymanDeveloperUtils.UninstallKeyboard(ChangeFileExt(ExtractFileName(ProjectFile.FileName), '')); +end; + +function TxmlLdmlProjectFileUI.TestKeyboardState(FCompiledName: string; FSilent: Boolean): Boolean; +var + ftxml, ftkmx: TDateTime; +begin + Result := False; + + if not FileExists(FCompiledName) then + if FSilent then + begin + if not CompileKeyboard(FSilent) then Exit; + end + else + case MessageDlg('You need to compile the keyboard before you can continue. Compile now?', + mtConfirmation, mbOkCancel, 0) of + mrOk: if not CompileKeyboard(FSilent) then Exit; + mrCancel: Exit; + end; + + FileAge(ProjectFile.FileName, ftxml); + FileAge(FCompiledName, ftkmx); + + if ProjectFile.Modified or (ftxml > ftkmx) then + if FSilent then + begin + if not CompileKeyboard(FSilent) then Exit; + end + else + case MessageDlg('The source file has changed. Recompile before continuing?', + mtConfirmation, mbYesNoCancel, 0) of + mrYes: if not CompileKeyboard(FSilent) then Exit; + mrNo: ; + mrCancel: Exit; + end; + + Result := True; +end; + +initialization + RegisterProjectFileUIType(TxmlLdmlProjectFileAction, TxmlLdmlProjectFileUI); +end. diff --git a/developer/src/tike/tike.dpr b/developer/src/tike/tike.dpr index ef058746915..9a0ff51c9b8 100644 --- a/developer/src/tike/tike.dpr +++ b/developer/src/tike/tike.dpr @@ -31,7 +31,7 @@ uses Keyman.Developer.System.Project.kpsProjectFile in 'project\Keyman.Developer.System.Project.kpsProjectFile.pas', Keyman.Developer.System.Project.kmxProjectFile in 'project\Keyman.Developer.System.Project.kmxProjectFile.pas', Keyman.Developer.System.Project.modelTsProjectFile in 'project\Keyman.Developer.System.Project.modelTsProjectFile.pas', - Keyman.Developer.UI.Project.UfrmProjectSettings in 'project\Keyman.Developer.UI.Project.UfrmProjectSettings.pas' {frmProjectSettings}, + Keyman.Developer.UI.Project.UfrmProjectSettings20 in 'project\Keyman.Developer.UI.Project.UfrmProjectSettings20.pas' {frmProjectSettings20}, Keyman.Developer.UI.Project.ProjectFileUI in 'project\Keyman.Developer.UI.Project.ProjectFileUI.pas', Keyman.Developer.UI.Project.ProjectUI in 'project\Keyman.Developer.UI.Project.ProjectUI.pas', Keyman.Developer.UI.Project.ProjectUIFileType in 'project\Keyman.Developer.UI.Project.ProjectUIFileType.pas', @@ -151,7 +151,7 @@ uses ErrorControlledRegistry in '..\..\..\common\windows\delphi\vcl\ErrorControlledRegistry.pas', UframeBitmapEditor in 'main\UframeBitmapEditor.pas' {frameBitmapEditor: TFrame}, UfrmMessages in 'main\UfrmMessages.pas' {frmMessages}, - dmActionsKeyboardEditor in 'actions\dmActionsKeyboardEditor.pas' {modActionsKeyboardEditor: TDataModule}, + dmActionsDebugger in 'actions\dmActionsDebugger.pas' {modActionsDebugger: TDataModule}, dmActionsMain in 'actions\dmActionsMain.pas' {modActionsMain: TDataModule}, dmActionsTextEditor in 'actions\dmActionsTextEditor.pas' {modActionsTextEditor: TDataModule}, UfrmCharacterMapNew in '..\..\..\common\windows\delphi\charmap\UfrmCharacterMapNew.pas' {frmCharacterMapNew}, @@ -281,7 +281,14 @@ uses Keyman.Developer.UI.UfrmEditLanguageExample in 'dialogs\examples\Keyman.Developer.UI.UfrmEditLanguageExample.pas' {frmEditLanguageExample}, Keyman.Developer.UI.UfrmEditRelatedPackage in 'dialogs\relatedPackages\Keyman.Developer.UI.UfrmEditRelatedPackage.pas' {frmEditRelatedPackage}, Keyman.Developer.UI.UfrmEditPackageWebFonts in 'dialogs\packageWebFonts\Keyman.Developer.UI.UfrmEditPackageWebFonts.pas' {frmEditPackageWebFonts}, - Keyman.Developer.System.KmcWrapper in 'compile\Keyman.Developer.System.KmcWrapper.pas'; + Keyman.Developer.System.KmcWrapper in 'compile\Keyman.Developer.System.KmcWrapper.pas', + Keyman.Developer.UI.Project.UpgradeProject in 'project\Keyman.Developer.UI.Project.UpgradeProject.pas', + Keyman.Developer.System.Project.xmlLdmlProjectFile in 'project\Keyman.Developer.System.Project.xmlLdmlProjectFile.pas', + Keyman.Developer.System.Project.xmlLdmlProjectFileAction in 'project\Keyman.Developer.System.Project.xmlLdmlProjectFileAction.pas', + Keyman.Developer.UI.Project.xmlLdmlProjectFileUI in 'project\Keyman.Developer.UI.Project.xmlLdmlProjectFileUI.pas', + Keyman.Developer.UI.UfrmLdmlKeyboardEditor in 'child\Keyman.Developer.UI.UfrmLdmlKeyboardEditor.pas' {frmLdmlKeyboardEditor}, + dmActionsKeyboardEditor in 'actions\dmActionsKeyboardEditor.pas' {modActionsKeyboardEditor: TDataModule}, + Keyman.Developer.UI.Project.UfrmProjectSettings in 'project\Keyman.Developer.UI.Project.UfrmProjectSettings.pas' {frmProjectSettings}; {$R *.RES} {$R ICONS.RES} diff --git a/developer/src/tike/tike.dproj b/developer/src/tike/tike.dproj index 81d0469dca5..cb9b54134cd 100644 --- a/developer/src/tike/tike.dproj +++ b/developer/src/tike/tike.dproj @@ -162,8 +162,8 @@ - -
frmProjectSettings
+ +
frmProjectSettings20
@@ -345,8 +345,8 @@
frmMessages
- -
modActionsKeyboardEditor
+ +
modActionsDebugger
TDataModule
@@ -559,6 +559,23 @@ dfm + + + + + +
frmLdmlKeyboardEditor
+ dfm +
+ +
modActionsKeyboardEditor
+ dfm + TDataModule +
+ +
frmProjectSettings
+ dfm +
Cfg_2 diff --git a/developer/src/tike/xml/layoutbuilder/.gitignore b/developer/src/tike/xml/layoutbuilder/.gitignore new file mode 100644 index 00000000000..ede6194347b --- /dev/null +++ b/developer/src/tike/xml/layoutbuilder/.gitignore @@ -0,0 +1,2 @@ +# Copied from configure +keymanweb-osk.ttf diff --git a/developer/src/tike/xml/project/distribution.xsl b/developer/src/tike/xml/project/distribution.xsl index 998065b8427..840a3287c06 100644 --- a/developer/src/tike/xml/project/distribution.xsl +++ b/developer/src/tike/xml/project/distribution.xsl @@ -44,28 +44,32 @@ + +
New file... keyman:fileaddnew?type=text - - Add existing file... - keyman:fileaddexisting?type=text - + + + Add existing file... + keyman:fileaddexisting?type=text + + | Build all keyman:compileall - false + false Clean all keyman:cleanall - false + false auto @@ -74,7 +78,7 @@
- + @@ -86,7 +90,7 @@
- + diff --git a/developer/src/tike/xml/project/elements.xsl b/developer/src/tike/xml/project/elements.xsl index b624dcc055e..0bcf796cc29 100644 --- a/developer/src/tike/xml/project/elements.xsl +++ b/developer/src/tike/xml/project/elements.xsl @@ -2,6 +2,23 @@ + + + + + + + + + + @@ -254,4 +271,19 @@ + + + + +
+

⚠️ This project file is in an old format. You should upgrade it to the Keyman Developer 17.0 project format. + + Upgrade project + keyman:upgradeproject + +

+
+
+
+
\ No newline at end of file diff --git a/developer/src/tike/xml/project/globalwelcome.xsl b/developer/src/tike/xml/project/globalwelcome.xsl index aecd70d90ff..6f956bd2336 100644 --- a/developer/src/tike/xml/project/globalwelcome.xsl +++ b/developer/src/tike/xml/project/globalwelcome.xsl @@ -57,6 +57,10 @@ Open Existing Project... keyman:openproject + + Open Existing Project Folder... + keyman:openprojectfolder +
diff --git a/developer/src/tike/xml/project/keyboards.xsl b/developer/src/tike/xml/project/keyboards.xsl index 9da09d7df3a..f537e8df62e 100644 --- a/developer/src/tike/xml/project/keyboards.xsl +++ b/developer/src/tike/xml/project/keyboards.xsl @@ -68,30 +68,34 @@ + +
New keyboard... keyman:fileaddnew?type=keyboard auto - - Add existing keyboard... - keyman:fileaddexisting?type=keyboard - auto - + + + Add existing keyboard... + keyman:fileaddexisting?type=keyboard + auto + + | Build all keyman:compileall - false + false Clean all keyman:cleanall - false + false auto @@ -100,7 +104,7 @@ Build keyboards keyman:keyboard_compileall - false + false auto @@ -108,7 +112,7 @@ Clean keyboards keyman:keyboard_cleanall - false + false auto @@ -116,7 +120,7 @@
- + @@ -134,7 +138,7 @@
- + - + diff --git a/developer/src/tike/xml/project/models.xsl b/developer/src/tike/xml/project/models.xsl index a1a41286b99..a0bd9c894b5 100644 --- a/developer/src/tike/xml/project/models.xsl +++ b/developer/src/tike/xml/project/models.xsl @@ -56,17 +56,21 @@ + +
New model... keyman:fileaddnew?type=model auto - - Add existing model... - keyman:fileaddexisting?type=model - auto - + + + Add existing model... + keyman:fileaddexisting?type=model + auto + + | Build all @@ -104,7 +108,7 @@
- + @@ -158,11 +162,13 @@ keyman:openbuildfolder?id= - - Remove from Project - keyman:removefile?id= - - + + + Remove from Project + keyman:removefile?id= + + +
@@ -189,11 +195,13 @@ keyman:opencontainingfolder?id=
- - Remove from Project - keyman:removefile?id= - - + + + Remove from Project + keyman:removefile?id= + + +
\ No newline at end of file diff --git a/developer/src/tike/xml/project/packages.xsl b/developer/src/tike/xml/project/packages.xsl index ff5a8e520a6..557ba1ce737 100644 --- a/developer/src/tike/xml/project/packages.xsl +++ b/developer/src/tike/xml/project/packages.xsl @@ -49,28 +49,32 @@ + +
New package... keyman:fileaddnew?type=package - - Add existing package... - keyman:fileaddexisting?type=package - + + + Add existing package... + keyman:fileaddexisting?type=package + + | Build all keyman:compileall - false + false Clean all keyman:cleanall - false + false auto @@ -95,7 +99,7 @@
- + () @@ -142,10 +146,12 @@ keyman:openbuildfolder?id= - - Remove from Project - keyman:removefile?id= - + + + Remove from Project + keyman:removefile?id= + +
diff --git a/developer/src/tike/xml/project/project.css b/developer/src/tike/xml/project/project.css index 30163b71310..f47cbcf338c 100644 --- a/developer/src/tike/xml/project/project.css +++ b/developer/src/tike/xml/project/project.css @@ -11,6 +11,20 @@ overflow-y: auto; } +.upgrade-warning { + padding: 8px; + border: solid 1px #c0c000; + background: #ffefb0; + margin: 0 8px 16px 10px; + max-width: 800px; + border-radius: 4px; +} + +.upgrade-warning input { + float: right; + margin-top: -3px; +} + .tabbackground { position: absolute; bottom: 0px; diff --git a/developer/src/tike/xml/project/welcome.xsl b/developer/src/tike/xml/project/welcome.xsl index cc387561653..253fbf00793 100644 --- a/developer/src/tike/xml/project/welcome.xsl +++ b/developer/src/tike/xml/project/welcome.xsl @@ -46,6 +46,8 @@
+ +
Open project folder @@ -78,7 +80,7 @@ Open keyman:openfile?id= - + View Source keyman:viewfilesource?id= diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 464387941af..7902ce7a682 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -26,7 +26,7 @@ /oem/firstvoices/windows/ @rc-swag @ermshiperete /resources/ @mcdurdin @jahorton -# Web is currently shared between Marc and Joshua: -/web/ @jahorton @mcdurdin +# Web is currently shared between Eberhard and Joshua: +/web/ @ermshiperete @jahorton /windows/ @rc-swag @ermshiperete diff --git a/docs/build/windows.md b/docs/build/windows.md index 81a96a4a209..da320f244e9 100644 --- a/docs/build/windows.md +++ b/docs/build/windows.md @@ -149,10 +149,8 @@ PowerShell. # Elevated PowerShell # for *much* faster download, hide progress bar (PowerShell/PowerShell#2138) $ProgressPreference = 'SilentlyContinue' -choco install git jq python ninja pandoc +choco install git jq python ninja pandoc meson refreshenv -# choco meson (0.55) is too old, 1.0 required: -python -m pip install meson ``` **Environment variables**: @@ -183,7 +181,7 @@ You can use Windows Settings to add these environment variables permanently: * KeymanWeb **Requirements**: -* emscripten 3.1.32 +* emscripten 3.1.40 * node.js 18+ * [openjdk 11](https://learn.microsoft.com/en-us/java/openjdk/download#openjdk-11)+ @@ -193,7 +191,7 @@ You can use Windows Settings to add these environment variables permanently: # for *much* faster download, hide progress bar (PowerShell/PowerShell#2138) $ProgressPreference = 'SilentlyContinue' -choco install emscripten +choco install emscripten --version 3.1.40 ``` Note: emscripten very unhelpfully overwrites JAVA_HOME, and adds its own diff --git a/docs/linux/ibus-keyman.md b/docs/linux/ibus-keyman.md index db09d67f5a8..ffc7b06c1cf 100644 --- a/docs/linux/ibus-keyman.md +++ b/docs/linux/ibus-keyman.md @@ -6,7 +6,7 @@ Source code for Keyman engine for IBus is in [linux/ibus-keyman](../../linux/ibu ## Requirements -meson (>= 0.53) +meson (>= 0.57) ## Building diff --git a/ios/Cartfile b/ios/Cartfile index b265ef77af2..57ec6de1e2b 100644 --- a/ios/Cartfile +++ b/ios/Cartfile @@ -1,4 +1,4 @@ -github "marmelroy/Zip" +github "weichsel/ZIPFoundation" ~> 0.9 github "keymanapp/dependency-XCGLogger" "master" github "devicekit/DeviceKit" ~> 5.0 github "ashleymills/Reachability.swift" diff --git a/ios/Cartfile.resolved b/ios/Cartfile.resolved index ff30a8871dd..5245b0dfa73 100644 --- a/ios/Cartfile.resolved +++ b/ios/Cartfile.resolved @@ -1,5 +1,5 @@ github "ashleymills/Reachability.swift" "v5.1.0" -github "devicekit/DeviceKit" "5.0.0" -github "getsentry/sentry-cocoa" "8.7.0" +github "devicekit/DeviceKit" "5.1.0" +github "getsentry/sentry-cocoa" "8.15.2" github "keymanapp/dependency-XCGLogger" "57a7b975dbb6fe4fe90cef3d1bc52b8adbd89113" -github "marmelroy/Zip" "2.1.2" +github "weichsel/ZIPFoundation" "0.9.17" diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index f0205767d54..374f08bdcc2 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 1645D5952036C6FF0076C51B /* KeymanPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D5942036C6FF0076C51B /* KeymanPackage.swift */; }; 1645D5972036C9F80076C51B /* KMPKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D5962036C9F80076C51B /* KMPKeyboard.swift */; }; 165EB3A12098993900040A69 /* KeyboardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB3A02098993900040A69 /* KeyboardError.swift */; }; + 296EF2C72AFA26C700E3E384 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */; }; 377D10DE26846B8900467431 /* SpacebarTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D10DD26846B8900467431 /* SpacebarTextViewController.swift */; }; 6CD5DFAA150F6DC8007A5DDE /* icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 6CD5DFA8150F6DC8007A5DDE /* icon.png */; }; 6CD5DFAB150F6DC8007A5DDE /* icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6CD5DFA9150F6DC8007A5DDE /* icon@2x.png */; }; @@ -121,7 +122,6 @@ CE5C8BE324B5B3BA00FAFB7F /* Queries+LexicalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5C8BE224B5B3BA00FAFB7F /* Queries+LexicalModel.swift */; }; CE5C8BE524B5BD1C00FAFB7F /* QueryModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5C8BE424B5BD1C00FAFB7F /* QueryModelTests.swift */; }; CE5EDDC526522EAA001733AC /* XCGLogger.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */; }; - CE5EDDC626522EAA001733AC /* Zip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC126522EA9001733AC /* Zip.xcframework */; }; CE5EDDC726522EAA001733AC /* Sentry.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC226522EAA001733AC /* Sentry.xcframework */; }; CE5EDDC826522EAA001733AC /* Reachability.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC326522EAA001733AC /* Reachability.xcframework */; }; CE5EDDC926522EAA001733AC /* ObjcExceptionBridging.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */; }; @@ -308,6 +308,7 @@ 2949146F2738DA6700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/ResourceInfoView.strings"; sourceTree = ""; }; 294914702738DD7400400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = ""; }; 294914712738DD9700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "ff-NG"; path = "ff-NG.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = ../../Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; }; 297810FE297FAEDF007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/ResourceInfoView.strings; sourceTree = ""; }; 297810FF297FAEF8007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = ""; }; 29781100297FAF06007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kn; path = kn.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -436,7 +437,6 @@ CE5C8BE424B5BD1C00FAFB7F /* QueryModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryModelTests.swift; sourceTree = ""; }; CE5EDDBB26522EA3001733AC /* DeviceKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = DeviceKit.xcframework; path = ../../Carthage/Build/DeviceKit.xcframework; sourceTree = ""; }; CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = XCGLogger.xcframework; path = ../../Carthage/Build/XCGLogger.xcframework; sourceTree = ""; }; - CE5EDDC126522EA9001733AC /* Zip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Zip.xcframework; path = ../../Carthage/Build/Zip.xcframework; sourceTree = ""; }; CE5EDDC226522EAA001733AC /* Sentry.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Sentry.xcframework; path = ../../Carthage/Build/Sentry.xcframework; sourceTree = ""; }; CE5EDDC326522EAA001733AC /* Reachability.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Reachability.xcframework; path = ../../Carthage/Build/Reachability.xcframework; sourceTree = ""; }; CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ObjcExceptionBridging.xcframework; path = ../../Carthage/Build/ObjcExceptionBridging.xcframework; sourceTree = ""; }; @@ -560,8 +560,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 296EF2C72AFA26C700E3E384 /* ZIPFoundation.xcframework in Frameworks */, CE5EDDC526522EAA001733AC /* XCGLogger.xcframework in Frameworks */, - CE5EDDC626522EAA001733AC /* Zip.xcframework in Frameworks */, CE5EDDD52652372A001733AC /* DeviceKit.xcframework in Frameworks */, CE5EDDC726522EAA001733AC /* Sentry.xcframework in Frameworks */, CE5EDDC826522EAA001733AC /* Reachability.xcframework in Frameworks */, @@ -949,11 +949,11 @@ F243888114BBD43000A3E055 /* Frameworks */ = { isa = PBXGroup; children = ( + 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */, CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */, CE5EDDC326522EAA001733AC /* Reachability.xcframework */, CE5EDDC226522EAA001733AC /* Sentry.xcframework */, CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */, - CE5EDDC126522EA9001733AC /* Zip.xcframework */, CE5EDDBB26522EA3001733AC /* DeviceKit.xcframework */, 9A079DE52231A69D00581263 /* Foundation.framework */, ); diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift index 28d89b7ee17..a13cef142c8 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift @@ -7,7 +7,7 @@ // import Foundation -import Zip +import ZIPFoundation // KMPErrors may be passed to UIAlertControllers, so they need localization. public enum KMPError : String, Error { @@ -443,13 +443,28 @@ public class KeymanPackage { @available(*, deprecated, message: "Use of the completion block is unnecessary; this method now returns synchronously.") static public func extract(fileUrl: URL, destination: URL, complete: @escaping (KeymanPackage?) -> Void) throws { - try unzipFile(fileUrl: fileUrl, destination: destination) { - do { - let package = try KeymanPackage.parse(destination) - complete(package) - } catch { - SentryManager.captureAndLog(error, sentryLevel: .info) - complete(nil) + let fileManager = FileManager() + do { + try fileManager.unzipItem(at: fileUrl, to: destination) + let package = try KeymanPackage.parse(destination) + complete(package) + } catch { + SentryManager.captureAndLog(error, sentryLevel: .info) + complete(nil) + } + } + + static public func clearDirectory(destination: URL) throws { + // First check to see if directory exists. If not, then do nothing. + var isDirectory: ObjCBool = false + if(FileManager.default.fileExists(atPath: destination.path, isDirectory: &isDirectory)){ + if (isDirectory.boolValue) { + // it exists and is actually a directory, so remove every file it contains + let fileArray = try FileManager.default.contentsOfDirectory(atPath: destination.path) + try fileArray.forEach { file in + let fileUrl = destination.appendingPathComponent(file) + try FileManager.default.removeItem(atPath: fileUrl.path) + } } } } @@ -460,7 +475,8 @@ public class KeymanPackage { } static public func unzipFile(fileUrl: URL, destination: URL, complete: @escaping () -> Void = {}) throws { - try Zip.unzipFile(fileUrl, destination: destination, overwrite: true, password: nil) + let fileManager = FileManager() + try fileManager.unzipItem(at: fileUrl, to: destination) complete() } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index 11740ce42b1..d3831413cbc 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -183,6 +183,9 @@ public class ResourceFileManager { var extractionFolder = cacheDirectory extractionFolder.appendPathComponent("temp/\(archiveUrl.lastPathComponent)") + // first clear extraction folder to avoid creating duplicates + try KeymanPackage.clearDirectory(destination: extractionFolder) + do { if let package = try KeymanPackage.extract(fileUrl: archiveUrl, destination: extractionFolder) { return package diff --git a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js index 6c31618fc48..3717b5e6485 100644 --- a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js +++ b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js @@ -24,6 +24,7 @@ if(_debug) { var oskHeight = 0; var oskWidth = 0; var bannerHeight = 0; +var bannerImgPath = ''; var sentryManager = new KeymanSentryManager({ hostPlatform: "ios" @@ -73,14 +74,13 @@ function verifyLoaded() { function showBanner(flag) { console.log("Setting banner display for dictionaryless keyboards to " + flag); - keyman.osk.bannerController.setOptions({'alwaysShow': flag}); + + var bc = keyman.osk.bannerController; + bc.inactiveBanner = flag ? new bc.ImageBanner(bannerImgPath) : null; } function setBannerImage(path) { - var kmw=window['keyman']; - if(kmw.osk) { - kmw.osk.bannerController.setOptions({"imagePath": path}); - } + bannerImgPath = path; } function setBannerHeight(h) { diff --git a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift index ee91cf96b98..baf61ee4872 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift @@ -29,14 +29,13 @@ class KeymanPackageTests: XCTestCase { } } - func testKeyboardPackageExtraction() throws { + func testKeyboardPackage_extractWithoutKmpExtension_succeeds() throws { let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp.zip") + let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp") try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip) let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor") - // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation. do { if let kmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) { // Run assertions on the package's kmp.info. @@ -58,14 +57,104 @@ class KeymanPackageTests: XCTestCase { } } + func testKeyboardPackage_clearNonexistentDirectory_doesNothing() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let destinationDirectory = cacheDirectory.appendingPathComponent("doesnotexist") + + do { + // clear directory + try KeymanPackage.clearDirectory(destination: destinationDirectory) + } catch { + XCTFail("error clearing the nonexistent directory \(error)") + } + } + + func testKeyboardPackage_clearEmptyDirectory_throwsNoError() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let destinationDirectory = cacheDirectory.appendingPathComponent("destination") + + do { + // create directory + try FileManager.default.createDirectory( + atPath: destinationDirectory.path, + withIntermediateDirectories: false, + attributes: nil + ) + + // clear directory + try KeymanPackage.clearDirectory(destination: destinationDirectory) + } catch { + XCTFail("error clearing the empty directory \(error)") + } + + let fileArray = try FileManager.default.contentsOfDirectory(atPath: destinationDirectory.path) + XCTAssert(fileArray.count == 0, "directory still contains \(fileArray.count) items") + } + + func testKeyboardPackage_clearNonEmptyDirectory_directoryIsEmpty() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let destinationDirectory = cacheDirectory.appendingPathComponent("destination") + + do { + // create directory + try FileManager.default.createDirectory( + atPath: destinationDirectory.path, + withIntermediateDirectories: false, + attributes: nil + ) + + // add some files + FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("fileone").path, contents: nil) + FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("filetwo").path, contents: nil) + FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("filethree").path, contents: nil) + + // clear directory + try KeymanPackage.clearDirectory(destination: destinationDirectory) + } catch { + XCTFail("error clearing the empty directory \(error)") + } + + let fileArray = try FileManager.default.contentsOfDirectory(atPath: destinationDirectory.path) + XCTAssert(fileArray.count == 0, "directory still contains \(fileArray.count) items") + } + + func testKeyboardPackage_extractTwice_noDuplicateFileError() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp") + try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip) + + let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor") + + do { + if let kmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) { + log.info("*** first unzip of \(kmp.id)") + do { + // clear directory before second extract + try KeymanPackage.clearDirectory(destination: destinationFolderURL) + + if let secondKmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) { + log.info("*** second unzip of \(secondKmp.id)") + } else { + XCTAssert(false, "*** second unzip failed") + } + } catch { + XCTFail("unzip 2 failure with error \(error)") + } + } else { + XCTAssert(false, "*** first unzip failed") + } + } catch { + XCTFail("unzip 1 failure with error \(error)") + } + } + func testLexicalModelPackageExtraction() throws { let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - let mtntZip = cacheDirectory.appendingPathComponent("nrc.en.mtnt.kmp.zip") + let mtntZip = cacheDirectory.appendingPathComponent("nrc.en.mtnt.kmp") try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.LexicalModels.mtntKMP, to: mtntZip) let destinationFolderURL = cacheDirectory.appendingPathComponent("nrc.en.mtnt.model") - // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation. do { if let kmp = try KeymanPackage.extract(fileUrl: mtntZip, destination: destinationFolderURL) { // Run assertions on the package's kmp.info. diff --git a/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift b/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift index 1f3bd4e27ca..b8950dc9c2b 100644 --- a/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift +++ b/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift @@ -8,7 +8,8 @@ import Foundation import XCTest -import Zip +import ZIPFoundation + @testable import KeymanEngine extension TestUtils { @@ -64,12 +65,17 @@ extension TestUtils { try FileManager.default.moveItem(at: pListPath.appendingPathComponent(testEngineFilename), to: pListPath.appendingPathComponent(appGroupFilename)) } - let attachmentFile = try Zip.quickZipFiles([bundleConstructionURL], fileName: "bundleArchive") - log.info("Archive source: \(bundleConstructionURL)") - let attachment = XCTAttachment(contentsOfFile: attachmentFile) - attachment.lifetime = .keepAlways - - return attachment - } + let archiveURL = bundleConstructionURL.appendingPathComponent("bundleArchive.zip") + do { + _ = try Archive(url: archiveURL, accessMode: .create) + log.info("archiveURL: \(archiveURL)") + let attachment = XCTAttachment(contentsOfFile: archiveURL) + attachment.lifetime = .keepAlways + return attachment + } catch let error { + print (error.localizedDescription) + return XCTAttachment(string: error.localizedDescription) + } + } } } diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj index 5f2fad4ced7..ddaa7a831cd 100644 --- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj +++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 162E2C9F2092D36800F40769 /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 162E2C9E2092D36800F40769 /* UIDevice+Extensions.swift */; }; 165EB39A2097165B00040A69 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB3992097165B00040A69 /* UIColor+Extensions.swift */; }; 165EB39C2097181D00040A69 /* KMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB39B2097181D00040A69 /* KMView.swift */; }; + 296EF2C42AFA267500E3E384 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */; }; + 296EF2C52AFA267500E3E384 /* ZIPFoundation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 981AFA8F19EF44DE006706BF /* 724-info@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9815725518E4F0930014DF0C /* 724-info@2x.png */; }; 981AFA9619EF44DE006706BF /* MainViewController_iPhone.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98ABADCE176935E500B62590 /* MainViewController_iPhone.xib */; }; 981AFA9819EF44DE006706BF /* 724-info.png in Resources */ = {isa = PBXBuildFile; fileRef = 9815725418E4F0930014DF0C /* 724-info.png */; }; @@ -155,8 +157,6 @@ CEBD34422654FEB400EB2EA8 /* Sentry.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CEBD34432654FEB400EB2EA8 /* XCGLogger.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */; }; CEBD34442654FEB400EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - CEBD34452654FEB400EB2EA8 /* Zip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */; }; - CEBD34462654FEB400EB2EA8 /* Zip.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CEF4E55623E95B7B0065B9C7 /* ImageBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */; }; CEF4E55723E95B7B0065B9C7 /* ImageBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */; }; CEF4E55923E967140065B9C7 /* ImageBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */; }; @@ -183,11 +183,11 @@ files = ( CEBD34402654FEB400EB2EA8 /* Reachability.xcframework in Embed Frameworks */, CEBD343E2654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework in Embed Frameworks */, + 296EF2C52AFA267500E3E384 /* ZIPFoundation.xcframework in Embed Frameworks */, CEBD343C2654FEB400EB2EA8 /* DeviceKit.xcframework in Embed Frameworks */, CEBD34422654FEB400EB2EA8 /* Sentry.xcframework in Embed Frameworks */, CEBD34442654FEB400EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */, CE80AD34257F2B4B008D2150 /* KeymanEngine.framework in Embed Frameworks */, - CEBD34462654FEB400EB2EA8 /* Zip.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -205,6 +205,7 @@ 16A9229D20325253003CC98E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Keyman/Info.plist; sourceTree = SOURCE_ROOT; }; 293EA3E027059C6900545EED /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/Localizable.strings; sourceTree = ""; }; 294914742738DF7700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = ""; }; + 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = ../../Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; }; 297810FD297FA818007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = ""; }; 298566B829802828004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 298566C42980C493004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; @@ -344,7 +345,6 @@ CEBD34372654FEB400EB2EA8 /* Reachability.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Reachability.xcframework; path = ../../Carthage/Build/Reachability.xcframework; sourceTree = ""; }; CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Sentry.xcframework; path = ../../Carthage/Build/Sentry.xcframework; sourceTree = ""; }; CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = XCGLogger.xcframework; path = ../../Carthage/Build/XCGLogger.xcframework; sourceTree = ""; }; - CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Zip.xcframework; path = ../../Carthage/Build/Zip.xcframework; sourceTree = ""; }; CEBD3458265511B700EB2EA8 /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/Localizable.strings; sourceTree = ""; }; CEEC468226F2F7BA009A5B7D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; CEEF81B92673019600EE6A07 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; @@ -359,11 +359,11 @@ files = ( CEBD343F2654FEB400EB2EA8 /* Reachability.xcframework in Frameworks */, CEBD343D2654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework in Frameworks */, + 296EF2C42AFA267500E3E384 /* ZIPFoundation.xcframework in Frameworks */, CEBD343B2654FEB400EB2EA8 /* DeviceKit.xcframework in Frameworks */, CEBD34412654FEB400EB2EA8 /* Sentry.xcframework in Frameworks */, CEBD34432654FEB400EB2EA8 /* XCGLogger.xcframework in Frameworks */, CE80AD33257F2B4A008D2150 /* KeymanEngine.framework in Frameworks */, - CEBD34452654FEB400EB2EA8 /* Zip.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -544,12 +544,12 @@ 98ABADB2176935E400B62590 /* Frameworks */ = { isa = PBXGroup; children = ( + 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */, CEBD34352654FEB400EB2EA8 /* DeviceKit.xcframework */, CEBD34362654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework */, CEBD34372654FEB400EB2EA8 /* Reachability.xcframework */, CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */, CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */, - CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */, CE80AD32257F2B4A008D2150 /* KeymanEngine.framework */, ); name = Frameworks; diff --git a/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme b/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme index abc85ae1420..8f86bea5338 100644 --- a/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme +++ b/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme @@ -40,7 +40,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES"> &2 + builder_echo "$1" +} + +output_ok() { + echo ":heavy_check_mark: $1" >&2 + builder_echo green "OK: $1" +} + +output_warning() { + echo ":warning: $1" >&2 + builder_echo warning "WARNING: $1" +} + +output_error() { + echo ":x: $1" >&2 + builder_echo error "ERROR: $1" +} + +check_api_not_changed() { + # Checks that the API did not change compared to what's documented in the .symbols file + tmpDir=$(mktemp -d) + # shellcheck disable=SC2064 + trap "rm -rf \"${tmpDir}\"" ERR + dpkg -x "${BIN_PKG}" "${tmpDir}" + cd debian + dpkg-gensymbols -v"${VERSION}" -p"${PKG_NAME}" -e"${tmpDir}"/usr/lib/x86_64-linux-gnu/"${LIB_NAME}".so* -O"${PKG_NAME}.symbols" -c4 + output_ok "${LIB_NAME} API didn't change" + cd "${REPO_ROOT}/linux" + rm -rf "${tmpDir}" + trap ERR +} + +# +# Compare the SHA of the base and head commits for changes to the .symbols file +# +is_symbols_file_changed() { + local CHANGED_REF CHANGED_BASE + CHANGED_REF=$(git rev-parse "${GIT_SHA}":"linux/debian/${PKG_NAME}.symbols") + CHANGED_BASE=$(git rev-parse "${GIT_BASE}":"linux/debian/${PKG_NAME}.symbols") + if [[ "${CHANGED_REF}" == "${CHANGED_BASE}" ]]; then + return 1 + fi + return 0 +} + +check_updated_version_number() { + # Checks that the package version number got updated in the .symbols file if it got changed + # shellcheck disable=SC2310 + if is_symbols_file_changed; then + # .symbols file changed, now check if the package version got updated as well + # Note: We don't check that ALL changes in that file have an updated package version - + # we hope this gets flagged in code review. + # Note: This version number check may not match the actual released version, if the branch + # is out of date when it is merged to the release branch (master/beta/stable-x.y). If this + # is considered important, then make sure the branch is up to date, and wait for test + # builds to complete, before merging. + if ! git log -p -1 -- "debian/${PKG_NAME}.symbols" | grep -q "${VERSION}"; then + output_error "${PKG_NAME}.symbols file got changed without changing the package version number of the symbol" + EXIT_CODE=1 + else + output_ok "${PKG_NAME}.symbols file got updated with package version number" + fi + else + output_ok "${PKG_NAME}.symbols file didn't change" + fi +} + +get_api_version_in_symbols_file() { + # Extract 1 from "libkeymancore.so.1 libkeymancore #MINVER#" + local firstline + firstline="$(head -1 "debian/${PKG_NAME}.symbols")" + firstline="${firstline#"${PKG_NAME}".so.}" + firstline="${firstline%% *}" + echo "${firstline}" +} + +is_api_version_updated() { + local NEW_VERSION OLD_VERSION + git checkout "${GIT_BASE}" -- "debian/${PKG_NAME}.symbols" + OLD_VERSION=$(get_api_version_in_symbols_file) + git checkout "${GIT_SHA}" -- "debian/${PKG_NAME}.symbols" + NEW_VERSION=$(get_api_version_in_symbols_file) + if (( NEW_VERSION > OLD_VERSION )); then + return 0 + fi + return 1 +} + +check_for_major_api_changes() { + # Checks that API version number gets updated if API changes + local WHAT_CHANGED CHANGES INSERTED DELETED MODIFIED + + # shellcheck disable=2310 + if ! is_symbols_file_changed; then + output_ok "No major API change" + return + fi + + WHAT_CHANGED=$(git diff "${GIT_BASE}".."${GIT_SHA}" -- "debian/${PKG_NAME}.symbols" | diffstat -m -t | tail -1) + + IFS=',' read -r -a CHANGES <<< "${WHAT_CHANGED}" + INSERTED="${CHANGES[0]}" + DELETED="${CHANGES[1]}" + MODIFIED="${CHANGES[2]}" + + if (( DELETED > 0 )) || (( MODIFIED > 0 )); then + builder_echo "Major API change: ${DELETED} lines deleted and ${MODIFIED} lines modified" + # shellcheck disable=2310 + if ! is_api_version_updated; then + output_error "Major API change without updating API version number in ${PKG_NAME}.symbols file" + EXIT_CODE=2 + else + output_ok "API version number got updated in ${PKG_NAME}.symbols file after major API change" + fi + elif (( INSERTED > 0 )); then + output_ok "Minor API change: ${INSERTED} lines added" + # We currently don't check version number for minor API changes + else + output_ok "No major API change" + fi +} + +check_for_api_version_consistency() { + # Checks that the (major) API version number in the .symbols file and + # in CORE_API_VERSION.md are the same + local symbols_version api_version + symbols_version=$(get_api_version_in_symbols_file) + api_version=$(cat ../core/CORE_API_VERSION.md) + # Extract major version number from "1.0.0" + api_version=${api_version%%.*} + + if (( symbols_version == api_version )); then + output_ok "API version in .symbols file and in CORE_API_VERSION.md is the same" + else + output_error "API version in .symbols file and in CORE_API_VERSION.md is different" + EXIT_CODE=3 + fi +} + verify_action() { - tar xf "${SRC_PKG}" PKG_NAME=libkeymancore LIB_NAME=libkeymancore - if [ ! -f debian/${PKG_NAME}.symbols ]; then - echo ":warning: Missing ${PKG_NAME}.symbols file" >&2 - else - tmpDir=$(mktemp -d) - dpkg -x "${BIN_PKG}" "$tmpDir" - cd debian - dpkg-gensymbols -v"${PKG_VERSION}" -p${PKG_NAME} -e"${tmpDir}"/usr/lib/x86_64-linux-gnu/${LIB_NAME}.so* -O${PKG_NAME}.symbols -c4 - echo ":heavy_check_mark: ${LIB_NAME} API didn't change" >&2 + if [[ ! -f debian/${PKG_NAME}.symbols ]]; then + output_warning "Missing ${PKG_NAME}.symbols file" + exit 0 fi + + EXIT_CODE=0 + check_api_not_changed + check_updated_version_number + check_for_major_api_changes + check_for_api_version_consistency + exit "${EXIT_CODE}" } builder_run_action dependencies dependencies_action diff --git a/oem/firstvoices/android/app/src/main/assets/banner.html b/oem/firstvoices/android/app/src/main/assets/banner.html new file mode 100644 index 00000000000..b0c30219cbe --- /dev/null +++ b/oem/firstvoices/android/app/src/main/assets/banner.html @@ -0,0 +1,10 @@ + +
+ + + + + +
+ +
diff --git a/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg b/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg new file mode 100644 index 00000000000..cf89f67db51 --- /dev/null +++ b/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java b/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java new file mode 100644 index 00000000000..a0df15b2f1e --- /dev/null +++ b/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java @@ -0,0 +1,29 @@ +package com.firstvoices.android; + +import android.content.Context; + +import com.keyman.engine.KMManager; +import com.keyman.engine.util.FileUtils; + +import java.io.File; + +public class BannerController { + + // Paths relative to assets folder for banner themes + public static final String FV_BANNER_DIR = "banner"; + public static final String FV_BANNER_THEME = "banner.html"; + + public static void setHTMLBanner(Context context, KMManager.KeyboardType keyboardType) { + if (keyboardType == KMManager.KeyboardType.KEYBOARD_TYPE_UNDEFINED) { + return; + } + + KMManager.copyHTMLBannerAssets(context, FV_BANNER_DIR); + + // Always use FirstVoices banner theme + String contents = FileUtils.readContents(context, FV_BANNER_THEME); + KMManager.setHTMLBanner(keyboardType, contents); + KMManager.setBanner(keyboardType, KMManager.BannerType.HTML); + KMManager.showBanner(true); + } +} diff --git a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java index 95d0c51b372..0fb770c9b51 100644 --- a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java +++ b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java @@ -20,6 +20,7 @@ import android.view.inputmethod.InputConnection; import android.widget.FrameLayout; +import com.firstvoices.android.BannerController; import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; import com.keyman.engine.KMHardwareKeyboardInterpreter; @@ -33,7 +34,6 @@ import io.sentry.Sentry; public class SystemKeyboard extends InputMethodService implements OnKeyboardEventListener { - private View inputView = null; private static ExtractedText exText = null; private KMHardwareKeyboardInterpreter interpreter = null; @@ -64,6 +64,9 @@ public void onCreate() { interpreter = new KMHardwareKeyboardInterpreter(getApplicationContext(), KeyboardType.KEYBOARD_TYPE_SYSTEM); KMManager.setInputMethodService(this); // for HW interface + + // Set the system keyboard HTML banner + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); } @Override @@ -79,7 +82,10 @@ public void onDestroy() { * is called after creation and any configuration change. */ @Override public void onInitializeInterface() { - super.onInitializeInterface(); + super.onInitializeInterface(); + + // KeymanWeb reloaded, so we have to pass the banner again + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); } /** Called by the framework when your view for creating input needs to diff --git a/oem/firstvoices/ios/Cartfile b/oem/firstvoices/ios/Cartfile index 10efbc3d5a0..57ec6de1e2b 100644 --- a/oem/firstvoices/ios/Cartfile +++ b/oem/firstvoices/ios/Cartfile @@ -1,3 +1,4 @@ +github "weichsel/ZIPFoundation" ~> 0.9 github "keymanapp/dependency-XCGLogger" "master" github "devicekit/DeviceKit" ~> 5.0 github "ashleymills/Reachability.swift" diff --git a/oem/firstvoices/ios/Cartfile.resolved b/oem/firstvoices/ios/Cartfile.resolved index 5ac3a553189..5245b0dfa73 100644 --- a/oem/firstvoices/ios/Cartfile.resolved +++ b/oem/firstvoices/ios/Cartfile.resolved @@ -1,4 +1,5 @@ github "ashleymills/Reachability.swift" "v5.1.0" -github "devicekit/DeviceKit" "5.0.0" -github "getsentry/sentry-cocoa" "8.7.0" +github "devicekit/DeviceKit" "5.1.0" +github "getsentry/sentry-cocoa" "8.15.2" github "keymanapp/dependency-XCGLogger" "57a7b975dbb6fe4fe90cef3d1bc52b8adbd89113" +github "weichsel/ZIPFoundation" "0.9.17" diff --git a/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj b/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj index 83e34c42a61..dcb81b684f2 100644 --- a/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj +++ b/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 290FB4632AFB358F00249D58 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; }; + 290FB4642AFB358F00249D58 /* ZIPFoundation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 290FB4652AFB359A00249D58 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; }; 294D40E9279FE62600DB37F6 /* KeyboardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294D40E8279FE62600DB37F6 /* KeyboardRepository.swift */; }; 294D40EC27A110BC00DB37F6 /* KeyboardSettingsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294D40EB27A110BC00DB37F6 /* KeyboardSettingsRepository.swift */; }; 29694DC227A27EC300EA6C18 /* LexicalModelRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29694DC127A27EC300EA6C18 /* LexicalModelRepository.swift */; }; @@ -74,6 +77,7 @@ files = ( CEBD34202654D76900EB2EA8 /* Reachability.xcframework in Embed Frameworks */, CEBD341E2654D76800EB2EA8 /* ObjcExceptionBridging.xcframework in Embed Frameworks */, + 290FB4642AFB358F00249D58 /* ZIPFoundation.xcframework in Embed Frameworks */, CEBD341B2654D76600EB2EA8 /* DeviceKit.xcframework in Embed Frameworks */, CEBD34232654D76B00EB2EA8 /* Sentry.xcframework in Embed Frameworks */, CEBD34262654D76C00EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */, @@ -96,6 +100,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; }; 294D40E8279FE62600DB37F6 /* KeyboardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardRepository.swift; sourceTree = ""; }; 294D40EB27A110BC00DB37F6 /* KeyboardSettingsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardSettingsRepository.swift; sourceTree = ""; }; 29694DC127A27EC300EA6C18 /* LexicalModelRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelRepository.swift; sourceTree = ""; }; @@ -153,6 +158,7 @@ files = ( CEBD341D2654D76800EB2EA8 /* ObjcExceptionBridging.xcframework in Frameworks */, CEBD341F2654D76900EB2EA8 /* Reachability.xcframework in Frameworks */, + 290FB4632AFB358F00249D58 /* ZIPFoundation.xcframework in Frameworks */, CEBD34222654D76B00EB2EA8 /* Sentry.xcframework in Frameworks */, CEBD341A2654D76600EB2EA8 /* DeviceKit.xcframework in Frameworks */, CEBD34022654D41200EB2EA8 /* KeymanEngine.xcframework in Frameworks */, @@ -166,6 +172,7 @@ files = ( CEDB327F265C9C58000A2009 /* DeviceKit.xcframework in Frameworks */, CEDB3280265C9C58000A2009 /* ObjcExceptionBridging.xcframework in Frameworks */, + 290FB4652AFB359A00249D58 /* ZIPFoundation.xcframework in Frameworks */, CEDB3281265C9C58000A2009 /* Reachability.xcframework in Frameworks */, CEDB3282265C9C58000A2009 /* Sentry.xcframework in Frameworks */, CEDB3283265C9C58000A2009 /* XCGLogger.xcframework in Frameworks */, @@ -284,6 +291,7 @@ 98C9A9FB1BFC19ED009E9A4F /* Frameworks */ = { isa = PBXGroup; children = ( + 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */, CEBD34082654D5AA00EB2EA8 /* ObjcExceptionBridging.xcframework */, CEBD340C2654D5AB00EB2EA8 /* Reachability.xcframework */, CEBD34092654D5AA00EB2EA8 /* Sentry.xcframework */, diff --git a/resources/build/trigger-builds.inc.sh b/resources/build/trigger-builds.inc.sh index dd950f1c512..d0ca586398a 100644 --- a/resources/build/trigger-builds.inc.sh +++ b/resources/build/trigger-builds.inc.sh @@ -69,23 +69,28 @@ function triggerGitHubActionsBuild() { local IS_TEST_BUILD="$1" local GITHUB_ACTION="$2" local GIT_BRANCH="${3:-master}" - local GIT_BUILD_SHA GIT_HEAD_SHA + local GIT_BASE_BRANCH="${GIT_BRANCH}" + local GIT_USER="keyman-server" + local GIT_BUILD_SHA GIT_BASE_REF JSON local GITHUB_SERVER=https://api.github.com/repos/keymanapp/keyman if [ "${action:-""}" == "commit" ]; then # This will only be true if we created and pushed a tag GIT_BUILD_SHA="$(git rev-parse "refs/tags/release@$VERSION_WITH_TAG")" - GIT_HEAD_SHA="${GIT_BUILD_SHA}" + GIT_BASE_REF="$(git rev-parse "${GIT_BUILD_SHA}^")" GIT_EVENT_TYPE="${GITHUB_ACTION}: release@${VERSION_WITH_TAG}" elif [[ $GIT_BRANCH != stable-* ]] && [[ $GIT_BRANCH =~ [0-9]+ ]]; then - GIT_BUILD_SHA="$(git rev-parse "refs/pull/${GIT_BRANCH}/head")" - GIT_HEAD_SHA="$(git rev-parse "refs/pull/${GIT_BRANCH}/head")" + JSON=$(curl -s "${GITHUB_SERVER}/pulls/${GIT_BRANCH}") + GIT_BUILD_SHA="$(echo "$JSON" | $JQ -r '.head.sha')" GIT_EVENT_TYPE="${GITHUB_ACTION}: PR #${GIT_BRANCH}" + GIT_USER="$(echo "$JSON" | $JQ -r '.user.login')" + GIT_BASE_BRANCH="$(echo "$JSON" | $JQ -r '.base.ref')" + GIT_BASE_REF="$(echo "$JSON" | $JQ -r '.base.sha')" GIT_BRANCH="PR-${GIT_BRANCH}" else GIT_BUILD_SHA="$(git rev-parse "refs/heads/${GIT_BRANCH}")" - GIT_HEAD_SHA="${GIT_BUILD_SHA}" + GIT_BASE_REF="$(git rev-parse "${GIT_BUILD_SHA}^")" GIT_EVENT_TYPE="${GITHUB_ACTION}: ${GIT_BRANCH}" fi @@ -93,8 +98,10 @@ function triggerGitHubActionsBuild() { \"event_type\": \"$GIT_EVENT_TYPE\", \ \"client_payload\": { \ \"buildSha\": \"$GIT_BUILD_SHA\", \ - \"headSha\": \"$GIT_HEAD_SHA\", \ \"branch\": \"$GIT_BRANCH\", \ + \"baseBranch\": \"$GIT_BASE_BRANCH\", \ + \"baseRef\": \"$GIT_BASE_REF\", \ + \"user\": \"$GIT_USER\", \ \"isTestBuild\": \"$IS_TEST_BUILD\" \ }}" diff --git a/resources/standards-data/ldml-keyboards/readme.md b/resources/standards-data/ldml-keyboards/readme.md index 66f54789c08..373c7c07451 100644 --- a/resources/standards-data/ldml-keyboards/readme.md +++ b/resources/standards-data/ldml-keyboards/readme.md @@ -9,8 +9,8 @@ That will very roughly correspond to - - + diff --git a/resources/standards-data/ldml-keyboards/techpreview/test/bn-test.xml b/resources/standards-data/ldml-keyboards/techpreview/test/bn-test.xml index ba0d7d84302..ad95223ca6e 100644 --- a/resources/standards-data/ldml-keyboards/techpreview/test/bn-test.xml +++ b/resources/standards-data/ldml-keyboards/techpreview/test/bn-test.xml @@ -2,7 +2,18 @@ - + + + + + + + + + + + + diff --git a/web/src/app/browser/src/languageMenu.ts b/web/src/app/browser/src/languageMenu.ts index 5a0953ec472..65294704b47 100644 --- a/web/src/app/browser/src/languageMenu.ts +++ b/web/src/app/browser/src/languageMenu.ts @@ -216,7 +216,7 @@ export class LanguageMenu { if(device.OS == 'ios') { if(device.formFactor == 'phone') { barWidth=(landscapeView() ? 36 : 0); - maxHeight=(window.innerHeight-barWidth-16)*util.getViewportScale(); + maxHeight=(window.innerHeight-barWidth-16)*util.getViewportScale(device.formFactor); } else if(device.formFactor == 'tablet') { barWidth=(landscapeView() ? 16 : 0); maxHeight=(maxHeight-barWidth); @@ -316,7 +316,7 @@ export class LanguageMenu { langs.sort(); // Get current scale factor (reciprocal of viewport scale) - var scale=Math.round(100/util.getViewportScale())/100; + var scale=Math.round(100/util.getViewportScale(device.formFactor))/100; var dx,lgBar,i,kb,activeLanguageIndex=-1; for(k=0; k viewportWidth) { - return 1; - } - - // Get the orientation corrected screen width - var screenWidth = screen.width; - if(this.landscapeView()) { - // Take larger of the two dimensions - if(screen.width < screen.height) { - screenWidth = screen.height; - } - } else { - // Take smaller of the two dimensions - if(screen.width > screen.height) { - screenWidth = screen.height; - } - } - // Calculate viewport scale - return Math.round(100*screenWidth / viewportWidth)/100; - } catch(ex) { - return 1; - } -} diff --git a/web/src/app/browser/src/utils/index.ts b/web/src/app/browser/src/utils/index.ts index d4decbd0026..ce193e88beb 100644 --- a/web/src/app/browser/src/utils/index.ts +++ b/web/src/app/browser/src/utils/index.ts @@ -1,5 +1,5 @@ export { AlertHost } from './alertHost.js'; export { _CreateElement } from './createElement.js'; export { getStyleValue } from './getStyleValue.js'; -export { getViewportScale } from './getViewportScale.js'; +export { getViewportScale } from 'keyman/engine/osk'; export { whenDocumentReady } from './documentReady.js'; \ No newline at end of file diff --git a/web/src/engine/main/src/keymanEngine.ts b/web/src/engine/main/src/keymanEngine.ts index de3876f1a86..5c94b30f488 100644 --- a/web/src/engine/main/src/keymanEngine.ts +++ b/web/src/engine/main/src/keymanEngine.ts @@ -202,6 +202,8 @@ export default class KeymanEngine< this.contextManager.configure({ resetContext: (target) => { + // Could reset the target's deadkeys here, but it's really more of a 'core' task. + // So we delegate that to keyboard-processor. this.core.resetContext(target); }, predictionContext: new PredictionContext(this.core.languageProcessor, this.core.keyboardProcessor), @@ -323,7 +325,7 @@ export default class KeymanEngine< id: this.core?.activeModel?.id || '' }, osk: { - banner: this.osk?.bannerController?.activeType ?? '', + banner: this.osk?.banner?.banner.type ?? '', layer: this.osk?.vkbd?.layerId || '' } }; diff --git a/web/src/engine/osk/build.sh b/web/src/engine/osk/build.sh index 542b1f21bc7..9aa0b128a8e 100755 --- a/web/src/engine/osk/build.sh +++ b/web/src/engine/osk/build.sh @@ -29,14 +29,19 @@ builder_describe "Builds the Keyman Engine for Web's On-Screen Keyboard package # "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \ builder_describe_outputs \ - configure /node_modules \ + configure /web/src/resources/osk/keymanweb-osk.ttf \ build /web/build/$SUBPROJECT_NAME/lib/index.mjs builder_parse "$@" #### Build action definitions #### -builder_run_action configure verify_npm_setup +if builder_start_action configure; then + verify_npm_setup + cp "$KEYMAN_ROOT/common/resources/fonts/keymanweb-osk.ttf" "$KEYMAN_ROOT/web/src/resources/osk/" + builder_finish_action success configure +fi + builder_run_action clean rm -rf "$KEYMAN_ROOT/web/build/$SUBPROJECT_NAME" builder_run_action build compile $SUBPROJECT_NAME builder_run_action test test-headless osk \ No newline at end of file diff --git a/web/src/engine/osk/src/banner/banner.ts b/web/src/engine/osk/src/banner/banner.ts index 999f7d63486..3394efc4382 100644 --- a/web/src/engine/osk/src/banner/banner.ts +++ b/web/src/engine/osk/src/banner/banner.ts @@ -1,13 +1,4 @@ -import EventEmitter from 'eventemitter3'; - -import { DeviceSpec } from '@keymanapp/web-utils'; import { Keyboard, KeyboardProperties } from '@keymanapp/keyboard-processor'; -import { type PredictionContext } from '@keymanapp/input-processor'; -import InputEventEngine, { InputEventEngineConfig } from '../input/event-interpreter/inputEventEngine.js'; -import MouseEventEngine from '../input/event-interpreter/mouseEventEngine.js'; -import TouchEventEngine from '../input/event-interpreter/touchEventEngine.js'; -import UITouchHandlerBase from '../input/event-interpreter/uiTouchHandlerBase.js'; - import { createUnselectableElement } from 'keyman/engine/dom-utils'; // Base class for a banner above the keyboard in the OSK @@ -98,473 +89,6 @@ export abstract class Banner { * @param keyboardProperties */ public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) { } -} - -/** - * Function BlankBanner - * Description A banner of height 0 that should not be shown - */ -export class BlankBanner extends Banner { - - constructor() { - super(0); - } -} - -/** - * Function ImageBanner - * @param {string} imagePath Path of image to display in the banner - * @param {number} height If provided, the height of the banner in pixels - * Description Display an image in the banner - */ -export class ImageBanner extends Banner { - private img: HTMLElement; - - constructor(imagePath: string, height?: number) { - if (imagePath.length > 0) { - super(); - if (height) { - this.height = height; - } - } else { - super(0); - } - - if(imagePath.indexOf('base64') >=0) { - console.log("Loading img from base64 data"); - } else { - console.log("Loading img with src '" + imagePath + "'"); - } - this.img = document.createElement('img'); - this.img.setAttribute('src', imagePath); - let ds = this.img.style; - ds.width = '100%'; - ds.height = '100%'; - this.getDiv().appendChild(this.img); - console.log("Image loaded."); - } - - /** - * Function setImagePath - * Scope Public - * @param {string} imagePath Path of image to display in the banner - * Description Update the image in the banner - */ - public setImagePath(imagePath: string) { - if (this.img) { - this.img.setAttribute('src', imagePath); - } - } -} - -export class BannerSuggestion { - div: HTMLDivElement; - private display: HTMLSpanElement; - private fontFamily?: string; - private rtl: boolean = false; - - private _suggestion: Suggestion; - - private index: number; - - static readonly BASE_ID = 'kmw-suggestion-'; - - constructor(index: number, isRTL: boolean) { - this.index = index; - this.rtl = isRTL; - - this.constructRoot(); - - // Provides an empty, base SPAN for text display. We'll swap these out regularly; - // `Suggestion`s will have varying length and may need different styling. - let display = this.display = createUnselectableElement('span'); - this.div.appendChild(display); - } - - private constructRoot() { - // Add OSK suggestion labels - let div = this.div = createUnselectableElement('div'), ds=div.style; - div.className = "kmw-suggest-option"; - div.id = BannerSuggestion.BASE_ID + this.index; - - // Ensures that a reasonable width % is set. - let usableWidth = 100 - SuggestionBanner.MARGIN * (SuggestionBanner.SUGGESTION_LIMIT - 1); - let widthpc = usableWidth / SuggestionBanner.SUGGESTION_LIMIT; - - ds.width = widthpc + '%'; - - this.div['suggestion'] = this; - } - - public matchKeyboardProperties(keyboardProperties: KeyboardProperties) { - const div = this.div; - - if(keyboardProperties) { - if (keyboardProperties['KLC']) { - div.lang = keyboardProperties['KLC']; - } - - // Establish base font settings - let font = keyboardProperties['KFont']; - if(font && font.family && font.family != '') { - div.style.fontFamily = this.fontFamily = font.family; - } - } - } - get suggestion(): Suggestion { - return this._suggestion; - } - - /** - * Function update - * @param {string} id Element ID for the suggestion span - * @param {Suggestion} suggestion Suggestion from the lexical model - * Description Update the ID and text of the BannerSuggestionSpec - */ - public update(suggestion: Suggestion) { - this._suggestion = suggestion; - this.updateText(); - } - - private updateText() { - let display = this.generateSuggestionText(this.rtl); - this.div.replaceChild(display, this.display); - this.display = display; - } - - public isEmpty(): boolean { - return !this._suggestion; - } - - /** - * Function generateSuggestionText - * @return {HTMLSpanElement} Span element of the suggestion - * Description Produces a HTMLSpanElement with the key's actual text. - */ - // - public generateSuggestionText(rtl: boolean): HTMLSpanElement { - let suggestion = this._suggestion; - var suggestionText: string; - - var s=createUnselectableElement('span'); - s.className = 'kmw-suggestion-text'; - - if(suggestion == null) { - return s; - } - - if(suggestion.displayAs == null || suggestion.displayAs == '') { - suggestionText = '\xa0'; // default: nbsp. - } else { - // Default the LTR ordering to match that of the active keyboard. - let orderCode = rtl ? 0x202e /* RTL */ : 0x202d /* LTR */; - suggestionText = String.fromCharCode(orderCode) + suggestion.displayAs; - } - - // TODO: Dynamic suggestion text resizing. (Refer to OSKKey.getTextWidth in visualKeyboard.ts.) - - // Finalize the suggestion text - s.innerHTML = suggestionText; - return s; - } -} - -/** - * Function SuggestionBanner - * Scope Public - * @param {number} height - If provided, the height of the banner in pixels - * Description Display lexical model suggestions in the banner - */ -export class SuggestionBanner extends Banner { - public static readonly SUGGESTION_LIMIT: number = 3; - public static readonly MARGIN = 1; - - public readonly events: EventEmitter; - - private currentSuggestions: Suggestion[] = []; - - private options : BannerSuggestion[] = []; - private hostDevice: DeviceSpec; - - private manager: SuggestionInputManager; - - private _predictionContext: PredictionContext; - - static readonly TOUCHED_CLASS: string = 'kmw-suggest-touched'; - static readonly BANNER_CLASS: string = 'kmw-suggest-banner'; - - constructor(hostDevice: DeviceSpec, height?: number) { - super(height || Banner.DEFAULT_HEIGHT); - this.hostDevice = hostDevice; - - this.getDiv().className = this.getDiv().className + ' ' + SuggestionBanner.BANNER_CLASS; - - this.buildInternals(false); - - this.manager = new SuggestionInputManager(this.getDiv()); - this.events = this.manager.events; - - this.setupInputHandling(); - } - - buildInternals(rtl: boolean) { - if(this.options.length > 0) { - this.options.splice(0, this.options.length); // Clear the array. - } - for (var i=0; i { - const elem = suggestion.div; - let classes = elem.className; - let cs = ' ' + SuggestionBanner.TOUCHED_CLASS; - - if(on && classes.indexOf(cs) < 0) { - elem.className=classes+cs; - } else { - elem.className=classes.replace(cs,''); - } - }); - - this.manager.events.on('apply', (option) => { - if(this.predictionContext) { - this.predictionContext.accept(option.suggestion); - } - }); - } - - public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) { - const rtl = keyboard.isRTL; - - // Removes all previous children. (.replaceChildren requires Chrome for Android 86.) - // Instantly replaces all children with an empty text node, bypassing the need to actually - // parse incoming HTML. - // - // Just in case, alternative approaches: https://stackoverflow.com/a/3955238 - this.getDiv().textContent = ''; - - // Builds new children to match needed RTL properties. - this.buildInternals(rtl); - - this.options.forEach((option) => option.matchKeyboardProperties(keyboardProperties)); - this.onSuggestionUpdate(this.currentSuggestions); // restore suggestions - } - - private get mouseEventConfig() { - const config: InputEventEngineConfig = { - targetRoot: this.getDiv(), - // document.body is the event root b/c we need to track the mouse if it leaves - // the VisualKeyboard's hierarchy. - eventRoot: document.body, - inputStartHandler: this.manager.touchStart.bind(this.manager), - inputMoveHandler: this.manager.touchMove.bind(this.manager), - inputEndHandler: this.manager.touchEnd.bind(this.manager), - coordConstrainedWithinInteractiveBounds: function() { return true; } - }; - - return new MouseEventEngine(config); - } - - private get touchEventConfig() { - const config: InputEventEngineConfig = { - targetRoot: this.getDiv(), - // document.body is the event root b/c we need to track the mouse if it leaves - // the VisualKeyboard's hierarchy. - eventRoot: this.getDiv(), - inputStartHandler: this.manager.touchStart.bind(this.manager), - inputMoveHandler: this.manager.touchMove.bind(this.manager), - inputEndHandler: this.manager.touchEnd.bind(this.manager), - coordConstrainedWithinInteractiveBounds: function() { return true; } - }; - - return new TouchEventEngine(config); - } - - public get predictionContext(): PredictionContext { - return this._predictionContext; - } - - public set predictionContext(context: PredictionContext) { - if(this._predictionContext) { - // disconnect the old one! - this._predictionContext.off('update', this.onSuggestionUpdate); - } - - // connect the new one! - this._predictionContext = context; - if(context) { - context.on('update', this.onSuggestionUpdate); - this.onSuggestionUpdate(context.currentSuggestions); - } - } - - public onSuggestionUpdate = (suggestions: Suggestion[]): void => { - this.currentSuggestions = suggestions; - - this.options.forEach((option: BannerSuggestion, i: number) => { - if(i < suggestions.length) { - option.update(suggestions[i]); - } else { - option.update(null); - } - }); - } -} - -interface SuggestionInputEventMap { - highlight: (bannerSuggestion: BannerSuggestion, state: boolean) => void, - apply: (bannerSuggestion: BannerSuggestion) => void; - hold: (bannerSuggestion: BannerSuggestion) => void; -} - -class SuggestionInputManager extends UITouchHandlerBase { - public readonly events = new EventEmitter(); - - private eventDisablePromise: Promise; - - platformHold: (suggestion: BannerSuggestion, isCustom: boolean) => void; - - //#region Touch handling implementation - findTargetFrom(e: HTMLElement): HTMLDivElement { - try { - if(e) { - if(e.classList.contains('kmw-suggest-option')) { - return e as HTMLDivElement; - } - if(e.parentElement && e.parentElement.classList.contains('kmw-suggest-option')) { - return e.parentElement as HTMLDivElement; - } - // if(e.firstChild && util.hasClass( e.firstChild,'kmw-suggest-option')) { - // return e.firstChild as HTMLDivElement; - // } - } - } catch(ex) {} - return null; - } - - protected highlight(t: HTMLDivElement, on: boolean): void { - let suggestion = t['suggestion'] as BannerSuggestion; - - // Never highlight an empty suggestion button. - if(suggestion.isEmpty()) { - on = false; - } - - this.events.emit('highlight', suggestion, on); - } - - protected select(t: HTMLDivElement): void { - this.events.emit('apply', t['suggestion'] as BannerSuggestion); - } - - //#region Long-press support - protected hold(t: HTMLDivElement): void { - // let suggestionObj = t['suggestion'] as BannerSuggestion; - // - // // Is this the suggestion? It's never in this.currentSuggestions, so check against that. - // let isCustom = this.currentSuggestions.indexOf(suggestionObj.suggestion) == -1; - - this.events.emit('hold', t['suggestion'] as BannerSuggestion); - } - protected clearHolds(): void { - // Temp, pending implementation of suggestion longpress submenus - // - nothing to clear without them - - - // only really used in native-KMW - } - - protected hasModalPopup(): boolean { - return this.eventsBlocked; - } - - protected dealiasSubTarget(target: HTMLDivElement): HTMLDivElement { - return target; - } - - protected hasSubmenu(t: HTMLDivElement): boolean { - // Temp, pending implementation of suggestion longpress submenus - - // Only really used by native-KMW - see kmwnative's highlightSubKeys func. - return false; - } - - protected isSubmenuActive(): boolean { - // Temp, pending implementation of suggestion longpress submenus - - // Utilized only by native-KMW - it parallels hasModalPopup() in purpose. - return false; - } - - protected displaySubmenuFor(target: HTMLDivElement) { - // Utilized only by native-KMW to show submenus. - throw new Error("Method not implemented."); - } - //#endregion - //#endregion - - public get eventsBlocked(): boolean { - return !!this.eventDisablePromise; - } - - /** - * Intended for use by the mobile apps, which sometimes 'takes over' touch handling. - * For such cases, input should be blocked within KMW when the apps are managing an - * ongoing touch-hold for any other interaction. - * - * Formerly: - ``` - let keyman = com.keyman.singleton; - return keyman['osk'].vkbd.subkeyGesture && keyman.isEmbedded; - ``` - */ - public temporarilyBlockEvents(promise: Promise) { // TODO: ensure connection for embedded mode! - this.eventDisablePromise = promise; // Will require routing; this class is not exported! - promise.finally(() => { - this.eventDisablePromise = null; - }) - } - - constructor(div: HTMLElement) { - // TODO: Determine appropriate CSS styling names, etc. - super(div, Banner.BANNER_CLASS, SuggestionBanner.TOUCHED_CLASS); - } -} + abstract get type(); +} \ No newline at end of file diff --git a/web/src/engine/osk/src/banner/bannerController.ts b/web/src/engine/osk/src/banner/bannerController.ts new file mode 100644 index 00000000000..4f77524a1b4 --- /dev/null +++ b/web/src/engine/osk/src/banner/bannerController.ts @@ -0,0 +1,95 @@ +import { DeviceSpec } from '@keymanapp/web-utils'; +import type { PredictionContext, StateChangeEnum } from '@keymanapp/input-processor'; +import { ImageBanner } from './imageBanner.js'; +import { SuggestionBanner } from './suggestionBanner.js'; +import { BannerView } from './bannerView.js'; +import { Banner } from './banner.js'; +import { BlankBanner } from './blankBanner.js'; +import { HTMLBanner } from './htmlBanner.js'; + +export class BannerController { + private container: BannerView; + + private predictionContext?: PredictionContext; + + private readonly hostDevice: DeviceSpec; + + private _inactiveBanner: Banner; + + /** + * Builds a banner for use when predictions are not active, supporting a single image. + */ + public readonly ImageBanner = ImageBanner; + + /** + * Builds a banner for use when predictions are not active, supporting a more generalized + * content pattern than ImageBanner via `innerHTML` specifications. + */ + public readonly HTMLBanner = HTMLBanner; + + constructor(bannerView: BannerView, hostDevice: DeviceSpec, predictionContext?: PredictionContext) { + // Step 1 - establish the container element. Must come before this.setOptions. + this.hostDevice = hostDevice; + this.container = bannerView; + this.predictionContext = predictionContext; + + this.inactiveBanner = new BlankBanner(); + } + + /** + * Specifies the `Banner` instance to use when predictive-text is _not_ available to the user. + * + * Defaults to a hidden, "blank" `Banner` if not otherwise specified. Changes to its value + * when predictive-text is not active will result in banner hot-swapping. + * + * The assigned instance will persist until directly changed through a new assignment, + * regardless of any keyboard swaps and/or activations of the suggestion banner that may + * occur in the meantime. + */ + public get inactiveBanner() { + return this._inactiveBanner; + } + + public set inactiveBanner(banner: Banner) { + this._inactiveBanner = banner ?? new BlankBanner(); + + if(!(this.container.banner instanceof SuggestionBanner)) { + this.container.banner = this._inactiveBanner; + } + } + + /** + * Sets the active `Banner` to match the specified state for predictive text. + * + * @param on Whether prediction is active (`true`) or disabled (`false`). + */ + public activateBanner(on: boolean) { + let banner: Banner; + + const oldBanner = this.container.banner; + if(oldBanner instanceof SuggestionBanner) { + this.predictionContext.off('update', oldBanner.onSuggestionUpdate); + } + + if(!on) { + this.container.banner = this.inactiveBanner; + } else { + let suggestBanner = banner = new SuggestionBanner(this.hostDevice, this.container.activeBannerHeight); + suggestBanner.predictionContext = this.predictionContext; + suggestBanner.events.on('apply', (selection) => this.predictionContext.accept(selection.suggestion)); + + this.predictionContext.on('update', suggestBanner.onSuggestionUpdate); + this.container.banner = suggestBanner; + } + } + + /** + * Handles `LanguageProcessor`'s `'statechange'` events, + * allowing logic to automatically hot-swap `Banner`s as needed. + * @param state + */ + selectBanner(state: StateChangeEnum) { + // Only display a SuggestionBanner when LanguageProcessor states it is active. + this.activateBanner(state == 'active' || state == 'configured'); + } +} \ No newline at end of file diff --git a/web/src/engine/osk/src/banner/bannerView.ts b/web/src/engine/osk/src/banner/bannerView.ts index 556ec61762d..b291f7454e0 100644 --- a/web/src/engine/osk/src/banner/bannerView.ts +++ b/web/src/engine/osk/src/banner/bannerView.ts @@ -1,12 +1,11 @@ import EventEmitter from 'eventemitter3'; -import { Banner, BlankBanner, ImageBanner, SuggestionBanner } from './banner.js'; +import { createUnselectableElement } from 'keyman/engine/dom-utils'; + +import { Banner } from './banner.js'; import OSKViewComponent from '../components/oskViewComponent.interface.js'; import { ParsedLengthStyle } from '../lengthStyle.js'; - -import { DeviceSpec } from '@keymanapp/web-utils'; -import type { PredictionContext, StateChangeEnum } from '@keymanapp/input-processor'; -import { createUnselectableElement } from 'keyman/engine/dom-utils'; +import { BlankBanner } from './blankBanner.js'; /** * This object is used to specify options by both `BannerManager.getOptions` @@ -18,7 +17,7 @@ export interface BannerOptions { imagePath?: string; } -export type BannerType = "blank" | "image" | "suggestion"; +export type BannerType = "blank" | "image" | "suggestion" | "html"; interface BannerViewEventMap { 'bannerchange': () => void; @@ -58,9 +57,13 @@ interface BannerViewEventMap { * needs to reserve this space (i.e: Keyman for iOS), * rather than as its standalone app. */ -export default class BannerView implements OSKViewComponent { +export class BannerView implements OSKViewComponent { private bannerContainer: HTMLDivElement; - private activeBanner: Banner; + + /** + * The currently active banner. + */ + private currentBanner: Banner; private _activeBannerHeight: number = Banner.DEFAULT_HEIGHT; public readonly events = new EventEmitter(); @@ -91,32 +94,31 @@ export default class BannerView implements OSKViewComponent { * Applies any stylesheets needed by specific `Banner` instances. */ public appendStyles() { - if(this.activeBanner) { - this.activeBanner.appendStyleSheet(); + if(this.currentBanner) { + this.currentBanner.appendStyleSheet(); } } public get banner(): Banner { - return this.activeBanner; + return this.currentBanner; } /** - * Sets the active `Banner` to the specified type, regardless of - * existing management logic settings. - * - * @param banner The `Banner` instance to set as active. + * The `Banner` actively being displayed to the user in the OSK's current state, + * whether a `SuggestionBanner` (with predictive-text active) or a different + * type for use when the predictive-text engine is inactive. */ public set banner(banner: Banner) { - if(this.activeBanner) { - if(banner == this.activeBanner) { + if(this.currentBanner) { + if(banner == this.currentBanner) { return; } else { - let prevBanner = this.activeBanner; - this.activeBanner = banner; + let prevBanner = this.currentBanner; + this.currentBanner = banner; this.bannerContainer.replaceChild(banner.getDiv(), prevBanner.getDiv()); } } else { - this.activeBanner = banner; + this.currentBanner = banner; if(banner) { this.bannerContainer.appendChild(banner.getDiv()); } @@ -133,8 +135,8 @@ export default class BannerView implements OSKViewComponent { * Gets the height (in pixels) of the active `Banner` instance. */ public get height(): number { - if(this.activeBanner) { - return this.activeBanner.height; + if(this.currentBanner) { + return this.currentBanner.height; } else { return 0; } @@ -150,8 +152,8 @@ export default class BannerView implements OSKViewComponent { public set activeBannerHeight(h: number) { this._activeBannerHeight = h; - if (this.activeBanner && !(this.activeBanner instanceof BlankBanner)) { - this.activeBanner.height = h; + if (this.currentBanner && !(this.currentBanner instanceof BlankBanner)) { + this.currentBanner.height = h; } } @@ -160,154 +162,4 @@ export default class BannerView implements OSKViewComponent { } public refreshLayout() {}; -} - -export class BannerController { - private _activeType: BannerType; - private _options: BannerOptions = {}; - private container: BannerView; - private alwaysShow: boolean; - private imagePath?: string = ""; - - private predictionContext?: PredictionContext; - - private readonly hostDevice: DeviceSpec; - - public static readonly DEFAULT_OPTIONS: BannerOptions = { - alwaysShow: false, - imagePath: "" - } - - constructor(bannerView: BannerView, hostDevice: DeviceSpec, predictionContext?: PredictionContext) { - // Step 1 - establish the container element. Must come before this.setOptions. - this.hostDevice = hostDevice; - this.container = bannerView; - this.predictionContext = predictionContext; - - // Initialize with the default options - any 'manually set' options come post-construction. - // This will also automatically set the default banner in place. - this.setOptions(BannerController.DEFAULT_OPTIONS); - } - - /** - * This function corresponds to `keyman.osk.banner.getOptions`. - * - * Gets the current control settings in use by `BannerManager`. - */ - public getOptions(): BannerOptions { - let retObj = {}; - - for(let key in this._options) { - retObj[key] = this._options[key]; - } - - return retObj; - } - - /** - * This function corresponds to `keyman.osk.banner.setOptions`. - * - * Sets options used to tweak the automatic `Banner` - * control logic used by `BannerManager`. - * @param optionSpec An object specifying one or more of the following options: - * * `persistentBanner` (boolean) When `true`, ensures that a `Banner` - * is always displayed, even when no predictive model exists - * for the active language. - * - * Default: `false` - * * `imagePath` (URL string) Specifies the file path to use for an - * `ImageBanner` when `persistentBanner` is `true` and no predictive model exists. - * - * Default: `''`. - * * `enablePredictions` (boolean) Turns KMW predictions - * on (when `true`) and off (when `false`). - * - * Default: `true`. - */ - public setOptions(optionSpec: BannerOptions) { - for(let key in optionSpec) { - switch(key) { - // Each defined option may require specialized handling. - case 'alwaysShow': - // Determines the banner type to activate. - this.alwaysShow = optionSpec[key]; - break; - case 'imagePath': - // Determines the image file to use for ImageBanners. - this.imagePath = optionSpec[key]; - break; - default: - // Invalid option specified! - } - this._options[key] = optionSpec[key]; - - // If no banner instance exists yet, go with a safe, blank initialization. - if(!this.container.banner) { - this.selectBanner('inactive'); - } - } - } - - /** - * Sets the active `Banner` to the specified type, regardless of - * existing management logic settings. - * - * @param type `'blank' | 'image' | 'suggestion'` - A plain-text string - * representing the type of `Banner` to set active. - * @param height - Optional banner height in pixels. - */ - public setBanner(type: BannerType) { - var banner: Banner; - - let oldBanner = this.container.banner; - if(oldBanner instanceof SuggestionBanner) { - this.predictionContext.off('update', oldBanner.onSuggestionUpdate); - } - - switch(type) { - case 'blank': - banner = new BlankBanner(); - break; - case 'image': - banner = new ImageBanner(this.imagePath, this.container.activeBannerHeight); - break; - case 'suggestion': - let suggestBanner = banner = new SuggestionBanner(this.hostDevice, this.container.activeBannerHeight); - suggestBanner.predictionContext = this.predictionContext; - suggestBanner.events.on('apply', (selection) => this.predictionContext.accept(selection.suggestion)); - - this.predictionContext.on('update', suggestBanner.onSuggestionUpdate); - break; - default: - throw new Error("Invalid type specified for the banner!"); - } - - this._activeType = type; - - if(banner) { - this.container.banner = banner; - } - } - - /** - * Handles `LanguageProcessor`'s `'statechange'` events, - * allowing logic to automatically hot-swap `Banner`s as needed. - * @param state - */ - selectBanner(state: StateChangeEnum) { - // Only display a SuggestionBanner when LanguageProcessor states it is active. - if(state == 'active' || state == 'configured') { - this.setBanner('suggestion'); - } else if(state == 'inactive') { - if(this.alwaysShow) { - this.setBanner('image'); - } else { - this.setBanner('blank'); - } - } - } - - public get activeType(): BannerType { - return this._activeType; - } } \ No newline at end of file diff --git a/web/src/engine/osk/src/banner/blankBanner.ts b/web/src/engine/osk/src/banner/blankBanner.ts new file mode 100644 index 00000000000..0c4b0acc143 --- /dev/null +++ b/web/src/engine/osk/src/banner/blankBanner.ts @@ -0,0 +1,13 @@ +import { Banner } from "./banner.js"; + +/** + * Function BlankBanner + * Description A banner of height 0 that should not be shown + */ +export class BlankBanner extends Banner { + readonly type = 'blank'; + + constructor() { + super(0); + } +} \ No newline at end of file diff --git a/web/src/engine/osk/src/banner/htmlBanner.ts b/web/src/engine/osk/src/banner/htmlBanner.ts new file mode 100644 index 00000000000..722f2e71a03 --- /dev/null +++ b/web/src/engine/osk/src/banner/htmlBanner.ts @@ -0,0 +1,32 @@ +import { Banner } from "./banner.js"; + +export class HTMLBanner extends Banner { + readonly container: ShadowRoot | HTMLElement; + readonly type = 'html'; + + constructor(contents?: string) { + super(); + + const bannerHost = this.getDiv(); + + // Ensure any HTML styling applied for the banner contents only apply to the contents, + // and not the banner's `position: 'relative'` hosting element. + const div = document.createElement('div'); + div.style.userSelect = 'none'; + div.style.height = '100%'; + div.style.width = '100%'; + bannerHost.appendChild(div); + + // If possible, quarantine styling and JS for the banner contents within Shadow DOM. + this.container = (div.attachShadow) ? div.attachShadow({mode: 'closed'}) : div; + this.container.innerHTML = contents; + } + + get innerHTML() { + return this.container.innerHTML; + } + + set innerHTML(raw: string) { + this.container.innerHTML = raw; + } +} \ No newline at end of file diff --git a/web/src/engine/osk/src/banner/imageBanner.ts b/web/src/engine/osk/src/banner/imageBanner.ts new file mode 100644 index 00000000000..e860f218d52 --- /dev/null +++ b/web/src/engine/osk/src/banner/imageBanner.ts @@ -0,0 +1,55 @@ +import { Banner } from "./banner.js"; + +/** + * Function ImageBanner + * @param {string} imagePath Path of image to display in the banner + * @param {number} height If provided, the height of the banner in pixels + * Description Display an image in the banner + */ +export class ImageBanner extends Banner { + private img: HTMLElement; + readonly type; + + constructor(imagePath: string, height?: number) { + if (imagePath.length > 0) { + super(); + if (height) { + this.height = height; + } + } else { + super(0); + } + + this.type = 'image'; + + if(imagePath.indexOf('base64') >=0) { + console.log("Loading img from base64 data"); + } else { + console.log("Loading img with src '" + imagePath + "'"); + } + this.img = document.createElement('img'); + this.img.setAttribute('src', imagePath); + let ds = this.img.style; + + // We may want to eliminate the width-spec in the future, once we're sure of + // no unintended side-effects for iOS's use of this banner. + // + // Maybe if/when we also add a style="background-color: #xxx" option. + ds.width = '100%'; + ds.height = '100%'; + this.getDiv().appendChild(this.img); + console.log("Image loaded."); + } + + /** + * Function setImagePath + * Scope Public + * @param {string} imagePath Path of image to display in the banner + * Description Update the image in the banner + */ + public setImagePath(imagePath: string) { + if (this.img) { + this.img.setAttribute('src', imagePath); + } + } +} \ No newline at end of file diff --git a/web/src/engine/osk/src/banner/suggestionBanner.ts b/web/src/engine/osk/src/banner/suggestionBanner.ts new file mode 100644 index 00000000000..c5ff7255569 --- /dev/null +++ b/web/src/engine/osk/src/banner/suggestionBanner.ts @@ -0,0 +1,425 @@ + +import { type PredictionContext } from '@keymanapp/input-processor'; +import { createUnselectableElement } from 'keyman/engine/dom-utils'; + +import InputEventEngine, { InputEventEngineConfig } from '../input/event-interpreter/inputEventEngine.js'; +import MouseEventEngine from '../input/event-interpreter/mouseEventEngine.js'; +import TouchEventEngine from '../input/event-interpreter/touchEventEngine.js'; +import UITouchHandlerBase from '../input/event-interpreter/uiTouchHandlerBase.js'; +import { DeviceSpec, Keyboard, KeyboardProperties } from '@keymanapp/keyboard-processor'; +import { Banner } from './banner.js'; +import EventEmitter from 'eventemitter3'; + +export class BannerSuggestion { + div: HTMLDivElement; + private display: HTMLSpanElement; + private fontFamily?: string; + private rtl: boolean = false; + + private _suggestion: Suggestion; + + private index: number; + + static readonly BASE_ID = 'kmw-suggestion-'; + + constructor(index: number, isRTL: boolean) { + this.index = index; + this.rtl = isRTL; + + this.constructRoot(); + + // Provides an empty, base SPAN for text display. We'll swap these out regularly; + // `Suggestion`s will have varying length and may need different styling. + let display = this.display = createUnselectableElement('span'); + this.div.appendChild(display); + } + + private constructRoot() { + // Add OSK suggestion labels + let div = this.div = createUnselectableElement('div'), ds=div.style; + div.className = "kmw-suggest-option"; + div.id = BannerSuggestion.BASE_ID + this.index; + + // Ensures that a reasonable width % is set. + let usableWidth = 100 - SuggestionBanner.MARGIN * (SuggestionBanner.SUGGESTION_LIMIT - 1); + let widthpc = usableWidth / SuggestionBanner.SUGGESTION_LIMIT; + + ds.width = widthpc + '%'; + + this.div['suggestion'] = this; + } + + public matchKeyboardProperties(keyboardProperties: KeyboardProperties) { + const div = this.div; + + if(keyboardProperties) { + if (keyboardProperties['KLC']) { + div.lang = keyboardProperties['KLC']; + } + + // Establish base font settings + let font = keyboardProperties['KFont']; + if(font && font.family && font.family != '') { + div.style.fontFamily = this.fontFamily = font.family; + } + } + } + + get suggestion(): Suggestion { + return this._suggestion; + } + + /** + * Function update + * @param {string} id Element ID for the suggestion span + * @param {Suggestion} suggestion Suggestion from the lexical model + * Description Update the ID and text of the BannerSuggestionSpec + */ + public update(suggestion: Suggestion) { + this._suggestion = suggestion; + this.updateText(); + } + + private updateText() { + let display = this.generateSuggestionText(this.rtl); + this.div.replaceChild(display, this.display); + this.display = display; + } + + public isEmpty(): boolean { + return !this._suggestion; + } + + /** + * Function generateSuggestionText + * @return {HTMLSpanElement} Span element of the suggestion + * Description Produces a HTMLSpanElement with the key's actual text. + */ + // + public generateSuggestionText(rtl: boolean): HTMLSpanElement { + let suggestion = this._suggestion; + var suggestionText: string; + + var s=createUnselectableElement('span'); + s.className = 'kmw-suggestion-text'; + + if(suggestion == null) { + return s; + } + + if(suggestion.displayAs == null || suggestion.displayAs == '') { + suggestionText = '\xa0'; // default: nbsp. + } else { + // Default the LTR ordering to match that of the active keyboard. + let orderCode = rtl ? 0x202e /* RTL */ : 0x202d /* LTR */; + suggestionText = String.fromCharCode(orderCode) + suggestion.displayAs; + } + + // TODO: Dynamic suggestion text resizing. (Refer to OSKKey.getTextWidth in visualKeyboard.ts.) + + // Finalize the suggestion text + s.innerHTML = suggestionText; + return s; + } +} + +/** + * Function SuggestionBanner + * Scope Public + * @param {number} height - If provided, the height of the banner in pixels + * Description Display lexical model suggestions in the banner + */ +export class SuggestionBanner extends Banner { + public static readonly SUGGESTION_LIMIT: number = 3; + public static readonly MARGIN = 1; + + public readonly events: EventEmitter; + + private currentSuggestions: Suggestion[] = []; + + private options : BannerSuggestion[] = []; + private hostDevice: DeviceSpec; + + private manager: SuggestionInputManager; + + readonly type = 'suggestion'; + + private _predictionContext: PredictionContext; + + static readonly TOUCHED_CLASS: string = 'kmw-suggest-touched'; + static readonly BANNER_CLASS: string = 'kmw-suggest-banner'; + + constructor(hostDevice: DeviceSpec, height?: number) { + super(height || Banner.DEFAULT_HEIGHT); + this.hostDevice = hostDevice; + + this.getDiv().className = this.getDiv().className + ' ' + SuggestionBanner.BANNER_CLASS; + + this.buildInternals(false); + + this.manager = new SuggestionInputManager(this.getDiv()); + this.events = this.manager.events; + + this.setupInputHandling(); + } + + buildInternals(rtl: boolean) { + if(this.options.length > 0) { + this.options.splice(0, this.options.length); // Clear the array. + } + for (var i=0; i { + const elem = suggestion.div; + let classes = elem.className; + let cs = ' ' + SuggestionBanner.TOUCHED_CLASS; + + if(on && classes.indexOf(cs) < 0) { + elem.className=classes+cs; + } else { + elem.className=classes.replace(cs,''); + } + }); + + this.manager.events.on('apply', (option) => { + if(this.predictionContext) { + this.predictionContext.accept(option.suggestion); + } + }); + } + + public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) { + const rtl = keyboard.isRTL; + + // Removes all previous children. (.replaceChildren requires Chrome for Android 86.) + // Instantly replaces all children with an empty text node, bypassing the need to actually + // parse incoming HTML. + // + // Just in case, alternative approaches: https://stackoverflow.com/a/3955238 + this.getDiv().textContent = ''; + + // Builds new children to match needed RTL properties. + this.buildInternals(rtl); + + this.options.forEach((option) => option.matchKeyboardProperties(keyboardProperties)); + this.onSuggestionUpdate(this.currentSuggestions); // restore suggestions + } + + private get mouseEventConfig() { + const config: InputEventEngineConfig = { + targetRoot: this.getDiv(), + // document.body is the event root b/c we need to track the mouse if it leaves + // the VisualKeyboard's hierarchy. + eventRoot: document.body, + inputStartHandler: this.manager.touchStart.bind(this.manager), + inputMoveHandler: this.manager.touchMove.bind(this.manager), + inputEndHandler: this.manager.touchEnd.bind(this.manager), + coordConstrainedWithinInteractiveBounds: function() { return true; } + }; + + return new MouseEventEngine(config); + } + + private get touchEventConfig() { + const config: InputEventEngineConfig = { + targetRoot: this.getDiv(), + // document.body is the event root b/c we need to track the mouse if it leaves + // the VisualKeyboard's hierarchy. + eventRoot: this.getDiv(), + inputStartHandler: this.manager.touchStart.bind(this.manager), + inputMoveHandler: this.manager.touchMove.bind(this.manager), + inputEndHandler: this.manager.touchEnd.bind(this.manager), + coordConstrainedWithinInteractiveBounds: function() { return true; } + }; + + return new TouchEventEngine(config); + } + + public get predictionContext(): PredictionContext { + return this._predictionContext; + } + + public set predictionContext(context: PredictionContext) { + if(this._predictionContext) { + // disconnect the old one! + this._predictionContext.off('update', this.onSuggestionUpdate); + } + + // connect the new one! + this._predictionContext = context; + if(context) { + context.on('update', this.onSuggestionUpdate); + this.onSuggestionUpdate(context.currentSuggestions); + } + } + + public onSuggestionUpdate = (suggestions: Suggestion[]): void => { + this.currentSuggestions = suggestions; + + this.options.forEach((option: BannerSuggestion, i: number) => { + if(i < suggestions.length) { + option.update(suggestions[i]); + } else { + option.update(null); + } + }); + } +} + +interface SuggestionInputEventMap { + highlight: (bannerSuggestion: BannerSuggestion, state: boolean) => void, + apply: (bannerSuggestion: BannerSuggestion) => void; + hold: (bannerSuggestion: BannerSuggestion) => void; +} + +class SuggestionInputManager extends UITouchHandlerBase { + public readonly events = new EventEmitter(); + + private eventDisablePromise: Promise; + + platformHold: (suggestion: BannerSuggestion, isCustom: boolean) => void; + + //#region Touch handling implementation + findTargetFrom(e: HTMLElement): HTMLDivElement { + try { + if(e) { + if(e.classList.contains('kmw-suggest-option')) { + return e as HTMLDivElement; + } + if(e.parentElement && e.parentElement.classList.contains('kmw-suggest-option')) { + return e.parentElement as HTMLDivElement; + } + // if(e.firstChild && util.hasClass( e.firstChild,'kmw-suggest-option')) { + // return e.firstChild as HTMLDivElement; + // } + } + } catch(ex) {} + return null; + } + + protected highlight(t: HTMLDivElement, on: boolean): void { + let suggestion = t['suggestion'] as BannerSuggestion; + + // Never highlight an empty suggestion button. + if(suggestion.isEmpty()) { + on = false; + } + + this.events.emit('highlight', suggestion, on); + } + + protected select(t: HTMLDivElement): void { + this.events.emit('apply', t['suggestion'] as BannerSuggestion); + } + + //#region Long-press support + protected hold(t: HTMLDivElement): void { + // let suggestionObj = t['suggestion'] as BannerSuggestion; + // + // // Is this the suggestion? It's never in this.currentSuggestions, so check against that. + // let isCustom = this.currentSuggestions.indexOf(suggestionObj.suggestion) == -1; + + this.events.emit('hold', t['suggestion'] as BannerSuggestion); + } + protected clearHolds(): void { + // Temp, pending implementation of suggestion longpress submenus + // - nothing to clear without them - + + // only really used in native-KMW + } + + protected hasModalPopup(): boolean { + return this.eventsBlocked; + } + + protected dealiasSubTarget(target: HTMLDivElement): HTMLDivElement { + return target; + } + + protected hasSubmenu(t: HTMLDivElement): boolean { + // Temp, pending implementation of suggestion longpress submenus + + // Only really used by native-KMW - see kmwnative's highlightSubKeys func. + return false; + } + + protected isSubmenuActive(): boolean { + // Temp, pending implementation of suggestion longpress submenus + + // Utilized only by native-KMW - it parallels hasModalPopup() in purpose. + return false; + } + + protected displaySubmenuFor(target: HTMLDivElement) { + // Utilized only by native-KMW to show submenus. + throw new Error("Method not implemented."); + } + //#endregion + //#endregion + + public get eventsBlocked(): boolean { + return !!this.eventDisablePromise; + } + + /** + * Intended for use by the mobile apps, which sometimes 'takes over' touch handling. + * For such cases, input should be blocked within KMW when the apps are managing an + * ongoing touch-hold for any other interaction. + * + * Formerly: + ``` + let keyman = com.keyman.singleton; + return keyman['osk'].vkbd.subkeyGesture && keyman.isEmbedded; + ``` + */ + public temporarilyBlockEvents(promise: Promise) { // TODO: ensure connection for embedded mode! + this.eventDisablePromise = promise; // Will require routing; this class is not exported! + promise.finally(() => { + this.eventDisablePromise = null; + }) + } + + constructor(div: HTMLElement) { + // TODO: Determine appropriate CSS styling names, etc. + super(div, Banner.BANNER_CLASS, SuggestionBanner.TOUCHED_CLASS); + } +} diff --git a/web/src/engine/osk/src/index.ts b/web/src/engine/osk/src/index.ts index b5729285d95..fd363dd123b 100644 --- a/web/src/engine/osk/src/index.ts +++ b/web/src/engine/osk/src/index.ts @@ -4,7 +4,7 @@ export { default as OSKView } from './views/oskView.js'; export { default as FloatingOSKView, FloatingOSKViewConfiguration } from './views/floatingOskView.js'; export { default as AnchoredOSKView } from './views/anchoredOskView.js'; export { default as InlinedOSKView } from './views/inlinedOskView.js'; -export { BannerController } from './banner/bannerView.js'; +export { BannerController } from './banner/bannerController.js'; // Is referenced by at least one desktop UI module. export { FloatingOSKCookie as FloatingOSKViewCookie } from './views/floatingOskCookie.js'; export { default as VisualKeyboard } from './visualKeyboard.js'; @@ -20,6 +20,8 @@ export { type OSKKeySpec } from './keyboard-layout/oskKey.js'; export { type default as InputEventCoordinate } from './input/inputEventCoordinate.js'; export { type default as EmbeddedGestureConfig } from './config/embeddedGestureConfig.js'; +export { getViewportScale } from './screenUtils.js'; + export { default as Activator, StaticActivator } from './views/activator.js'; export { default as SimpleActivator } from './views/simpleActivator.js'; export { default as TwoStateActivator } from './views/twoStateActivator.js'; diff --git a/web/src/engine/osk/src/keyboard-layout/oskKey.ts b/web/src/engine/osk/src/keyboard-layout/oskKey.ts index 77d8927b60a..d21939f2ec0 100644 --- a/web/src/engine/osk/src/keyboard-layout/oskKey.ts +++ b/web/src/engine/osk/src/keyboard-layout/oskKey.ts @@ -208,6 +208,7 @@ export default abstract class OSKKey { /** * Calculate the font size required for a key cap, scaling to fit longer text * @param vkbd + * @param text * @param style specification for the desired base font size * @param override if true, don't use the font spec from the button, just use the passed in spec * @returns font size as a style string @@ -239,14 +240,15 @@ export default abstract class OSKKey { const MAX_X_PROPORTION = 0.90; const MAX_Y_PROPORTION = 0.90; const X_PADDING = 2; - const Y_PADDING = 2; var fontHeight: number, keyHeight: number; if(metrics.fontBoundingBoxAscent) { fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent; } - let textHeight = fontHeight ? fontHeight + Y_PADDING : 0; + // Don't add extra padding to height - multiplying with MAX_Y_PROPORTION already gives + // padding + let textHeight = fontHeight ?? 0; if(style.height && style.height.indexOf('px') != -1) { keyHeight = Number.parseFloat(style.height.substring(0, style.height.indexOf('px'))); } diff --git a/web/src/engine/osk/src/screenUtils.ts b/web/src/engine/osk/src/screenUtils.ts index 474584c1d1b..c2906c2d6dc 100644 --- a/web/src/engine/osk/src/screenUtils.ts +++ b/web/src/engine/osk/src/screenUtils.ts @@ -1,16 +1,17 @@ import { landscapeView } from "keyman/engine/dom-utils"; +import { DeviceSpec } from "@keymanapp/web-utils"; /** * Get viewport scale factor for this document * * @return {number} */ -export function getViewportScale(): number { +export function getViewportScale(formFactor: DeviceSpec.FormFactor): number { // This can sometimes fail with some browsers if called before document defined, // so catch the exception try { // For emulation of iOS on a desktop device, use a default value - if(this.device.formFactor == 'desktop') { + if(formFactor == 'desktop') { return 1; } diff --git a/web/src/engine/osk/src/views/anchoredOskView.ts b/web/src/engine/osk/src/views/anchoredOskView.ts index be9ab957898..458577ce2f3 100644 --- a/web/src/engine/osk/src/views/anchoredOskView.ts +++ b/web/src/engine/osk/src/views/anchoredOskView.ts @@ -161,7 +161,7 @@ export default class AnchoredOSKView extends OSKView { // Correct for viewport scaling (iOS - Android 4.2 does not want this, at least on Galaxy Tab 3)) if(this.targetDevice.OS == DeviceSpec.OperatingSystem.iOS) { - height=height/getViewportScale(); + height=height/getViewportScale(this.targetDevice.formFactor); } return height; diff --git a/web/src/engine/osk/src/views/floatingOskView.ts b/web/src/engine/osk/src/views/floatingOskView.ts index 2e1ef462720..2399eefe258 100644 --- a/web/src/engine/osk/src/views/floatingOskView.ts +++ b/web/src/engine/osk/src/views/floatingOskView.ts @@ -314,7 +314,7 @@ export default class FloatingOSKView extends OSKView { // Correct for viewport scaling (iOS - Android 4.2 does not want this, at least on Galaxy Tab 3)) if(this.targetDevice.OS == DeviceSpec.OperatingSystem.iOS) { - height=height/getViewportScale(); + height=height/getViewportScale(this.targetDevice.formFactor); } return height; diff --git a/web/src/engine/osk/src/views/oskView.ts b/web/src/engine/osk/src/views/oskView.ts index dfc6aa18db1..0399991fe08 100644 --- a/web/src/engine/osk/src/views/oskView.ts +++ b/web/src/engine/osk/src/views/oskView.ts @@ -1,6 +1,7 @@ import EventEmitter from 'eventemitter3'; -import BannerView, { BannerController } from '../banner/bannerView.js'; +import { BannerView } from '../banner/bannerView.js'; +import { BannerController } from '../banner/bannerController.js'; import OSKViewComponent from '../components/oskViewComponent.interface.js'; import EmptyView from '../components/emptyView.js'; import HelpPageView from '../components/helpPageView.js'; diff --git a/web/src/engine/osk/src/visualKeyboard.ts b/web/src/engine/osk/src/visualKeyboard.ts index 204b99bfbbc..0f40f08a322 100644 --- a/web/src/engine/osk/src/visualKeyboard.ts +++ b/web/src/engine/osk/src/visualKeyboard.ts @@ -1395,7 +1395,7 @@ export default class VisualKeyboard extends EventEmitter implements Ke var fs = 1.0; // TODO: Logically, this should be needed for Android, too - may need to be changed for the next version! if (device.OS == DeviceSpec.OperatingSystem.iOS && !this.isEmbedded) { - fs = fs / getViewportScale(); + fs = fs / getViewportScale(this.device.formFactor); } let paddedHeight: number; diff --git a/web/src/resources/.gitignore b/web/src/resources/.gitignore new file mode 100644 index 00000000000..ede6194347b --- /dev/null +++ b/web/src/resources/.gitignore @@ -0,0 +1,2 @@ +# Copied from configure +keymanweb-osk.ttf diff --git a/web/src/resources/osk/keymanweb-osk.ttf b/web/src/resources/osk/keymanweb-osk.ttf deleted file mode 100644 index e0651b14d4f..00000000000 Binary files a/web/src/resources/osk/keymanweb-osk.ttf and /dev/null differ diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index 029a0f74665..7bab478eb7d 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -526,7 +526,8 @@ div.android div.kmw-keytip-cap { .kmw-wait-graphic{width:100%;min-height:19px;background:url('ajax-loader.gif') no-repeat;background-position:center top;} .kmw-alert-text{margin:10px;white-space:default;font-family:Arial,sans-serif;} -.kmw-spacebar-caption{font:0.6em Arial !important;color:rgba(0,0,0,0.25);} +/* Note: font size gets overwritten in source code! */ +.kmw-spacebar-caption{font:1em Arial !important;color:rgba(0,0,0,0.25);} /* Probably best to make this its own CSS that can be optionally included? */ @media (prefers-color-scheme: dark) { diff --git a/windows/src/desktop/inst/download.in b/windows/src/desktop/inst/download.in index aab048d8508..689a3065e49 100644 --- a/windows/src/desktop/inst/download.in +++ b/windows/src/desktop/inst/download.in @@ -73,9 +73,9 @@ candle-locale: # setup-inf: - echo [Setup] > setup.inf - echo Version=$VersionWin >> setup.inf - echo MSIFileName=keymandesktop.msi >> setup.inf - echo MSIOptions= >> setup.inf - echo License=license.html >> setup.inf - echo [Packages] >> setup.inf + echo [Setup]>setup.inf + echo Version=$VersionWin>>setup.inf + echo MSIFileName=keymandesktop.msi>>setup.inf + echo MSIOptions=>>setup.inf + echo License=license.html>>setup.inf + echo [Packages]>>setup.inf diff --git a/windows/src/desktop/setup/Keyman.Setup.System.InstallInfo.pas b/windows/src/desktop/setup/Keyman.Setup.System.InstallInfo.pas index b90a583f1fc..951ae8da980 100644 --- a/windows/src/desktop/setup/Keyman.Setup.System.InstallInfo.pas +++ b/windows/src/desktop/setup/Keyman.Setup.System.InstallInfo.pas @@ -270,7 +270,7 @@ procedure TInstallInfo.LoadSetupInf(const SetupInfPath: string); FInPackages: Boolean; FInStrings: Boolean; location: TInstallInfoFileLocation; - val, nm: WideString; + s, val, nm: string; pack: TInstallInfoPackage; packLocation: TInstallInfoPackageFileLocation; FVersion, FMSIFileName: string; @@ -286,16 +286,17 @@ procedure TInstallInfo.LoadSetupInf(const SetupInfPath: string); for i := 0 to Count - 1 do begin - if Trim(Strings[i]) = '' then Continue; + s := Trim(Strings[i]); + if s = '' then Continue; - nm := KeyNames[i]; - val := ValueFromIndex[i]; + nm := Trim(KeyNames[i]); + val := Trim(ValueFromIndex[i]); - if Copy(Strings[i], 1, 1) = '[' then + if Copy(s, 1, 1) = '[' then begin - FInSetup := WideSameText(Strings[i], '[Setup]'); - FInPackages := WideSameText(Strings[i], '[Packages]'); - FInStrings := WideSameText(Strings[i], '[Strings]'); + FInSetup := WideSameText(s, '[Setup]'); + FInPackages := WideSameText(s, '[Packages]'); + FInStrings := WideSameText(s, '[Strings]'); end else if FInSetup then begin @@ -318,7 +319,7 @@ procedure TInstallInfo.LoadSetupInf(const SetupInfPath: string); end; end else if FInStrings then - FStrings.Add(Strings[i]); + FStrings.Add(s); end; if System.SysUtils.FileExists(FMSIFileName) then // I3476 diff --git a/windows/src/engine/keyman32/K32_load.cpp b/windows/src/engine/keyman32/K32_load.cpp index 999d8941c9c..9917e7edbf5 100644 --- a/windows/src/engine/keyman32/K32_load.cpp +++ b/windows/src/engine/keyman32/K32_load.cpp @@ -30,17 +30,6 @@ #include "pch.h" -HBITMAP LoadBitmapFile(LPBYTE data, DWORD sz); -BOOL VerifyKeyboard(LPBYTE filebase); - -#ifdef _WIN64 -LPKEYBOARD CopyKeyboard(PBYTE bufp, PBYTE base, DWORD dwFileSize); -#else -LPKEYBOARD FixupKeyboard(PBYTE bufp, PBYTE base, DWORD dwFileSize); -#endif - -HBITMAP LoadBitmapFileEx(PBYTE filebase); - BOOL GetKeyboardFileName(LPSTR kbname, LPSTR buf, int nbuf) { PKEYMAN64THREADDATA _td = ThreadGlobals(); @@ -128,768 +117,3 @@ BOOL LoadlpKeyboard(int i) return TRUE; } - -/* - * Instead of performing a straightforward calculation of the 32 bit - * CRC using a series of logical operations, this program uses the - * faster table lookup method. This routine is called once when the - * program starts up to build the table which will be used later - * when calculating the CRC values. - */ - -#define CRC32_POLYNOMIAL 0xEDB88320L - -unsigned long CRCTable[256]; - -void BuildCRCTable(void) -{ - static BOOL TableBuilt = FALSE; - int i; - int j; - unsigned long crc; - - if(!TableBuilt) - { - for(i = 0; i <= 255; i++) - { - crc = i; - - for(j = 8; j > 0; j--) - if(crc & 1) crc = (crc >> 1) ^ CRC32_POLYNOMIAL; else crc >>= 1; - - CRCTable[i] = crc; - } - } -} - - -/* - * This routine calculates the CRC for a block of data using the - * table lookup method. It accepts an original value for the crc, - * and returns the updated value. - */ - -unsigned long CalculateBufferCRC(unsigned long count, BYTE *p) -{ - unsigned long temp1; - unsigned long temp2; - unsigned long crc = 0xFFFFFFFFL; - - BuildCRCTable(); - - while (count-- != 0) - { - temp1 = ( crc >> 8 ) & 0x00FFFFFFL; - temp2 = CRCTable[((int) crc ^ *p++) & 0xff]; - crc = temp1 ^ temp2; - } - - return crc; -} - -//#define Err(s) - -void Err(char *s) -{ - SendDebugMessageFormat(0, sdmLoad, 0, "LoadKeyboard: %s", s); -} - -BOOL LoadKeyboard(LPSTR fileName, LPKEYBOARD *lpKeyboard) -{ - DWORD sz; - LPBYTE buf; - HANDLE hFile; - LPKEYBOARD kbp; - PBYTE filebase; - - if(!fileName || !lpKeyboard) - { - Err("Bad Filename"); - return FALSE; - } - - hFile = CreateFile(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); - if(hFile == INVALID_HANDLE_VALUE) - { - Err("Could not open file"); - return FALSE; - } - - sz = GetFileSize(hFile, NULL); - -#ifdef _WIN64 - // allocate enough memory for expanded data structure + original data. - // Expanded data structure is double the size of data on disk (8-byte - // pointers) - on disk the "pointers" are relative to the beginning of - // the file. - // We save the original data at the end of buf; we don't copy strings, so - // those will remain in the location at the end of the buffer. - buf = new BYTE[sz * 3]; -#else - buf = new BYTE[sz]; -#endif - - if(!buf) - { - CloseHandle(hFile); - Err("Not allocmem"); - return FALSE; - } -#ifdef _WIN64 - filebase = buf + sz*2; -#else - filebase = buf; -#endif - - ReadFile(hFile, filebase, sz, &sz, NULL); - CloseHandle(hFile); - - PKEYMAN64THREADDATA _td = ThreadGlobals(); - if (!_td) { - delete[] buf; - return FALSE; - } - - if(*LPDWORD(filebase) != FILEID_COMPILED) - { - delete[] buf; - Err("Invalid file"); - return FALSE; - } - - if(!VerifyKeyboard(filebase)) return FALSE; - -#ifdef _WIN64 - kbp = CopyKeyboard(buf, filebase, sz); -#else - kbp = FixupKeyboard(buf, filebase, sz); -#endif - - if(!kbp) return FALSE; - - if(kbp->dwIdentifier != FILEID_COMPILED) { delete buf; Err("errNotFileID"); return FALSE; } - - kbp->hBitmap = LoadBitmapFileEx(filebase); - - *lpKeyboard = kbp; - - return TRUE; -} - - - -// These next two structs represent how the icon information is stored -// in an ICO file. -typedef struct -{ - BYTE bWidth; // Width of the image - BYTE bHeight; // Height of the image (times 2) - BYTE bColorCount; // Number of colors in image (0 if >=8bpp) - BYTE bReserved; // Reserved - WORD wPlanes; // Color Planes - WORD wBitCount; // Bits per pixel - DWORD dwBytesInRes; // how many bytes in this resource? - DWORD dwImageOffset; // where in the file is this image -} ICONDIRENTRY, *LPICONDIRENTRY; -typedef struct -{ - WORD idReserved; // Reserved - WORD idType; // resource type (1 for icons) - WORD idCount; // how many images? - ICONDIRENTRY idEntries[1]; // the entries for each image -} ICONDIR, *LPICONDIR; - -// The following two structs are for the use of this program in -// manipulating icons. They are more closely tied to the operation -// of this program than the structures listed above. One of the -// main differences is that they provide a pointer to the DIB -// information of the masks. -typedef struct -{ - UINT Width, Height, Colors; // Width, Height and bpp - LPBYTE lpBits; // ptr to DIB bits - DWORD dwNumBytes; // how many bytes? - LPBITMAPINFO lpbi; // ptr to header - LPBYTE lpXOR; // ptr to XOR image bits - LPBYTE lpAND; // ptr to AND image bits -} ICONIMAGE, *LPICONIMAGE; -typedef struct -{ - UINT nNumImages; // How many images? - ICONIMAGE IconImages[1]; // Image entries -} ICONRESOURCE, *LPICONRESOURCE; -/****************************************************************************/ -LPICONRESOURCE ReadIconFromICOFile( PBYTE buf, int sz ); -void FreeIconResource(LPICONRESOURCE lpIR); -HICON MakeIconFromResource( LPICONIMAGE lpIcon ); - -HBITMAP LoadBitmapFile(LPBYTE data, DWORD sz) -{ - BITMAPFILEHEADER *bmfh; - BITMAPINFO *bmi; - HBITMAP hBitmap, hBitmap2, hOldBmp1, hOldBmp2; - HDC hDC, hSrcDC, hSrcDC2; - LPICONRESOURCE lpir = NULL; - - bmfh = (BITMAPFILEHEADER *) data; - - if(bmfh->bfType == 0x4D42) - { - SendDebugMessageFormat(0, sdmLoad, 0, "LoadKeyboard: Bitmap found"); - bmi = (BITMAPINFO *) (data + sizeof(BITMAPFILEHEADER)); - - hDC = GetDC(GetDesktopWindow()); - hSrcDC = CreateCompatibleDC(hDC); - hSrcDC2 = CreateCompatibleDC(hDC); - hBitmap = CreateDIBitmap(hDC, &bmi->bmiHeader, CBM_INIT, data + bmfh->bfOffBits, bmi, DIB_RGB_COLORS); - hBitmap2 = CreateCompatibleBitmap(hDC, 16, 16); - ReleaseDC(GetDesktopWindow(), hDC); - - hOldBmp1 = (HBITMAP) SelectObject(hSrcDC, hBitmap); - hOldBmp2 = (HBITMAP) SelectObject(hSrcDC2, hBitmap2); - BitBlt(hSrcDC2, 0, 0, 16, 16, hSrcDC, 0, 0, SRCCOPY); - SelectObject(hSrcDC, hOldBmp1); - SelectObject(hSrcDC2, hOldBmp2); - DeleteDC(hSrcDC); - DeleteDC(hSrcDC2); - DeleteObject(hBitmap); - } - else - { - SendDebugMessageFormat(0, sdmLoad, 0, "LoadKeyboard: Icon found"); - - lpir = ReadIconFromICOFile(data, sz); - if(!lpir) - { - SendDebugMessageFormat(0, sdmLoad, 0, "LoadKeyboard: icon not loaded"); - return 0; - } - - if(lpir->nNumImages == 0) - { - FreeIconResource(lpir); - return 0; - } - - HICON hIcon = MakeIconFromResource(&lpir->IconImages[0]); - //HICON hIcon = CreateIcon(GetModuleHandle(LIBRARY_NAME), lpir->IconImages[0].Width, lpir->IconImages[0].Height, - // 1, lpir->IconImages[0].Colors, lpir->IconImages[0].lpAND, lpir->IconImages[0].lpXOR); - FreeIconResource(lpir); - - - if(hIcon == 0) - { - DebugLastError("MakeIconFromResource"); - return 0; - } - hDC = GetDC(GetDesktopWindow()); - - hBitmap2 = CreateCompatibleBitmap(hDC, 16, 16); - hSrcDC = CreateCompatibleDC(hDC); - ReleaseDC(GetDesktopWindow(), hDC); - - hOldBmp2 = (HBITMAP) SelectObject(hSrcDC, hBitmap2); - DrawIconEx(hSrcDC, 0, 0, hIcon, 16, 16, 0, NULL, DI_NORMAL); - SelectObject(hSrcDC, hOldBmp2); - DeleteDC(hSrcDC); - - DestroyIcon(hIcon); - - } - return hBitmap2; -} - - - -/**************************************************************************** -* -* FUNCTION: ReadICOHeader -* -* PURPOSE: Reads the header from an ICO file -* -* PARAMS: HANDLE hFile - handle to the file -* -* RETURNS: UINT - Number of images in file, -1 for failure -* -* History: -* July '95 - Created -* -\****************************************************************************/ -UINT ReadICOHeader( PBYTE buf ) -{ - if(*(PWORD)(buf) != 0) return (UINT)-1; - if(*(PWORD)(buf+2) != 1) return (UINT)-1; - return *(PWORD)(buf+4); -} -/* End ReadICOHeader() ****************************************************/ - - - - -/**************************************************************************** -* -* FUNCTION: DIBNumColors -* -* PURPOSE: Calculates the number of entries in the color table. -* -* PARAMS: LPSTR lpbi - pointer to the CF_DIB memory block -* -* RETURNS: WORD - Number of entries in the color table. -* -* History: -* July '95 - Copied -* -\****************************************************************************/ -WORD DIBNumColors( LPSTR lpbi ) -{ - WORD wBitCount; - DWORD dwClrUsed; - - dwClrUsed = ((LPBITMAPINFOHEADER) lpbi)->biClrUsed; - - if (dwClrUsed) - return (WORD) dwClrUsed; - - wBitCount = ((LPBITMAPINFOHEADER) lpbi)->biBitCount; - - switch (wBitCount) - { - case 1: return 2; - case 4: return 16; - case 8: return 256; - } - return 0; -} -/* End DIBNumColors() ******************************************************/ - - - -/**************************************************************************** -* -* FUNCTION: PaletteSize -* -* PURPOSE: Calculates the number of bytes in the color table. -* -* PARAMS: LPSTR lpbi - pointer to the CF_DIB memory block -* -* RETURNS: WORD - number of bytes in the color table -* -* -* History: -* July '95 - Copied -* -\****************************************************************************/ -WORD PaletteSize( LPSTR lpbi ) -{ - return ( DIBNumColors( lpbi ) * sizeof( RGBQUAD ) ); -} -/* End PaletteSize() ********************************************************/ - -/**************************************************************************** -* -* FUNCTION: FindDIBits -* -* PURPOSE: Locate the image bits in a CF_DIB format DIB. -* -* PARAMS: LPSTR lpbi - pointer to the CF_DIB memory block -* -* RETURNS: LPSTR - pointer to the image bits -* -* History: -* July '95 - Copied -* -\****************************************************************************/ -LPSTR FindDIBBits( LPSTR lpbi ) -{ - return ( lpbi + *(LPDWORD)lpbi + PaletteSize( lpbi ) ); -} -/* End FindDIBits() *********************************************************/ - - - -// How wide, in bytes, would this many bits be, DWORD aligned? -#define WIDTHBYTES(bits) ((((bits) + 31)>>5)<<2) - -/**************************************************************************** -* -* FUNCTION: BytesPerLine -* -* PURPOSE: Calculates the number of bytes in one scan line. -* -* PARAMS: LPBITMAPINFOHEADER lpBMIH - pointer to the BITMAPINFOHEADER -* that begins the CF_DIB block -* -* RETURNS: DWORD - number of bytes in one scan line (DWORD aligned) -* -* History: -* July '95 - Created -* -\****************************************************************************/ -DWORD BytesPerLine( LPBITMAPINFOHEADER lpBMIH ) -{ - return WIDTHBYTES(lpBMIH->biWidth * lpBMIH->biPlanes * lpBMIH->biBitCount); -} -/* End BytesPerLine() ********************************************************/ - - -/**************************************************************************** -* -* FUNCTION: AdjustIconImagePointers -* -* PURPOSE: Adjusts internal pointers in icon resource struct -* -* PARAMS: LPICONIMAGE lpImage - the resource to handle -* -* RETURNS: BOOL - TRUE for success, FALSE for failure -* -* History: -* July '95 - Created -* -\****************************************************************************/ -BOOL AdjustIconImagePointers( LPICONIMAGE lpImage ) -{ - // Sanity check - if( lpImage==NULL ) - return FALSE; - // BITMAPINFO is at beginning of bits - lpImage->lpbi = (LPBITMAPINFO)lpImage->lpBits; - // Width - simple enough - lpImage->Width = lpImage->lpbi->bmiHeader.biWidth; - // Icons are stored in funky format where height is doubled - account for it - lpImage->Height = (lpImage->lpbi->bmiHeader.biHeight)/2; - // How many colors? - lpImage->Colors = lpImage->lpbi->bmiHeader.biPlanes * lpImage->lpbi->bmiHeader.biBitCount; - // XOR bits follow the header and color table - lpImage->lpXOR = (LPBYTE) FindDIBBits((LPSTR)lpImage->lpbi); - // AND bits follow the XOR bits - lpImage->lpAND = lpImage->lpXOR + (lpImage->Height*BytesPerLine((LPBITMAPINFOHEADER)(lpImage->lpbi))); - return TRUE; -} -/* End AdjustIconImagePointers() *******************************************/ - -/**************************************************************************** -* -* FUNCTION: ReadIconFromICOFile -* -* PURPOSE: Reads an Icon Resource from an ICO file -* -* PARAMS: LPCTSTR szFileName - Name of the ICO file -* -* RETURNS: LPICONRESOURCE - pointer to the resource, NULL for failure -* -* History: -* July '95 - Created -* -\****************************************************************************/ -LPICONRESOURCE ReadIconFromICOFile( PBYTE buf, int sz ) -{ - UNREFERENCED_PARAMETER(sz); - LPICONRESOURCE lpIR = NULL, lpNew = NULL; - HANDLE hFile = NULL; - //LPRESOURCEPOSINFO lpRPI = NULL; - UINT i; - LPICONDIRENTRY lpIDE = NULL; - - // Allocate memory for the resource structure - if( (lpIR = (LPICONRESOURCE) malloc( sizeof(ICONRESOURCE) )) == NULL ) - { - SendDebugMessageFormat(0, sdmLoad, 0, "Error Allocating Memory"); - return NULL; - } - - // Read in the header - if( (lpIR->nNumImages = ReadICOHeader(buf)) == (UINT)-1 ) - { - SendDebugMessageFormat(0, sdmLoad, 0, "Error Reading File Header"); - free( lpIR ); - return NULL; - } - // Adjust the size of the struct to account for the images - if( (lpNew = (LPICONRESOURCE) realloc( lpIR, sizeof(ICONRESOURCE) + ((lpIR->nNumImages-1) * sizeof(ICONIMAGE)) )) == NULL ) - { - SendDebugMessageFormat(0, sdmLoad, 0, "Error Allocating Memory"); - CloseHandle( hFile ); - free( lpIR ); - return NULL; - } - lpIR = lpNew; - // Store the original name - - // Allocate enough memory for the icon directory entries - if( (lpIDE = (LPICONDIRENTRY) malloc( lpIR->nNumImages * sizeof( ICONDIRENTRY ) ) ) == NULL ) - { - SendDebugMessageFormat(0, sdmLoad, 0, "Error Allocating Memory"); - free( lpIR ); - return NULL; - } - memcpy(lpIDE, buf + 6, lpIR->nNumImages * sizeof( ICONDIRENTRY )); - - // Loop through and read in each image - for( i = 0; i < lpIR->nNumImages; i++ ) - { - // Allocate memory for the resource - if( (lpIR->IconImages[i].lpBits = (LPBYTE) malloc(lpIDE[i].dwBytesInRes)) == NULL ) - { - SendDebugMessageFormat(0, sdmLoad, 0, "Error Allocating Memory"); - free( lpIR ); - free( lpIDE ); - return NULL; - } - lpIR->IconImages[i].dwNumBytes = lpIDE[i].dwBytesInRes; - // Seek to beginning of this image - memcpy(lpIR->IconImages[i].lpBits, buf + lpIDE[i].dwImageOffset, lpIDE[i].dwBytesInRes); - - // Set the internal pointers appropriately - if( ! AdjustIconImagePointers( &(lpIR->IconImages[i]) ) ) - { - SendDebugMessageFormat(0, sdmLoad, 0, "Error Converting to Internal Format"); - free( lpIDE ); - free( lpIR ); - return NULL; - } - } - // Clean up - free( lpIDE ); - //free( lpRPI ); - return lpIR; -} -/* End ReadIconFromICOFile() **********************************************/ - -void FreeIconResource(LPICONRESOURCE lpIR) -{ - for( UINT i = 0; i < lpIR->nNumImages; i++ ) - // Allocate memory for the resource - free(lpIR->IconImages[i].lpBits); - free( lpIR ); -} - -/**************************************************************************** -* -* FUNCTION: MakeIconFromResource -* -* PURPOSE: Makes an HICON from an icon resource -* -* PARAMS: LPICONIMAGE lpIcon - pointer to the icon resource -* -* RETURNS: HICON - handle to the new icon, NULL for failure -* -* History: -* July '95 - Created -* -\****************************************************************************/ -HICON MakeIconFromResource( LPICONIMAGE lpIcon ) -{ - HICON hIcon = NULL; - - // Sanity Check - if( lpIcon == NULL ) - return NULL; - if( lpIcon->lpBits == NULL ) - return NULL; - // Let the OS do the real work :) - hIcon = CreateIconFromResourceEx( lpIcon->lpBits, lpIcon->dwNumBytes, TRUE, 0x00030000, - (*(LPBITMAPINFOHEADER)(lpIcon->lpBits)).biWidth, (*(LPBITMAPINFOHEADER)(lpIcon->lpBits)).biHeight/2, 0 ); - - // It failed, odds are good we're on NT so try the non-Ex way - if( hIcon == NULL ) - { - // We would break on NT if we try with a 16bpp image - if(lpIcon->lpbi->bmiHeader.biBitCount != 16) - { - hIcon = CreateIconFromResource( lpIcon->lpBits, lpIcon->dwNumBytes, TRUE, 0x00030000 ); - } - } - return hIcon; -} -/* End MakeIconFromResource() **********************************************/ - -PWCHAR StringOffset(PBYTE base, DWORD offset) -{ - if(offset == 0) return NULL; - return (PWCHAR)(base + offset); -} - -#ifdef _WIN64 - -/** - CopyKeyboard will copy the data read into bufp from x86-sized structures into - x64-sized structures starting at `base` - * After this function finishes, we still need to keep the original data because - we don't copy the strings - This method is used on 64-bit architectures. -*/ -LPKEYBOARD CopyKeyboard(PBYTE bufp, PBYTE base, DWORD dwFileSize) -{ - UNREFERENCED_PARAMETER(dwFileSize); - - PCOMP_KEYBOARD ckbp = (PCOMP_KEYBOARD) base; - - /* Copy keyboard structure */ - - LPKEYBOARD kbp = (LPKEYBOARD) bufp; - bufp += sizeof(KEYBOARD); - - kbp->dwIdentifier = ckbp->dwIdentifier; - kbp->dwFileVersion = ckbp->dwFileVersion; - kbp->dwCheckSum = ckbp->dwCheckSum; - kbp->xxkbdlayout = ckbp->KeyboardID; - kbp->IsRegistered = ckbp->IsRegistered; - kbp->version = ckbp->version; - kbp->cxStoreArray = ckbp->cxStoreArray; - kbp->cxGroupArray = ckbp->cxGroupArray; - kbp->StartGroup[0] = ckbp->StartGroup[0]; - kbp->StartGroup[1] = ckbp->StartGroup[1]; - kbp->dwFlags = ckbp->dwFlags; - kbp->dwHotKey = ckbp->dwHotKey; - kbp->hBitmap = 0; // will be built later - - kbp->dpStoreArray = (LPSTORE) bufp; - bufp += sizeof(STORE) * kbp->cxStoreArray; - - kbp->dpGroupArray = (LPGROUP) bufp; - bufp += sizeof(GROUP) * kbp->cxGroupArray; - - PCOMP_STORE csp; - LPSTORE sp; - DWORD i; - - for( - csp = (PCOMP_STORE)(base + ckbp->dpStoreArray), sp = kbp->dpStoreArray, i = 0; - i < kbp->cxStoreArray; - i++, sp++, csp++) - { - sp->dwSystemID = csp->dwSystemID; - sp->dpName = StringOffset(base, csp->dpName); - sp->dpString = StringOffset(base, csp->dpString); - } - - PCOMP_GROUP cgp; - LPGROUP gp; - - for( - cgp = (PCOMP_GROUP)(base + ckbp->dpGroupArray), gp = kbp->dpGroupArray, i = 0; - i < kbp->cxGroupArray; - i++, gp++, cgp++) - { - gp->dpName = StringOffset(base, cgp->dpName); - gp->dpKeyArray = cgp->cxKeyArray > 0 ? (LPKEY) bufp : NULL; - gp->cxKeyArray = cgp->cxKeyArray; - bufp += sizeof(KEY) * gp->cxKeyArray; - gp->dpMatch = StringOffset(base, cgp->dpMatch); - gp->dpNoMatch = StringOffset(base, cgp->dpNoMatch); - gp->fUsingKeys = cgp->fUsingKeys; - - PCOMP_KEY ckp; - LPKEY kp; - DWORD j; - - for( - ckp = (PCOMP_KEY)(base + cgp->dpKeyArray), kp = gp->dpKeyArray, j = 0; - j < gp->cxKeyArray; - j++, kp++, ckp++) - { - kp->Key = ckp->Key; - kp->Line = ckp->Line; - kp->ShiftFlags = ckp->ShiftFlags; - kp->dpOutput = StringOffset(base, ckp->dpOutput); - kp->dpContext = StringOffset(base, ckp->dpContext); - } - } - - return kbp; -} - -#else - -/** - Fixup the keyboard by expanding pointers. On disk the pointers are stored relative to the - beginning of the file, but we need real pointers. This method is used on 32-bit architectures. -*/ -LPKEYBOARD FixupKeyboard(PBYTE bufp, PBYTE base, DWORD dwFileSize) -{ - UNREFERENCED_PARAMETER(dwFileSize); - - DWORD i, j; - PCOMP_KEYBOARD ckbp = (PCOMP_KEYBOARD) base; - PCOMP_GROUP cgp; - PCOMP_STORE csp; - PCOMP_KEY ckp; - LPKEYBOARD kbp = (LPKEYBOARD) bufp; - LPSTORE sp; - LPGROUP gp; - LPKEY kp; - - kbp->dpStoreArray = (LPSTORE) (base + ckbp->dpStoreArray); - kbp->dpGroupArray = (LPGROUP) (base + ckbp->dpGroupArray); - -/*if( ckbp->dwBitmapSize > 0 ) - kbp->hBitmap = LoadBitmapFile((buf + ckbp->dpBitmapOffset), ckbp->dwBitmapSize); - else - kbp->hBitmap = NULL; -*/ - - for(sp = kbp->dpStoreArray, csp = (PCOMP_STORE) sp, i = 0; i < kbp->cxStoreArray; i++, sp++, csp++) - { - sp->dpName = StringOffset(base, csp->dpName); - sp->dpString = StringOffset(base, csp->dpString); - } - - for(gp = kbp->dpGroupArray, cgp = (PCOMP_GROUP) gp, i = 0; i < kbp->cxGroupArray; i++, gp++, cgp++) - { - gp->dpName = StringOffset(base, cgp->dpName); - gp->dpKeyArray = cgp->cxKeyArray > 0 ? (LPKEY) (base + cgp->dpKeyArray) : NULL; - gp->dpMatch = StringOffset(base, cgp->dpMatch); - gp->dpNoMatch = StringOffset(base, cgp->dpNoMatch); - - for(kp = gp->dpKeyArray, ckp = (PCOMP_KEY) kp, j = 0; j < gp->cxKeyArray; j++, kp++, ckp++) - { - kp->dpOutput = StringOffset(base, ckp->dpOutput); - kp->dpContext = StringOffset(base, ckp->dpContext); - } - } - - return kbp; -} - -#endif - -HBITMAP LoadBitmapFileEx(PBYTE filebase) -{ - PCOMP_KEYBOARD ckbp = (PCOMP_KEYBOARD) filebase; - - if( ckbp->dwBitmapSize > 0 ) - return LoadBitmapFile(filebase + ckbp->dpBitmapOffset, ckbp->dwBitmapSize); - else - return NULL; -} - - -BOOL VerifyKeyboard(LPBYTE filebase) -{ - DWORD i; - PCOMP_KEYBOARD ckbp = (PCOMP_KEYBOARD) filebase; - PCOMP_STORE csp; - - /* Check file version */ - - if(ckbp->dwFileVersion < VERSION_MIN || - ckbp->dwFileVersion > VERSION_MAX) - { - for(csp = (PCOMP_STORE)(filebase + ckbp->dpStoreArray), i = 0; i < ckbp->cxStoreArray; i++, csp++) { - if(csp->dwSystemID == TSS_COMPILEDVERSION) - { - char buf2[256]; - if(csp->dpString == 0) - wsprintf(buf2, "errWrongFileVersion:NULL"); - else - wsprintf(buf2, "errWrongFileVersion:%10.10ls", StringOffset(filebase, csp->dpString)); - Err(buf2); - return FALSE; - } - } - Err("errWrongFileVersion"); - return FALSE; - } - - return TRUE; -} diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index b6f591845b5..a7faa7170fe 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -234,14 +234,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKey(WPARAM wParam, LPARAM AITIP::AITIP() { ::AIWin2000Unicode(); // I3574 - FIsDebugControlWindow = FALSE; useLegacy = FALSE; - - WM_KEYMANDEBUG_CANDEBUG = RegisterWindowMessage("WM_KEYMANDEBUG_CANDEBUG"); - WM_KEYMANDEBUG_GETUNICODESTATUS = RegisterWindowMessage("WM_KEYMANDEBUG_GETUNICODESTATUS"); - WM_KEYMANDEBUG_GETCONTEXT = RegisterWindowMessage("WM_KEYMANDEBUG_GETCONTEXT"); - WM_KEYMANDEBUG_ACTION = RegisterWindowMessage("WM_KEYMANDEBUG_ACTION"); - WM_KEYMANDEBUG_RULEMATCH = RegisterWindowMessage("WM_KEYMANDEBUG_RULEMATCH"); } AITIP::~AITIP() { @@ -254,15 +247,6 @@ BOOL AITIP::CanHandleWindow(HWND ahwnd) { return TRUE; // I3574 } -BOOL AITIP::HandleWindow(HWND ahwnd) { - FIsDebugControlWindow = IsDebugControlWindow(ahwnd); - return AIWin2000Unicode::HandleWindow(ahwnd); // I3574 -} - -BOOL AITIP::IsWindowHandled(HWND ahwnd) { - return AIWin2000Unicode::IsWindowHandled(ahwnd); // I3574 -} - BOOL AITIP::IsUnicode() { return TRUE; } @@ -335,11 +319,6 @@ void AITIP::MergeContextWithCache(PWSTR buf, AppContext *local_context) { // I } void AITIP::ReadContext() { - if(DebugControlled()) { - Debug_FillContextBuffer(); - return; - } - WCHAR buf[MAXCONTEXT]; PKEYMAN64THREADDATA _td = ThreadGlobals(); if(!_td) return; @@ -376,44 +355,7 @@ void AITIP::RestoreContextOnly(AppContext *savedContext) { /* Output actions */ -BOOL AITIP::QueueAction(int ItemType, DWORD dwData) { - if(DebugControlled()) { - switch(ItemType) { - case QIT_VKEYDOWN: - if((dwData & QVK_KEYMASK) <= VK__MAX && VKContextReset[(BYTE) dwData]) context->Reset(); // I3438 // I4370 - break; - - case QIT_DEADKEY: - context->Add(UC_SENTINEL); // I4370 - context->Add(CODE_DEADKEY); // I4370 - context->Add((WORD) dwData); // I4370 - break; - - case QIT_CHAR: - context->Add((WORD) dwData); // I4370 - break; - - case QIT_BACK: - if(dwData & BK_BACKSPACE) - while(context->CharIsDeadkey()) context->Delete(); // I4370 - context->Delete(); // I4370 - if(dwData & BK_BACKSPACE) - while(context->CharIsDeadkey()) context->Delete(); // I4370 - break; - } - - SendMessage(GetDebugControlWindow(), WM_KEYMANDEBUG_ACTION, ItemType, dwData); - return TRUE; - } - - return AIWin2000Unicode::QueueAction(ItemType, dwData); // I3575 -} - BOOL AITIP::SendActions() { // I4196 - if(DebugControlled()) { - return TRUE; - } - PKEYMAN64THREADDATA _td = ThreadGlobals(); if(!_td) return FALSE; @@ -504,89 +446,3 @@ DWORD TSFShiftToShift(LPARAM shift) // I3588 if(shift & TF_MOD_RCONTROL) res |= RCTRLFLAG; return res; } - -/* Debug Integration */ - -BOOL AITIP::IsDebugControlWindow(HWND hwnd) -{ - static int WM_KEYMANDEBUG_CANDEBUG = RegisterWindowMessage("WM_KEYMANDEBUG_CANDEBUG"); - DWORD_PTR dwResult; - SendMessageTimeout(hwnd, WM_KEYMANDEBUG_CANDEBUG, 0, 0, SMTO_BLOCK, 50, &dwResult); - return dwResult != 0; -} - -HWND AITIP::GetDebugControlWindow() -{ - if(!FIsDebugControlWindow) return NULL; - return hwnd; -} - -BOOL AITIP::DebugControlled() -{ - return FIsDebugControlWindow; -} - -void AITIP::Debug_FillContextBuffer() -{ - WCHAR buf[MAXCONTEXT]; - if(DebugControlled() && - SendMessage(GetDebugControlWindow(), WM_KEYMANDEBUG_GETCONTEXT, MAXCONTEXT, (LPARAM) buf)) - { - context->Set(buf); // I4370 - SendDebugMessageFormat(0, sdmKeyboard, 0, "AIDebugger::FillContextBuffer(%ls)", buf); - } - else - { - context->Reset(); // I4370 - SendDebugMessageFormat(0, sdmKeyboard, 0, "AIDebugger::FillContextBuffer()-Reset"); - } -} - -#define MAXSTOREOFFSETS 20 - -struct AIDEBUGINFO -{ - int cbSize; - int ItemType; - PWSTR Context, Output; - LPKEY Rule; - LPGROUP Group; - DWORD_PTR Flags; - WORD StoreOffsets[MAXSTOREOFFSETS*2+1]; // pairs--store, char position, terminated by 0xFFFF -}; - -void FillStoreOffsets(AIDEBUGINFO *di) -{ - int i, n; - PWSTR p; - - PKEYMAN64THREADDATA _td = ThreadGlobals(); - if(!_td) return; - - for(i = n = 0, p = di->Rule->dpContext; *p; p = incxstr(p), i++) - { - if(*p == UC_SENTINEL && (*(p+1) == CODE_ANY || *(p+1) == CODE_NOTANY)) - { - di->StoreOffsets[n++] = *(p+2) - 1; - di->StoreOffsets[n++] = _td->IndexStack[i]; - } - if(*p == UC_SENTINEL && *(p+1) == CODE_INDEX) - { - di->StoreOffsets[n++] = *(p+2) - 1; - di->StoreOffsets[n++] = _td->IndexStack[*(p+3) - 1]; - } - if(n == MAXSTOREOFFSETS*2) break; - } - - if(n < MAXSTOREOFFSETS*2 - 1) - for(p = di->Rule->dpOutput; *p; p = incxstr(p)) - { - if(*p == UC_SENTINEL && *(p+1) == CODE_INDEX) - { - di->StoreOffsets[n++] = *(p+2) - 1; - di->StoreOffsets[n++] = _td->IndexStack[*(p+3) - 1]; - } - if(n == MAXSTOREOFFSETS*2) break; - } - di->StoreOffsets[n] = 0xFFFF; -} diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index 3c6fd9b6980..93c9acd4ab5 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -35,30 +35,11 @@ // This can be customised with HKLM\Software\Keyman\Keyman Engine\zap virtual key code #define _VK_PREFIX_DEFAULT 0x0E -struct AIDEBUGKEYINFO -{ - UINT VirtualKey; - DWORD shiftFlags; - WCHAR Character, DeadKeyCharacter; - BOOL IsUp; -}; - class AITIP : public AIWin2000Unicode { private: - int WM_KEYMANDEBUG_CANDEBUG, - WM_KEYMANDEBUG_GETUNICODESTATUS, - WM_KEYMANDEBUG_GETCONTEXT, - WM_KEYMANDEBUG_ACTION, - WM_KEYMANDEBUG_RULEMATCH; - - BOOL FIsDebugControlWindow; - HWND GetDebugControlWindow(); - void Debug_FillContextBuffer(); void MergeContextWithCache(PWSTR buf, AppContext *context); // I4262 - static BOOL IsDebugControlWindow(HWND hwnd); - private: BOOL useLegacy; @@ -69,8 +50,6 @@ class AITIP : public AIWin2000Unicode AITIP(); ~AITIP(); - BOOL DebugControlled(); - /** * Copy the member context * @@ -85,13 +64,9 @@ class AITIP : public AIWin2000Unicode */ void RestoreContextOnly(AppContext *savedContext); - virtual BOOL QueueAction(int ItemType, DWORD dwData); - /* Information functions */ virtual BOOL CanHandleWindow(HWND ahwnd); - virtual BOOL IsWindowHandled(HWND ahwnd); - virtual BOOL HandleWindow(HWND ahwnd); virtual BOOL IsUnicode(); /* Context functions */ diff --git a/windows/src/engine/keyman32/globals.h b/windows/src/engine/keyman32/globals.h index 320ef811de4..8f1dfc4c90d 100644 --- a/windows/src/engine/keyman32/globals.h +++ b/windows/src/engine/keyman32/globals.h @@ -54,10 +54,6 @@ #include -#define GLOBAL_ContextStackSize 80 -#define GLOBAL_MsgStackSize 80 -#define GLOBAL_MaxKeyboards 32 - #include "serialkeyeventclient.h" #include "SharedBuffers.h" @@ -66,8 +62,6 @@ class Globals { public: - //static INI *Ini(); - static HHOOK *hhookGetMessage(); static HHOOK *hhookCallWndProc(); @@ -196,16 +190,9 @@ typedef struct tagKEYMAN64THREADDATA LPINTKEYBOARDINFO lpKeyboards; // keyboard definitions LPINTKEYBOARDINFO lpActiveKeyboard; - // I3616 - LPMSG msgbuf; // Message buffer (alloc at runtime) - int nKeyboards; // nLoadedKeyboards int nLanguages; // I1087 //TODO UNUSED - LPWORD IndexStack; - LPWSTR miniContext; - int miniContextIfLen; - KMSTATE state; AITIP *app; @@ -263,12 +250,10 @@ typedef struct tagKEYMAN64THREADDATA extern UINT wm_keyman, // user message - ignore msg - wm_kmdebug, // " " " " - debugging wm_keyman_control, // messages to main Keyman window - replaces WM_USER+* wm_keyman_control_internal, // messages to all windows to notify of changes to Keyman // I4412 wm_keymankeydown, wm_keymankeyup, - wm_keymandebug, wm_keyman_grabwindowproc, wm_keyman_refresh, wm_kmgetactivekeymanid, diff --git a/windows/src/engine/keyman32/k32_globals.cpp b/windows/src/engine/keyman32/k32_globals.cpp index 989a787f959..fb827353771 100644 --- a/windows/src/engine/keyman32/k32_globals.cpp +++ b/windows/src/engine/keyman32/k32_globals.cpp @@ -72,7 +72,6 @@ UINT //TODO: consolidate these messages -- they are probably not all required now wm_keyman = 0, // user message - ignore msg // I3594 - wm_kmdebug = 0, // " " " " - debugging wm_keymankeydown = 0, wm_keymankeyup = 0, @@ -218,9 +217,6 @@ BOOL Globals_ProcessInitialised() #pragma data_seg(".SHARDATA") -//static INI // I3158 // I3524 -// f_Ini = {0}; // KEYMAN.INI options - static HHOOK f_hhookGetMessage = NULL, // GETMESSAGE hook handle f_hhookCallWndProc = NULL; // CALLWNDPROC hook handle @@ -296,8 +292,6 @@ DWORD //static HANDLE f_hLockMutex = 0; -//INI *Globals::Ini() { return &f_Ini; } // I3158 // I3524 - HHOOK *Globals::hhookGetMessage() { return &f_hhookGetMessage; } HHOOK *Globals::hhookCallWndProc() { return &f_hhookCallWndProc; } @@ -428,11 +422,6 @@ BOOL Globals::ResetControllers() // I3092 f_MasterController = NULL; f_MaxControllerThreads = 0; - /* I3158 // I3524 - f_Ini.ContextStackSize = 0; - f_Ini.MaxKeyboards = 0; - f_Ini.MsgStackSize = 0; - */ f_hhookCallWndProc = NULL; f_hhookGetMessage = NULL; #ifndef _WIN64 diff --git a/windows/src/engine/keyman32/keyman32.cpp b/windows/src/engine/keyman32/keyman32.cpp index 5dfdaab0793..9dbd692525d 100644 --- a/windows/src/engine/keyman32/keyman32.cpp +++ b/windows/src/engine/keyman32/keyman32.cpp @@ -157,27 +157,9 @@ void UninitDebuggingEx(); BOOL UninitialiseProcess(BOOL Lock) { if(!Globals_ProcessInitialised()) return TRUE; - - ReleaseKeyboards(Lock); - - UninitDebuggingEx(); - - PKEYMAN64THREADDATA _td = ThreadGlobals(); // This is safe because of Globals_ProcessInitialised call above - if(_td) - { - if(_td->msgbuf) delete _td->msgbuf; - _td->msgbuf = NULL; - - if(_td->IndexStack) delete _td->IndexStack; - _td->IndexStack = NULL; - - if (_td->miniContext) delete _td->miniContext; - _td->miniContext = NULL; - - if (_td->msgbuf) delete _td->msgbuf; - _td->msgbuf = NULL; - } - return TRUE; + ReleaseKeyboards(Lock); + UninitDebuggingEx(); + return TRUE; } LONG FStartedInitialise = FALSE; @@ -239,34 +221,8 @@ BOOL InitThread(HWND hwnd) // I2448 - Removed "|| _td->FInitialised" -- this should not be included because sometimes it can be re-initialised. } - // TODO: Check if we are initialising again and don't reallocate memory. This can happen if Keyman is restarted - _td->FInitialising = TRUE; // Control re-entrancy, this is thread safe because the variable is per-thread - _td->IndexStack = new WORD[GLOBAL_ContextStackSize]; //Globals::Ini()->ContextStackSize]; // I3158 // I3524 - if(!_td->IndexStack) - { - SendDebugMessage(hwnd, sdmGlobal, 0, "InitialiseProcess: Failed to allocate memory for IndexStack"); - SetLastError(ERROR_KEYMAN_MEMORY_ALLOCATION_FAILED); // I3143 // I3523 - return FALSE; - } - - _td->miniContext = new WCHAR[GLOBAL_ContextStackSize]; //Globals::Ini()->ContextStackSize]; // I3158 // I3524 - if(!_td->miniContext) - { - SendDebugMessage(hwnd, sdmGlobal, 0, "InitialiseProcess: Failed to allocate memory for miniContext"); - SetLastError(ERROR_KEYMAN_MEMORY_ALLOCATION_FAILED); // I3143 // I3523 - return FALSE; - } - - _td->msgbuf = new MSG[GLOBAL_MsgStackSize]; //Globals::Ini()->MsgStackSize]; // I3158 // I3524 - if(!_td->msgbuf) - { - SendDebugMessage(hwnd, sdmGlobal, 0, "InitialiseProcess: Failed to allocate memory for msgbuf"); - SetLastError(ERROR_KEYMAN_MEMORY_ALLOCATION_FAILED); // I3143 // I3523 - return FALSE; - } - RefreshKeyboards(TRUE); SendDebugMessageFormat(hwnd, sdmGlobal, 0, "--InitialiseProcess LEAVE--"); @@ -316,10 +272,6 @@ BOOL InitialiseProcess(HWND hwnd) DoChangeWindowMessageFilter(); - //GetINIAdvanced(); /* I163x - Fix crash due to memory corruption - Globals::Ini not initialized before use - only appeared to happen on x64? */ // I3158 // I3524 - - SendDebugMessageFormat(hwnd, sdmGlobal, 0, "ContextStackSize: %d", GLOBAL_ContextStackSize); // Globals::Ini()->ContextStackSize); // I3158 // I3524 - return InitThread(hwnd); } @@ -461,13 +413,6 @@ extern "C" BOOL _declspec(dllexport) WINAPI Keyman_Initialise(HWND Handle, BOOL return TRUE; } -/*void GetINIAdvanced(void) // I3158 // I3524 -{ - Globals::Ini()->ContextStackSize = 80; - Globals::Ini()->MsgStackSize = 80; - Globals::Ini()->MaxKeyboards = 32; -}*/ - extern "C" BOOL _declspec(dllexport) WINAPI Keyman_StartExit(void) // I3092 { *Globals::Keyman_Shutdown() = TRUE; diff --git a/windows/src/engine/keyman32/keymanengine.h b/windows/src/engine/keyman32/keymanengine.h index c563210a765..3c6e2d3affc 100644 --- a/windows/src/engine/keyman32/keymanengine.h +++ b/windows/src/engine/keyman32/keymanengine.h @@ -92,13 +92,6 @@ typedef struct tagINTKEYBOARDINFO km_core_keyboard_imx* lpIMXList; } INTKEYBOARDINFO, * LPINTKEYBOARDINFO; -typedef struct tagINI -{ - int MsgStackSize; - int MaxKeyboards; - int ContextStackSize; -} INI; - typedef struct tagKMSTATE { BOOL NoMatches; @@ -124,14 +117,7 @@ LRESULT CALLBACK kmnLowLevelKeyboardProc( // I4124 BOOL ReleaseStateMemoryCore(km_core_state** state); BOOL ReleaseKeyboardMemoryCore(km_core_keyboard** kbd); -void PostGETNEXT(HWND hwnd); -BOOL CompareMsg(LPMSG MsgA, LPMSG MsgB); BOOL ProcessHook(); // returns FALSE on error or key not matched [only for AITip] -BOOL ProcessMessage( LPMSG mp ); -BOOL ProcessGroup(LPGROUP gp); -BOOL ContextMatch(LPKEY kkp); -int PostString(PWSTR str, LPMSG mp, LPKEYBOARD lpkb, PWSTR endstr); -BOOL LoadAllKeymanKeyboards(DWORD layout); BOOL IsSysTrayWindow(HWND hwnd); @@ -149,7 +135,6 @@ BOOL SelectKeyboard(DWORD KeymanID); extern "C" DWORD _declspec(dllexport) WINAPI GetActiveKeymanID(); BOOL GetKeyboardFileName(LPSTR kbname, LPSTR buf, int nbuf); -BOOL LoadKeyboard(LPSTR fileName, LPKEYBOARD *lpKeyboard); BOOL LoadlpKeyboard(int i); PSTR wstrtostr(PCWSTR in); diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index e129a0b8da3..d4d1177ced3 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -266,11 +266,6 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) SendDebugMessageFormat(0, sdmInternat, 0, "GetMessage: wm_keymanshift %x %x", mp->wParam, mp->lParam); SelectApplicationIntegration(); if(!_td->app->IsWindowHandled(mp->hwnd)) _td->app->HandleWindow(mp->hwnd); - if(_td->app->DebugControlled()) - { - if(mp->wParam == 1) *Globals::ShiftState() = (DWORD) mp->lParam; - else *Globals::ShiftState() = 0; - } return CallNextHookEx(Globals::get_hhookGetMessage(), nCode, wParam, lParam); } diff --git a/windows/src/engine/keyman32/kmprocess.cpp b/windows/src/engine/keyman32/kmprocess.cpp index 5772836de2a..7e5e8070567 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -138,16 +138,7 @@ BOOL ProcessHook() if(!_td) return FALSE; fOutputKeystroke = FALSE; // TODO: 5442 no longer needs to be global once we use core processor - // - // If we are running in the debugger, don't do a second run through - // - if(_td->app->DebugControlled() && !_td->TIPFUpdateable) { // I4287 - if(_td->state.vkey == VK_ESCAPE || (_td->state.vkey >= VK_PRIOR && _td->state.vkey <= VK_DOWN) || (_td->state.vkey == VK_DELETE)) return FALSE; // I4033 // I4826 // I4845 - else return TRUE; - } - - //app->NoSetShift = FALSE; _td->app->ReadContext(); if(_td->state.msg.message == wm_keymankeydown) { // I4827 @@ -216,17 +207,6 @@ BOOL ProcessHook() } } - if (fOutputKeystroke && _td->app->DebugControlled()) { - // The debug memo does not receive default key events because - // we capture them all here. So we synthesize the key event for - // the debugger. - _td->app->QueueAction(QIT_VSHIFTDOWN, Globals::get_ShiftState()); - _td->app->QueueAction(QIT_VKEYDOWN, _td->state.vkey); - _td->app->QueueAction(QIT_VKEYUP, _td->state.vkey); - _td->app->QueueAction(QIT_VSHIFTUP, Globals::get_ShiftState()); - fOutputKeystroke = FALSE; - } - if(*Globals::hwndIM() == 0 || *Globals::hwndIMAlways()) { _td->app->SetCurrentShiftState(Globals::get_ShiftState());