diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 77ab2fa736..766e2cd417 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -32,7 +32,7 @@ android { defaultConfig { applicationId "com.tavultesoft.kmapro" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 35 //println "===DUMPING PROPERTIES===" @@ -184,6 +184,7 @@ dependencies { implementation ('com.github.kenglxn.QRGen:android:3.0.1') { transitive = true } + implementation 'com.google.android.gms:play-services-maps3d:0.2.0' } /*def void dumpProperties(it){ 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 deleted file mode 100644 index 16330bfcda..0000000000 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ /dev/null @@ -1,352 +0,0 @@ -/** - * Copyright (C) 2017 SIL International. All rights reserved. - */ - -package com.keyman.android; - -import com.keyman.engine.util.DownloadFileUtils; -import com.tavultesoft.kmapro.BuildConfig; -import com.tavultesoft.kmapro.DefaultLanguageResource; -import com.tavultesoft.kmapro.KeymanSettingsActivity; -import com.tavultesoft.kmapro.PreferencesManager; -import com.keyman.engine.KMManager; -import com.keyman.engine.KMManager.KeyboardType; -import com.keyman.engine.KMHardwareKeyboardInterpreter; -import com.keyman.engine.KMManager.SuggestionType; -import com.keyman.engine.KeyboardEventHandler.OnKeyboardEventListener; -import com.keyman.engine.R; -import com.keyman.engine.data.Keyboard; -import com.keyman.engine.util.DependencyUtil; -import com.keyman.engine.util.DependencyUtil.LibraryType; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.graphics.Point; -import android.inputmethodservice.InputMethodService; -import android.text.InputType; -import android.util.Log; -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.widget.FrameLayout; -import android.content.Intent; - -import io.sentry.android.core.SentryAndroid; -import io.sentry.Sentry; - -public class SystemKeyboard extends InputMethodService implements OnKeyboardEventListener { - - private static View inputView = null; - private KMHardwareKeyboardInterpreter interpreter = null; - private int inputType = InputType.TYPE_NULL; - private int lastOrientation = Configuration.ORIENTATION_UNDEFINED; - - private static final String TAG = "SystemKeyboard"; - - /** - * Main initialization of the input method component. Be sure to call - * to super class. - */ - @Override - public void onCreate() { - super.onCreate(); - - if (DependencyUtil.libraryExists(LibraryType.SENTRY) && !Sentry.isEnabled()) { - Log.d(TAG, "Initializing Sentry"); - SentryAndroid.init(getApplicationContext(), options -> { - options.setEnableAutoSessionTracking(false); - options.setRelease(com.tavultesoft.kmapro.BuildConfig.KEYMAN_VERSION_GIT_TAG); - options.setEnvironment(com.tavultesoft.kmapro.BuildConfig.KEYMAN_VERSION_ENVIRONMENT); - }); - } - if (BuildConfig.DEBUG) { - KMManager.setDebugMode(true); - } - KMManager.addKeyboardEventListener(this); - Context context = getApplicationContext(); - KMManager.initialize(context, KeyboardType.KEYBOARD_TYPE_SYSTEM); - DefaultLanguageResource.install(context); - interpreter = new KMHardwareKeyboardInterpreter(context, KeyboardType.KEYBOARD_TYPE_SYSTEM); - KMManager.setInputMethodService(this); // for HW interface - - SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - 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); - - // Checking for updates should never be allowed to crash the keyboard. - // Just silently fail if this occurs. - if(DownloadFileUtils.getDownloadManager(this) != null) { - // Will try to emit a toast if it fails - i.e., is not silent. - KMManager.executeResourceUpdate(this); - } - } - - @Override - public void onDestroy() { - inputView = null; - KMManager.removeKeyboardEventListener(this); - interpreter = null; // Throw it away, since we're losing our application's context. - KMManager.onDestroy(); - super.onDestroy(); - } - - /** - * This is the point where you can do all of your UI initialization. It - * is called after creation and any configuration change. - */ - @Override - public void onInitializeInterface() { - super.onInitializeInterface(); - } - - /** - * Called by the framework when your view for creating input needs to - * be generated. This will be called the first time your input method - * is displayed, and every time it needs to be re-created such as due to - * a configuration change. - */ - @Override - public View onCreateInputView() { - if (inputView == null) { - inputView = KMManager.createInputView(this); - } - - ViewGroup parent = (ViewGroup) inputView.getParent(); - if (parent != null) { - parent.removeView(inputView); - } - - return inputView; - } - - /** - * Deal with the editor reporting movement of its cursor. - */ - @Override - public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) { - super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd); - KMManager.updateSelectionRange(KMManager.KeyboardType.KEYBOARD_TYPE_SYSTEM); - } - - private void sendCurrentFontName() { - Keyboard keyboardInfo = KMManager.getCurrentKeyboardInfo(this); - if (keyboardInfo != null) { - String fontName = keyboardInfo.getFont(); - - Intent intent; - if (BuildConfig.DEBUG) { - intent = new Intent("com.tavultesoft.kmapro.debug.keyboard_changed"); - } else { - intent = new Intent("com.tavultesoft.kmapro.keyboard_changed"); - } - intent.putExtra("fontName", fontName); - sendBroadcast(intent); - } - } - - /** - * This is the main point where we do our initialization of the input method - * to begin operating on an application. At this point we have been - * bound to the client, and are now receiving all of the detailed information - * about the target of our edits. - */ - @Override - public void onStartInput(EditorInfo attribute, boolean restarting) { - attribute.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN; - super.onStartInput(attribute, restarting); - KMManager.onStartInput(attribute, restarting); - KMManager.resetContext(KeyboardType.KEYBOARD_TYPE_SYSTEM); - - Context appContext = getApplicationContext(); - // Temporarily disable predictions on certain fields (e.g. hidden password field or numeric) - inputType = attribute.inputType; - KMManager.setPredictionsSuspended(inputType, KeyboardType.KEYBOARD_TYPE_SYSTEM); - if (KMManager.getPredictionsSuspended(KeyboardType.KEYBOARD_TYPE_SYSTEM)) { - KMManager.setBannerOptions(false); - // Set the system keyboard HTML banner - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); - } else if (KMManager.isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)){ - // Check if predictions needs to be re-enabled per Settings preference - Keyboard kbInfo = KMManager.getCurrentKeyboardInfo(appContext); - if (kbInfo != null) { - String langId = kbInfo.getLanguageID(); - SharedPreferences prefs = appContext.getSharedPreferences(appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - int maySuggest = prefs.getInt(KMManager.getLanguageAutoCorrectionPreferenceKey(langId), KMManager.KMDefault_Suggestion); - // Enable banner if maySuggest is not SuggestionType.SUGGESTIONS_DISABLED (0) - KMManager.setBannerOptions(maySuggest != SuggestionType.SUGGESTIONS_DISABLED.toInt()); - } else { - KMManager.setBannerOptions(false); - } - } - - // Determine special handling for ENTER key - KMManager.setEnterMode(attribute.imeOptions, inputType); - - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - ExtractedText icText = ic.getExtractedText(new ExtractedTextRequest(), 0); - /* - We do sometimes receive null `icText.text`, even though - getExtractedText() docs does not list this as a possible - return value, so we test for that as well (#11479) - */ - if (icText != null && icText.text != null) { - // Update the text selection but ignore the returned statuses - KMManager.updateText(KeyboardType.KEYBOARD_TYPE_SYSTEM, icText.text.toString()); - KMManager.updateSelectionRange(KeyboardType.KEYBOARD_TYPE_SYSTEM); - } - } - - // Select numeric layer if applicable - if (KMManager.isNumericField(inputType)) { - KMManager.setNumericLayer(KeyboardType.KEYBOARD_TYPE_SYSTEM); - } - - if (KMManager.isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)) { - sendCurrentFontName(); - } - } - - @Override - public void onStartInputView(EditorInfo attribute, boolean restarting) { - super.onStartInputView(attribute, restarting); - setInputView(onCreateInputView()); - - // Update the input type - inputType = attribute.inputType; - } - - @Override - public void onUpdateExtractingVisibility(EditorInfo ei) { - super.onUpdateExtractingVisibility(ei); - } - - @Override - public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) { - super.onConfigureWindow(win, isFullscreen, isCandidatesOnly); - - // We don't currently use isFullscreen or isCandidatesOnly; we always want to MATCH_PARENT, - // unlike the default for height which is WRAP_CONTENT. We then adjust the touchable area - // in `onCalculateInsets` - win.setLayout(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); - } - - @Override - public void onComputeInsets(InputMethodService.Insets outInsets) { - super.onComputeInsets(outInsets); - - // This method (likely) includes the IME equivalent to `onResume` for `Activity`-based classes, - // making it an important time to detect orientation changes. - Context appContext = getApplicationContext(); - int newOrientation = KMManager.getOrientation(appContext); - if(newOrientation != lastOrientation) { - lastOrientation = newOrientation; - Configuration newConfig = this.getResources().getConfiguration(); - KMManager.onConfigurationChanged(newConfig); - } - - // Update the touchable region of the Keyman keyboard - Point size = KMManager.getWindowSize(getApplicationContext()); - - int inputViewHeight = 0; - if (inputView != null) { - inputViewHeight = inputView.getHeight(); - } - - int navigationHeight = KMManager.getNavigationBarHeight(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); - int bannerHeight = KMManager.getBannerHeight(this); - int kbHeight = KMManager.getKeyboardHeight(this); - outInsets.contentTopInsets = inputViewHeight - bannerHeight - kbHeight - navigationHeight; - outInsets.visibleTopInsets = outInsets.contentTopInsets; - outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION; - outInsets.touchableRegion.set(0, outInsets.contentTopInsets, size.x, size.y); - } - - @Override - public void onKeyboardLoaded(KeyboardType keyboardType) { - // Do nothing - } - - @Override - public void onKeyboardChanged(String newKeyboard) { - // Refresh banner theme - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); - KMManager.showSystemKeyboard(); - sendCurrentFontName(); - } - - @Override - public void onKeyboardShown() { - // Refresh banner theme - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); - } - - @Override - public void onKeyboardDismissed() { - // Do nothing - } - - @Override - public boolean onEvaluateInputViewShown() { - // On Android API 36+, the OSK defaults to not appearing when a physical keyboard is connected. - // If the default implementation returns true, recommend honoring it - // Reference: https://android.googlesource.com/platform/frameworks/base/+/7b739a8%5E%21/ - if (super.onEvaluateInputViewShown()) { - return true; - }; - - Context context = getApplicationContext(); - SharedPreferences prefs = context.getSharedPreferences(PreferencesManager.kma_prefs_name, Context.MODE_PRIVATE); - boolean showOSK = prefs.getBoolean(KeymanSettingsActivity.oskWithPhysicalKeyboardKey, false); - return showOSK; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - // Determine if Physical keystroke should be passed off - if (inputType == InputType.TYPE_NULL) { - return false; // Revert to default handling - } - - if (event.getAction() == KeyEvent.ACTION_DOWN) { - switch (keyCode) { - case KeyEvent.KEYCODE_BACK: - // Dismiss the keyboard if currently shown - if (isInputViewShown()) { - KMManager.hideSystemKeyboard(); - return true; - } - break; - } - } - - return interpreter.onKeyDown(keyCode, event); // if false, will revert to default handling. - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - return interpreter.onKeyUp(keyCode, event); - } - - @Override - public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { - return interpreter.onKeyMultiple(keyCode, count, event); - } - - @Override - public boolean onKeyLongPress(int keyCode, KeyEvent event) { - return interpreter.onKeyLongPress(keyCode, event); - } -} \ No newline at end of file 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 8b0ea603b1..726e6db829 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 @@ -25,6 +25,7 @@ import com.keyman.engine.KMHelpFileActivity; import com.keyman.engine.KMKeyboardDownloaderActivity; import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; +import com.keyman.engine.KeyboardEventHandler; import com.keyman.engine.KmpInstallMode; import com.keyman.engine.KMTextView; import com.keyman.engine.KeyboardEventHandler.OnKeyboardDownloadEventListener; @@ -59,7 +60,13 @@ import android.os.ParcelFileDescriptor; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; +import androidx.appcompat.widget.SwitchCompat; import androidx.appcompat.app.AlertDialog; import androidx.constraintlayout.widget.ConstraintLayout; import android.content.ClipData; @@ -80,6 +87,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; +import androidx.drawerlayout.widget.DrawerLayout; import androidx.drawerlayout.widget.DrawerLayout; @@ -94,18 +102,33 @@ import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.WindowManager; import android.widget.Button; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; +import android.view.Gravity; +import android.view.inputmethod.InputMethodManager; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.appcompat.app.ActionBarDrawerToggle; +import com.google.android.material.navigation.NavigationView; +import androidx.core.view.GravityCompat; +import android.view.MenuItem; +import androidx.appcompat.widget.AppCompatCheckBox; + import io.sentry.android.core.SentryAndroid; public class MainActivity extends BaseActivity implements OnKeyboardEventListener, OnKeyboardDownloadEventListener, ActivityCompat.OnRequestPermissionsResultCallback { public static Context context; + private DrawerLayout drawerLayout; + private ActionBarDrawerToggle drawerToggle; + private NavigationView navigationView; + // Fields used for installing kmp packages public static final int PERMISSION_REQUEST_STORAGE = 0; public static final int READ_REQUEST_CODE = 42; @@ -175,6 +198,15 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene getSupportActionBar().setDisplayUseLogoEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); + getSupportActionBar().setDisplayShowTitleEnabled(true); +// ImageView logo = new ImageView(this); +// logo.setImageResource(R.drawable.keyman_logo); +// Toolbar.LayoutParams params = new Toolbar.LayoutParams( +// Toolbar.LayoutParams.WRAP_CONTENT, +// Toolbar.LayoutParams.WRAP_CONTENT, +// Gravity.CENTER +// ); +// toolbar.addView(logo, params); getSupportActionBar().setLogo(R.drawable.keyman_logo); getSupportActionBar().setDisplayUseLogoEnabled(false); @@ -188,6 +220,82 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene getSupportActionBar().setDisplayShowTitleEnabled(false); getSupportActionBar().setBackgroundDrawable(getActionBarDrawable(this)); + drawerLayout = findViewById(R.id.drawer_layout); + drawerLayout.setScrimColor(Color.parseColor("#80000000")); + // Initialize drawerToggle WITHOUT toolbar to prevent it from showing up at the start + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open , R.string.drawer_close); + drawerLayout.addDrawerListener(drawerToggle); + drawerToggle.syncState(); + + navigationView = findViewById(R.id.nav_view); + navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + int id = item.getItemId(); + if (id == R.id.nav_toggle_show_osk || id == R.id.nav_toggle_haptic_feedback || id == R.id.nav_toggle_send_crash_report) { + toggleDrawerSwitch(navigationView, id); + } else if (id == R.id.nav_checkbox_enable_system_keyboard) { + drawerLayout.closeDrawers(); + startActivity(new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)); + } else if (id == R.id.nav_checkbox_set_default_keyboard) { + drawerLayout.closeDrawers(); + InputMethodManager imManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imManager != null) { + imManager.showInputMethodPicker(); + } + } else if (id == R.id.nav_installed_languages) { + drawerLayout.closeDrawers(); + Intent intent = new Intent(context, LanguagesSettingsActivity.class); + intent.putExtra(KMManager.KMKey_DisplayKeyboardSwitcher, false); + startActivity(intent); + } else if (id == R.id.nav_install_keyboard) { + drawerLayout.closeDrawers(); + startActivity(new Intent(context, KeymanSettingsInstallActivity.class)); + } else if (id == R.id.nav_display_language) { + drawerLayout.closeDrawers(); + startActivity(new Intent(context, KeymanSettingsLocalizeActivity.class)); + } else if (id == R.id.nav_keyboard_height) { + drawerLayout.closeDrawers(); + startActivity(new Intent(context, AdjustKeyboardHeightActivity.class)); + } else if (id == R.id.nav_longpress_delay) { + drawerLayout.closeDrawers(); + startActivity(new Intent(context, AdjustLongpressDelayActivity.class)); + } else if (id == R.id.nav_settings) { + drawerLayout.closeDrawers(); + startActivity(new Intent(context, KeymanSettingsActivity.class)); + } else if (id == R.id.nav_about) { + drawerLayout.closeDrawers(); + startActivity(new Intent(context, InfoActivity.class)); + } + return true; + } + }); + initializeDrawerToggleOptions(navigationView); + initializeDrawerCheckboxOptions(navigationView); + refreshDrawerSystemKeyboardCheckboxes(navigationView); + + drawerLayout.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + KMManager.hideSystemKeyboard(); + textView.dismissKeyboard(); + } + @Override + public void onDrawerOpened(View drawerView) { + + } + + @Override + public void onDrawerClosed(View drawerView) { + textView.callOnClick(); + } + }); + + drawerToggle.getDrawerArrowDrawable().setColor(Color.BLACK); + drawerToggle.getDrawerArrowDrawable().setBarThickness(10f); + drawerToggle.getDrawerArrowDrawable().setGapSize(10f); + textView = (KMTextView) findViewById(R.id.kmTextView); textView.setText(prefs.getString(userTextKey, "")); textSize = prefs.getInt(userTextSizeKey, minTextSize); @@ -254,6 +362,10 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene protected void onResume() { super.onResume(); + if (navigationView != null) { + refreshDrawerSystemKeyboardCheckboxes(navigationView); + } + if (textView != null) { // Reset inAppPredictionsSuspendedForSensitiveInput flag KMManager.setPredictionsSuspended(textView.getInputType(), KeyboardType.KEYBOARD_TYPE_INAPP); @@ -481,6 +593,14 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene _keyboardupdate.setVisible(false); } + return true; + } else if (item.getItemId() == R.id.action_overflow) { + // Open the drawer from the end when the hamburger (overflow) icon is clicked + if (drawerLayout.isDrawerOpen(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } else { + drawerLayout.openDrawer(GravityCompat.END); + } return true; } else { return super.onOptionsItemSelected(item); @@ -491,7 +611,11 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene public boolean onKeyUp(int keycode, KeyEvent e) { switch (keycode) { case KeyEvent.KEYCODE_MENU: - menu.performIdentifierAction(R.id.action_overflow, Menu.FLAG_PERFORM_NO_CLOSE); + if (drawerLayout.isDrawerOpen(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } else { + drawerLayout.openDrawer(GravityCompat.END); + } return true; } @@ -980,6 +1104,180 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene startActivity(settingsIntent); } + private void initializeDrawerToggleOptions(NavigationView navigationView) { + SharedPreferences prefs = getSharedPreferences(getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + + bindDrawerSwitch(navigationView, R.id.nav_toggle_show_osk, + prefs.getBoolean(KeymanSettingsActivity.oskWithPhysicalKeyboardKey, false), + new SwitchChangeHandler() { + @Override + public void onChanged(boolean isChecked) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(KeymanSettingsActivity.oskWithPhysicalKeyboardKey, isChecked); + editor.apply(); + } + }); + + bindDrawerSwitch(navigationView, R.id.nav_toggle_haptic_feedback, + prefs.getBoolean(KeymanSettingsActivity.hapticFeedbackKey, false), + new SwitchChangeHandler() { + @Override + public void onChanged(boolean isChecked) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(KeymanSettingsActivity.hapticFeedbackKey, isChecked); + editor.apply(); + KMManager.setHapticFeedback(isChecked); + } + }); + + bindDrawerSwitch(navigationView, R.id.nav_toggle_send_crash_report, + prefs.getBoolean(KeymanSettingsActivity.sendCrashReport, true), + new SwitchChangeHandler() { + @Override + public void onChanged(boolean isChecked) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(KeymanSettingsActivity.sendCrashReport, isChecked); + editor.apply(); + KMManager.setMaySendCrashReport(isChecked); + } + }); + } + + private void initializeDrawerCheckboxOptions(NavigationView navigationView) { + bindDrawerCheckboxAction(navigationView, R.id.nav_checkbox_enable_system_keyboard, + new Runnable() { + @Override + public void run() { + startActivity(new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)); + } + }); + + bindDrawerCheckboxAction(navigationView, R.id.nav_checkbox_set_default_keyboard, + new Runnable() { + @Override + public void run() { + InputMethodManager imManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imManager != null) { + imManager.showInputMethodPicker(); + } + } + }); + } + + private void refreshDrawerSystemKeyboardCheckboxes(NavigationView navigationView) { + setDrawerCheckboxState(navigationView, R.id.nav_checkbox_enable_system_keyboard, + SystemIMESettings.isEnabledAsSystemKB(context)); + setDrawerCheckboxState(navigationView, R.id.nav_checkbox_set_default_keyboard, + SystemIMESettings.isDefaultKB(context)); + } + + private void bindDrawerSwitch(NavigationView navigationView, int menuItemId, boolean defaultValue, + final SwitchChangeHandler switchChangeHandler) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null) { + return; + } + + menuItem.setCheckable(false); + + View actionView = menuItem.getActionView(); + if (actionView == null) { + return; + } + + final SwitchCompat switchView; + if (actionView instanceof SwitchCompat) { + switchView = (SwitchCompat) actionView; + } else { + switchView = actionView.findViewById(R.id.nav_switch); + } + + if (switchView == null) { + return; + } + + switchView.setChecked(defaultValue); + switchView.setOnCheckedChangeListener((buttonView, isChecked) -> switchChangeHandler.onChanged(isChecked)); + actionView.setOnClickListener(v -> switchView.toggle()); + } + + private void toggleDrawerSwitch(NavigationView navigationView, int menuItemId) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null) { + return; + } + + View actionView = menuItem.getActionView(); + if (actionView == null) { + return; + } + + if (actionView instanceof SwitchCompat) { + ((SwitchCompat) actionView).toggle(); + return; + } + + SwitchCompat switchView = actionView.findViewById(R.id.nav_switch); + if (switchView != null) { + switchView.toggle(); + } + } + + private void bindDrawerCheckboxAction(NavigationView navigationView, int menuItemId, final Runnable onActivate) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null) { + return; + } + + menuItem.setCheckable(false); + + View actionView = menuItem.getActionView(); + if (actionView == null) { + return; + } + + final AppCompatCheckBox checkBox; + if (actionView instanceof AppCompatCheckBox) { + checkBox = (AppCompatCheckBox) actionView; + } else { + checkBox = actionView.findViewById(R.id.nav_checkbox); + } + + if (checkBox == null) { + return; + } + + checkBox.setOnClickListener(v -> onActivate.run()); + actionView.setOnClickListener(v -> onActivate.run()); + } + + private void setDrawerCheckboxState(NavigationView navigationView, int menuItemId, boolean isChecked) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null) { + return; + } + + View actionView = menuItem.getActionView(); + if (actionView == null) { + return; + } + + AppCompatCheckBox checkBox; + if (actionView instanceof AppCompatCheckBox) { + checkBox = (AppCompatCheckBox) actionView; + } else { + checkBox = actionView.findViewById(R.id.nav_checkbox); + } + + if (checkBox != null) { + checkBox.setChecked(isChecked); + } + } + + private interface SwitchChangeHandler { + void onChanged(boolean isChecked); + } + /** * Dismiss the download progress dialog */ diff --git a/android/KMAPro/kMAPro/src/main/res/layout/activity_main.xml b/android/KMAPro/kMAPro/src/main/res/layout/activity_main.xml index ffcd4a2421..e31fa00e0c 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/activity_main.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/activity_main.xml @@ -1,42 +1,62 @@ - + android:fitsSystemWindows="true"> - - - + android:layout_height="match_parent" + android:layout_above="@+id/KMKeyboard" + android:orientation="vertical" + android:background="@android:color/white" + tools:context=".MainActivity" + android:fitsSystemWindows="true"> - + - - + - + + + + + + + + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/layout/nav_checkbox_item.xml b/android/KMAPro/kMAPro/src/main/res/layout/nav_checkbox_item.xml new file mode 100644 index 0000000000..6988d63664 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/layout/nav_checkbox_item.xml @@ -0,0 +1,10 @@ + + diff --git a/android/KMAPro/kMAPro/src/main/res/layout/nav_divider.xml b/android/KMAPro/kMAPro/src/main/res/layout/nav_divider.xml new file mode 100644 index 0000000000..192100c926 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/layout/nav_divider.xml @@ -0,0 +1,10 @@ + + diff --git a/android/KMAPro/kMAPro/src/main/res/layout/nav_header.xml b/android/KMAPro/kMAPro/src/main/res/layout/nav_header.xml new file mode 100644 index 0000000000..a59be8fd9d --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/layout/nav_header.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/layout/nav_switch_item.xml b/android/KMAPro/kMAPro/src/main/res/layout/nav_switch_item.xml new file mode 100644 index 0000000000..af00b3fe78 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/layout/nav_switch_item.xml @@ -0,0 +1,10 @@ + + diff --git a/android/KMAPro/kMAPro/src/main/res/menu/main.xml b/android/KMAPro/kMAPro/src/main/res/menu/main.xml index 7f26efbf81..d98360a9a9 100644 --- a/android/KMAPro/kMAPro/src/main/res/menu/main.xml +++ b/android/KMAPro/kMAPro/src/main/res/menu/main.xml @@ -4,19 +4,24 @@ xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity"> - + + + + - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/values/strings.xml b/android/KMAPro/kMAPro/src/main/res/values/strings.xml index 0423bdc015..bf53ca935a 100644 --- a/android/KMAPro/kMAPro/src/main/res/values/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values/strings.xml @@ -6,6 +6,9 @@ Share + Open navigation drawer + Close navigation drawer + Web Browser diff --git a/common/web/types/package.json b/common/web/types/package.json index 2b2122f682..4c2e2bfd2e 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -36,6 +36,8 @@ "restructure": "3.0.1" }, "devDependencies": { + "@types/mocha": "^5.2.7", + "@types/node": "^20.4.1", "ajv": "^8.12.0", "ajv-cli": "^5.0.0", "ajv-formats": "^2.1.1", diff --git a/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas b/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas deleted file mode 100644 index abc1b0f169..0000000000 --- a/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas +++ /dev/null @@ -1,789 +0,0 @@ -unit Keyman.UI.UframeCEFHost; - -interface - -uses - System.Classes, - System.Contnrs, - System.SysUtils, - System.Types, - System.UITypes, - Vcl.Controls, - Vcl.Dialogs, - Vcl.ExtCtrls, - Vcl.Forms, - Vcl.Graphics, - Vcl.Menus, - Winapi.Messages, - Winapi.Windows, - - uCEFChromium, - uCEFChromiumCore, - uCEFChromiumEvents, - uCEFChromiumWindow, - uCEFInterfaces, - uCEFTypes, - uCEFWinControl, - uCEFWindowParent, - - Keyman.System.CEFManager, - UserMessages, - utilexecute; - -const - CEF_DESTROY = WM_USER + 300; - CEF_AFTERDESTROY = WM_USER + 301; - CEF_AFTERCREATE = WM_USER + 302; - CEF_SHOW = WM_USER + 303; - CEF_LOADEND = WM_USER + 304; - CEF_KEYEVENT = WM_USER + 306; - CEF_BEFOREBROWSE = WM_USER + 307; - CEF_TITLECHANGE = WM_USER + 309; - CEF_COMMAND = WM_USER + 310; - CEF_SETFOCUS = WM_USER + 312; - CEF_LOADINGSTATECHANGE = WM_USER + 313; - - - CEF_LOADINGSTATECHANGE_ISLOADING = $0001; - CEF_LOADINGSTATECHANGE_CANGOBACK = $0002; - CEF_LOADINGSTATECHANGE_CANGOFORWARD = $0004; - -type - TCEFHostKeyEventData = record - browserid: Integer; - event: TCefKeyEvent; - osEvent: TMsg; - end; - - PCEFHostKeyEventData = ^TCEFHostKeyEventData; - - TCEFConsoleMessageEventData = record - browserid: Integer; - level: Cardinal; - message, source: ustring; - line: Integer; - end; - - PCEFConsoleMessageEventData = ^TCEFConsoleMessageEventData; - - TCEFTitleChangeEventData = record - browserid: Integer; - title: string; - end; - - PCEFTitleChangeEventData = ^TCEFTitleChangeEventData; - - TCEFHostBeforeBrowseExSyncEvent = procedure(Sender: TObject; const Url: string; isMain, isPopup: Boolean; out Handled: Boolean) of object; - TCEFHostBeforeBrowseSyncEvent = procedure(Sender: TObject; const Url: string; isPopup: Boolean; out Handled: Boolean) of object; - TCEFHostBeforeBrowseExEvent = procedure(Sender: TObject; const Url: string; isMain, isPopup, wasHandled: Boolean) of object; - TCEFHostBeforeBrowseEvent = procedure(Sender: TObject; const Url: string; isPopup, wasHandled: Boolean) of object; - TCEFCommandEvent = procedure(Sender: TObject; const command: string; params: TStringList) of object; - - TCEFHostPreKeySyncEvent = procedure(Sender: TObject; e: TCEFHostKeyEventData; out isShortcut, Handled: Boolean) of object; - TCEFHostKeyEvent = procedure(Sender: TObject; e: TCEFHostKeyEventData; wasShortcut, wasHandled: Boolean) of object; - TCEFHostTitleChangeEvent = procedure(Sender: TObject; const title: string) of object; - TCEFHostLoadingStateChangeEvent = procedure(Sender: TObject; isLoading, canGoBack, canGoForward: Boolean) of object; - - TframeCEFHost = class(TForm, IKeymanCEFHost) - tmrRefresh: TTimer; - tmrCreateBrowser: TTimer; - cefwp: TCEFWindowParent; - cef: TChromium; - procedure FormCreate(Sender: TObject); - procedure tmrCreateBrowserTimer(Sender: TObject); - procedure FormShow(Sender: TObject); - procedure FormDestroy(Sender: TObject); - procedure cefAfterCreated(Sender: TObject; const browser: ICefBrowser); - procedure cefBeforeClose(Sender: TObject; const browser: ICefBrowser); - procedure cefClose(Sender: TObject; const browser: ICefBrowser; - var aAction: TCefCloseBrowserAction); - procedure cefPreKeyEvent(Sender: TObject; const browser: ICefBrowser; - const event: PCefKeyEvent; osEvent: TCefEventHandle; out isKeyboardShortcut, - Result: Boolean); // I2986 - - procedure cefLoadEnd(Sender: TObject; const browser: ICefBrowser; - const frame: ICefFrame; httpStatusCode: Integer); - procedure cefBeforeBrowse(Sender: TObject; const browser: ICefBrowser; - const frame: ICefFrame; const request: ICefRequest; user_gesture, - isRedirect: Boolean; out Result: Boolean); - procedure cefRunContextMenu(Sender: TObject; const browser: ICefBrowser; - const frame: ICefFrame; const params: ICefContextMenuParams; - const model: ICefMenuModel; - const callback: ICefRunContextMenuCallback; - var aResult : Boolean); - procedure cefBeforePopup(Sender: TObject; - const browser: ICefBrowser; - const frame: ICefFrame; - const targetUrl, - targetFrameName: ustring; - targetDisposition: TCefWindowOpenDisposition; - userGesture: Boolean; - const popupFeatures: TCefPopupFeatures; - var windowInfo: TCefWindowInfo; - var client: ICefClient; - var settings: TCefBrowserSettings; - var extra_info: ICefDictionaryValue; - var noJavascriptAccess: Boolean; - var Result: Boolean); - procedure cefSetFocus(Sender: TObject; const browser: ICefBrowser; - source: TCefFocusSource; out Result: Boolean); - procedure cefTitleChange(Sender: TObject; const browser: ICefBrowser; - const title: ustring); - procedure cefWidgetCompMsg(Sender: TObject; var aMessage: TMessage; var aHandled: Boolean); - procedure cefLoadingStateChange(Sender: TObject; const browser: ICefBrowser; - isLoading, canGoBack, canGoForward: Boolean); - private - FApplicationHandle: THandle; - FNextURL: string; - FOnLoadEnd: TNotifyEvent; - FOnBeforeBrowseSync: TCEFHostBeforeBrowseSyncEvent; - FOnBeforeBrowseExSync: TCEFHostBeforeBrowseExSyncEvent; - FOnAfterCreated: TNotifyEvent; - FShutdownCompletionHandler: TShutdownCompletionHandlerEvent; - FIsClosing: Boolean; - FShouldShowContextMenu: Boolean; - FShouldOpenRemoteUrlsInBrowser: Boolean; - - FCallbackWnd: THandle; - FOnPreKeySyncEvent: TCEFHostPreKeySyncEvent; - FOnKeyEvent: TCEFHostKeyEvent; - FOnBeforeBrowse: TCEFHostBeforeBrowseEvent; - FOnBeforeBrowseEx: TCEFHostBeforeBrowseExEvent; - FOnTitleChange: TCEFHostTitleChangeEvent; - FOnCommand: TCEFCommandEvent; - FOnHelpTopic: TNotifyEvent; - FIsCreated: Boolean; - FOnLoadingStateChange: TCEFHostLoadingStateChangeEvent; - - procedure CallbackWndProc(var Message: TMessage); - - // IKeymanCEFHost - procedure StartShutdown(CompletionHandler: TShutdownCompletionHandlerEvent); - function GetDebugInfo: string; - - procedure Handle_CEF_DESTROY(var Message: TMessage); - procedure Handle_CEF_AFTERDESTROY(var Message: TMessage); - procedure Handle_CEF_AFTERCREATE(var Message: TMessage); - procedure Handle_CEF_SHOW(var message: TMessage); - procedure Handle_CEF_LOADEND(var message: TMessage); - procedure Handle_CEF_KEYEVENT(var message: TMessage); - procedure Handle_CEF_BEFOREBROWSE(var message: TMessage); - procedure Handle_CEF_TITLECHANGE(var message: TMessage); - procedure Handle_CEF_COMMAND(var message: TMessage); - procedure Handle_CEF_SETFOCUS(var message: TMessage); - procedure Handle_CEF_LOADINGSTATECHANGE(var message: TMessage); - - // CEF: You have to handle this two messages to call NotifyMoveOrResizeStarted or some page elements will be misaligned. - procedure WMMove(var aMessage : TWMMove); message WM_MOVE; - procedure WMMoving(var aMessage : TMessage); message WM_MOVING; - // CEF: You also have to handle these two messages to set GlobalCEFApp.OsmodalLoop - procedure WMEnterMenuLoop(var aMessage: TMessage); message WM_ENTERMENULOOP; - procedure WMExitMenuLoop(var aMessage: TMessage); message WM_EXITMENULOOP; - - procedure CreateBrowser; - procedure Navigate; overload; - procedure DoBeforeBrowse(const url: string; isMain, isPopup, ShouldOpenUrlIfNotHandled: Boolean; out Handled: Boolean); - public - procedure SetFocus; override; - procedure StartClose; - procedure Navigate(const url: string); overload; - function HasFocus: Boolean; - property ShouldShowContextMenu: Boolean read FShouldShowContextMenu write FShouldShowContextMenu; - property ShouldOpenRemoteUrlsInBrowser: Boolean read FShouldOpenRemoteUrlsInBrowser write FShouldOpenRemoteUrlsInBrowser; - property OnAfterCreated: TNotifyEvent read FOnAfterCreated write FOnAfterCreated; - property OnBeforeBrowseExSync: TCEFHostBeforeBrowseExSyncEvent read FOnBeforeBrowseExSync write FOnBeforeBrowseExSync; - property OnBeforeBrowseSync: TCEFHostBeforeBrowseSyncEvent read FOnBeforeBrowseSync write FOnBeforeBrowseSync; - property OnCommand: TCEFCommandEvent read FOnCommand write FOnCommand; - property OnBeforeBrowseEx: TCEFHostBeforeBrowseExEvent read FOnBeforeBrowseEx write FOnBeforeBrowseEx; - property OnBeforeBrowse: TCEFHostBeforeBrowseEvent read FOnBeforeBrowse write FOnBeforeBrowse; - property OnHelpTopic: TNotifyEvent read FOnHelpTopic write FOnHelpTopic; - property OnLoadEnd: TNotifyEvent read FOnLoadEnd write FOnLoadEnd; - property OnTitleChange: TCEFHostTitleChangeEvent read FOnTitleChange write FOnTitleChange; - property OnPreKeySyncEvent: TCEFHostPreKeySyncEvent read FOnPreKeySyncEvent write FOnPreKeySyncEvent; - property OnKeyEvent: TCEFHostKeyEvent read FOnKeyEvent write FOnKeyEvent; - property OnLoadingStateChange: TCEFHostLoadingStateChangeEvent read FOnLoadingStateChange write FOnLoadingStateChange; - end; - -// Helpers to make sure we don't accidentally code -// VCL references into non-VCL-thread functions -procedure AssertVclThread; -procedure AssertCefThread; - -implementation - -uses - System.StrUtils, - Winapi.ShellApi, - - ErrorControlledRegistry, - utilhttp, - uCEFApplication, - uCEFConstants, - uCEFProcessMessage, - VersionInfo; - -{$R *.DFM} - -{ TfrmCEFHost } - -// Destruction steps -// ================= -// 1. The FormCloseQuery event sets CanClose to False and calls TChromiumWindow.CloseBrowser, which triggers the TChromiumWindow.OnClose event. -// 2. The TChromiumWindow.OnClose event calls TChromiumWindow.DestroyChildWindow which triggers the TChromiumWindow.OnBeforeClose event. -// 3. TChromiumWindow.OnBeforeClose sets FCanClose to True and closes the form. - -procedure AssertVclThread; -begin - Assert(GetCurrentThreadId = MainThreadID); -end; - -procedure AssertCefThread; -begin - Assert(GetCurrentThreadId <> MainThreadID); -end; - -procedure TframeCEFHost.StartClose; -begin - AssertVclThread; - Visible := False; - FIsClosing := True; - cef.CloseBrowser(True); -end; - -procedure TframeCEFHost.StartShutdown(CompletionHandler: TShutdownCompletionHandlerEvent); -begin - AssertVclThread; - OutputDebugString(PChar('TframeCEFHost.StartShutdown')); - FIsClosing := True; - FShutdownCompletionHandler := CompletionHandler; - - // If the browser has not been initialized, we'll not get the close signal, so we - // post it to occur on next idle. - if cef.Initialized - then cef.CloseBrowser(False) - else PostMessage(FCallbackWnd, CEF_AFTERDESTROY, 0, 0); -end; - -procedure TframeCEFHost.FormCreate(Sender: TObject); -begin - AssertVclThread; - inherited; - FApplicationHandle := Application.Handle; // take a copy to avoid Vcl thread mismatches in cef callbacks - - // We need our own window handle for events, because VCL windows can be destroyed - // and recreated at any time. With our own handle, we can guarantee the lifetime - // of it across threads. - FCallbackWnd := AllocateHWnd(CallbackWndProc); - FInitializeCEF.RegisterWindow(Self); -// CreateBrowser; -end; - -procedure TframeCEFHost.FormDestroy(Sender: TObject); -begin - AssertVclThread; -// OutputDebugString(PChar('TframeCEFHost.FormDestroy')); - inherited; - FInitializeCEF.UnregisterWindow(Self); - DeallocateHWnd(FCallbackWnd); -end; - -procedure TframeCEFHost.FormShow(Sender: TObject); -begin - AssertVclThread; - inherited; - PostMessage(FCallbackWnd, CEF_SHOW, 0, 0); -end; - -function TframeCEFHost.GetDebugInfo: string; -begin - Result := FNextURL; - if Assigned(Owner) then Result := Owner.ClassName+':'+Result; -end; - -function TframeCEFHost.HasFocus: Boolean; -begin - AssertVclThread; - Result := Assigned(cefwp) and cefwp.HandleAllocated and IsChild(cefwp.Handle, GetFocus); -end; - -procedure TframeCEFHost.Handle_CEF_SHOW(var message: TMessage); -begin - AssertVclThread; - CreateBrowser; -end; - -procedure TframeCEFHost.Handle_CEF_TITLECHANGE(var message: TMessage); -var - p: PCEFTitleChangeEventData; -begin - AssertVclThread; - p := PCEFTitleChangeEventData(message.LParam); - - if Assigned(FOnTitleChange) then - FOnTitleChange(Self, p.title); - - FreeMem(p); -end; - -procedure TframeCEFHost.cefWidgetCompMsg(Sender: TObject; var aMessage: TMessage; - var aHandled: Boolean); -begin - AssertCefThread; - if aMessage.Msg = WM_SETFOCUS then - PostMessage(FCallbackWnd, CEF_SETFOCUS, 0, 0); -end; - -procedure TframeCEFHost.CreateBrowser; -begin - AssertVclThread; - FIsCreated := True; - tmrCreateBrowser.Enabled := not cef.CreateBrowser(cefwp); -end; - -procedure TframeCEFHost.Navigate(const url: string); -begin - AssertVclThread; - FNextURL := url; - Navigate; -end; - -procedure TframeCEFHost.Navigate; -begin - AssertVclThread; - if FNextURL = '' then - Exit; - - if not FIsCreated then - begin - cef.DefaultUrl := FNextURL; - FNextURL := ''; - Exit; - end; - - if not cef.Initialized then - begin - // After initialization, refresh will happen - // See cefAfterCreated - Exit; - end; - - cef.LoadURL(FNextURL); -end; - -procedure TframeCEFHost.SetFocus; -begin - AssertVclThread; - if not FIsClosing and Assigned(cefwp) and Assigned(cef) and cefwp.CanFocus then - begin - GetParentForm(Self).ActiveControl := Self; - cef.SetFocus(True); - end; -end; - -procedure TframeCEFHost.CallbackWndProc(var Message: TMessage); -begin - AssertVclThread; - case Message.Msg of - CEF_DESTROY: Handle_CEF_DESTROY(Message); - CEF_AFTERDESTROY: Handle_CEF_AFTERDESTROY(Message); - CEF_AFTERCREATE: Handle_CEF_AFTERCREATE(Message); - CEF_SHOW: Handle_CEF_SHOW(Message); - CEF_LOADEND: Handle_CEF_LOADEND(Message); - CEF_KEYEVENT: Handle_CEF_KEYEVENT(Message); - CEF_BEFOREBROWSE: Handle_CEF_BEFOREBROWSE(Message); - CEF_TITLECHANGE: Handle_CEF_TITLECHANGE(Message); - CEF_COMMAND: Handle_CEF_COMMAND(Message); - CEF_SETFOCUS: Handle_CEF_SETFOCUS(Message); - CEF_LOADINGSTATECHANGE: Handle_CEF_LOADINGSTATECHANGE(Message); - end; - - if Self <> nil then - Message.Result := DefWindowProc(FCallbackWnd, Message.Msg, Message.WParam, Message.LParam); -end; - -procedure TframeCEFHost.Handle_CEF_AFTERCREATE(var Message: TMessage); -begin - AssertVclThread; - Navigate; - if Assigned(FOnAfterCreated) then - FOnAfterCreated(Self); -end; - -procedure TframeCEFHost.cefAfterCreated(Sender: TObject; - const browser: ICefBrowser); -begin - AssertCefThread; - PostMessage(FCallbackWnd, CEF_AFTERCREATE, 0, 0); -end; - -procedure TframeCEFHost.Handle_CEF_AFTERDESTROY(var Message: TMessage); -begin - AssertVclThread; - if Assigned(FShutdownCompletionHandler) then - begin - FShutdownCompletionHandler(Self); - FShutdownCompletionHandler := nil; - end; -end; - -function IsLocalURL(URL: string): Boolean; -begin - Result := - URL.StartsWith('file:') or - URL.StartsWith('/') or - URL.StartsWith('http://localhost:') or - URL.StartsWith('http://localhost/') or - URL.StartsWith('http://127.0.0.1:') or - URL.StartsWith('http://127.0.0.1/'); -end; - -function IsPDFURL(URL: string): Boolean; -begin - Result := LowerCase(URL).EndsWith('.pdf'); -end; - -procedure TframeCEFHost.Handle_CEF_BEFOREBROWSE(var message: TMessage); -var - params: TStringList; - url: string; - isMain, isPopup, wasHandled, - shouldOpenUrlIfNotHandled: Boolean; -begin - AssertVclThread; - - params := TStringList(message.LParam); - url := params[0]; - wasHandled := (message.WParam and 1) = 1; - shouldOpenUrlIfNotHandled := (message.WParam and 2) = 2; - isPopup := (message.WParam and 4) = 4; - isMain := (message.WParam and 8) = 8; - - if wasHandled then - begin - if Assigned(FOnBeforeBrowseEx) then - begin - FOnBeforeBrowseEx(Self, url, isMain, isPopup, wasHandled); - end - else if Assigned(FOnBeforeBrowse) then - begin - FOnBeforeBrowse(Self, url, isPopup, wasHandled); - end; - - if FShouldOpenRemoteUrlsInBrowser and (not IsLocalURL(URL) or IsPDFURL(URL)) then - begin - {$MESSAGE HINT 'Refactor how remote URLs are handled'} - // TODO: refactor links - TUtilExecute.URL(url); - end; - end - else if shouldOpenUrlIfNotHandled then - cef.LoadURL(url); - - params.Free; -end; - -procedure TframeCEFHost.Handle_CEF_COMMAND(var message: TMessage); -var - params: TStringList; - command: string; -begin - AssertVclThread; - params := TStringList(message.LParam); - - if Assigned(FOnCommand) then - begin - params.Delete(0); // url; not really used currently - command := params[0]; - params.Delete(0); - FOnCommand(Self, command, params); - end; - - params.Free; -end; - -procedure TframeCEFHost.cefBeforeBrowse(Sender: TObject; - const browser: ICefBrowser; const frame: ICefFrame; - const request: ICefRequest; user_gesture, isRedirect: Boolean; - out Result: Boolean); -begin - AssertCefThread; - - DoBeforeBrowse(request.Url, frame.IsMain, False, False, Result); -end; - -procedure TframeCEFHost.DoBeforeBrowse(const url: string; isMain, isPopup, ShouldOpenUrlIfNotHandled: Boolean; out Handled: Boolean); -var - params: TStringList; - wParam: DWORD; -begin - AssertCefThread; - - Handled := False; - - if Assigned(FOnBeforeBrowseExSync) then - begin - FOnBeforeBrowseExSync(Self, url, isMain, isPopup, Handled); - end - else if Assigned(FOnBeforeBrowseSync) then - begin - FOnBeforeBrowseSync(Self, url, isPopup, Handled); - end; - - if not Handled and GetParamsFromURL(Url, params) then - begin - Handled := True; - // Use OnCommand for keyman: URLs - params.Insert(0, Url); - PostMessage(FCallbackWnd, CEF_COMMAND, 0, LPARAM(params)); - end - else - begin - // Use OnBeforeBrowse for other URLs - if not Handled and FShouldOpenRemoteUrlsInBrowser and (not IsLocalURL(URL) or IsPDFURL(URL)) then - Handled := True; - - params := TStringList.Create; - params.Add(Url); - wParam := 0; - if Handled then - wParam := wParam or 1; - if ShouldOpenUrlIfNotHandled then - wParam := wParam or 2; - if isPopup then - wParam := wParam or 4; - if isMain then - wParam := wParam or 8; - PostMessage(FCallbackWnd, CEF_BEFOREBROWSE, wParam, LPARAM(params)); - end; -end; - -procedure TframeCEFHost.cefBeforeClose(Sender: TObject; const browser: ICefBrowser); -begin - AssertCefThread; - PostMessage(FCallbackWnd, CEF_AFTERDESTROY, 0, 0); -end; - -procedure TframeCEFHost.cefClose(Sender: TObject; const browser: ICefBrowser; - var aAction: TCefCloseBrowserAction); -begin - AssertCefThread; - PostMessage(FCallbackWnd, CEF_DESTROY, 0, 0); - aAction := cbaClose; -end; - -procedure TframeCEFHost.Handle_CEF_DESTROY(var Message: TMessage); -begin - AssertVclThread; - if Assigned(cefwp) then - cefwp.DestroyChildWindow; - FreeAndNil(cefwp); -end; - -procedure TframeCEFHost.tmrCreateBrowserTimer(Sender: TObject); -begin - AssertVclThread; - tmrCreateBrowser.Enabled := False; - CreateBrowser; -end; - -procedure TframeCEFHost.cefLoadEnd(Sender: TObject; const browser: ICefBrowser; - const frame: ICefFrame; httpStatusCode: Integer); -begin - AssertCefThread; - PostMessage(FCallbackWnd, CEF_LOADEND, WPARAM(httpStatusCode), 0); -end; - -procedure TframeCEFHost.cefLoadingStateChange(Sender: TObject; - const browser: ICefBrowser; isLoading, canGoBack, canGoForward: Boolean); -var - v: Integer; -begin - AssertCefThread; - v := 0; - if isLoading then v := v or CEF_LOADINGSTATECHANGE_ISLOADING; - if canGoBack then v := v or CEF_LOADINGSTATECHANGE_CANGOBACK; - if canGoForward then v := v or CEF_LOADINGSTATECHANGE_CANGOFORWARD; - PostMessage(FCallbackWnd, CEF_LOADINGSTATECHANGE, v, 0); -end; - -procedure TframeCEFHost.Handle_CEF_KEYEVENT(var message: TMessage); -var - p: PCEFHostKeyEventData; - wasHandled: Boolean; - wasShortcut: Boolean; -begin - p := PCEFHostKeyEventData(message.LParam); - - if p.event.windows_key_code = VK_F1 then - begin - if Assigned(FOnHelpTopic) then FOnHelpTopic(Self); // TODO: frmKeymanDeveloper.HelpTopic(Self) - end - else if p.event.windows_key_code = VK_F12 then - begin - cef.ShowDevTools(Point(Low(Integer),Low(Integer)), nil); - end - else if Assigned(FOnKeyEvent) then - begin - wasHandled := message.WParamLo <> 0; - wasShortcut := message.WParamHi <> 0; - FOnKeyEvent(Self, p^, wasShortcut, wasHandled); - end; - FreeMem(p); -end; - -procedure TframeCEFHost.Handle_CEF_LOADEND(var message: TMessage); -begin - if csDestroying in ComponentState then - Exit; - - // The focus needs to be set again for key events to - // be passed to the CEF window, even if it appears - // to already be focused to the expected window. - if IsChild(Handle, GetFocus) then - SetFocus; - - if Assigned(FOnLoadEnd) then - FOnLoadEnd(Self); -end; - -procedure TframeCEFHost.Handle_CEF_LOADINGSTATECHANGE(var message: TMessage); -begin - if csDestroying in ComponentState then - Exit; - AssertVclThread; - - if Assigned(FOnLoadingStateChange) then - FOnLoadingStateChange(Self, - (message.WParam and CEF_LOADINGSTATECHANGE_ISLOADING) <> 0, - (message.WParam and CEF_LOADINGSTATECHANGE_CANGOBACK) <> 0, - (message.WParam and CEF_LOADINGSTATECHANGE_CANGOFORWARD) <> 0); -end; - -procedure TframeCEFHost.Handle_CEF_SETFOCUS(var message: TMessage); -begin - AssertVclThread; - if Assigned(cefwp) and cefwp.Visible and cefwp.CanFocus then - GetParentForm(cefwp).ActiveControl := cefwp; -end; - -procedure TframeCEFHost.cefPreKeyEvent(Sender: TObject; - const browser: ICefBrowser; const event: PCefKeyEvent; osEvent: TCefEventHandle; - out isKeyboardShortcut, Result: Boolean); -var - p: PCEFHostKeyEventData; -begin - AssertCefThread; - Result := False; - - p := AllocMem(Sizeof(TCEFHostKeyEventData)); - p.browserid := browser.Identifier; - p.event := event^; - if Assigned(osEvent) then - p.osEvent := osEvent^; - - if event.kind in [TCefKeyEventType.KEYEVENT_KEYDOWN, TCefKeyEventType.KEYEVENT_RAWKEYDOWN] then - begin - if Assigned(FOnPreKeySyncEvent) then - begin - FOnPreKeySyncEvent(Self, p^, isKeyboardShortcut, Result); - end; - - if not Result then // only run this if the prekeysyncevent didn't swallow the keystroke - begin - if event.windows_key_code = VK_F1 then - begin - isKeyboardShortcut := True; - Result := True; - end - else if event.windows_key_code = VK_F12 then - begin - isKeyboardShortcut := True; - Result := True; - end - else if event.windows_key_code <> VK_CONTROL then - begin - if SendMessage(FApplicationHandle, CM_APPKEYDOWN, event.windows_key_code, 0) = 1 then - begin - isKeyboardShortcut := True; - Result := True; - end; - end; - end; - - PostMessage(FCallbackWnd, CEF_KEYEVENT, MAKELONG(WORD(Result), WORD(isKeyboardShortcut)), LPARAM(p)); - end; -end; - - -procedure TframeCEFHost.cefBeforePopup(Sender: TObject; - const browser: ICefBrowser; const frame: ICefFrame; const targetUrl, - targetFrameName: ustring; targetDisposition: TCefWindowOpenDisposition; - userGesture: Boolean; const popupFeatures: TCefPopupFeatures; - var windowInfo: TCefWindowInfo; var client: ICefClient; - var settings: TCefBrowserSettings; - var extra_info: ICefDictionaryValue; - var noJavascriptAccess, Result: Boolean); -begin - AssertCefThread; - DoBeforeBrowse(targetUrl, frame.IsMain, True, True, Result); -end; - -procedure TframeCEFHost.cefRunContextMenu(Sender: TObject; - const browser: ICefBrowser; const frame: ICefFrame; - const params: ICefContextMenuParams; const model: ICefMenuModel; - const callback: ICefRunContextMenuCallback; var aResult: Boolean); -begin - AssertCefThread; - // Return FALSE to show default context menu - aResult := not FShouldShowContextMenu and (GetKeyState(VK_SHIFT) >= 0); -end; - -procedure TframeCEFHost.cefSetFocus(Sender: TObject; const browser: ICefBrowser; - source: TCefFocusSource; out Result: Boolean); -begin - Result := source = FOCUS_SOURCE_NAVIGATION; -end; - -procedure TframeCEFHost.cefTitleChange(Sender: TObject; - const browser: ICefBrowser; const title: ustring); -var - p: PCEFTitleChangeEventData; -begin - AssertCefThread; - - p := AllocMem(SizeOf(TCEFTitleChangeEventData)); - p.browserid := browser.Identifier; - p.title := title; - - PostMessage(FCallbackWnd, CEF_TITLECHANGE, 0, LPARAM(p)); -end; - -procedure TframeCEFHost.WMEnterMenuLoop(var aMessage: TMessage); -begin - AssertVclThread; - inherited; - if (aMessage.wParam = 0) and (GlobalCEFApp <> nil) then GlobalCEFApp.OsmodalLoop := True; -end; - -procedure TframeCEFHost.WMExitMenuLoop(var aMessage: TMessage); -begin - AssertVclThread; - inherited; - if (aMessage.wParam = 0) and (GlobalCEFApp <> nil) then GlobalCEFApp.OsmodalLoop := False; -end; - -procedure TframeCEFHost.WMMove(var aMessage: TWMMove); -begin - AssertVclThread; - inherited; - if cef <> nil then cef.NotifyMoveOrResizeStarted; -end; - -procedure TframeCEFHost.WMMoving(var aMessage: TMessage); -begin - AssertVclThread; - inherited; - if cef <> nil then cef.NotifyMoveOrResizeStarted; -end; - -end.