Merge branch 'side-panel-prod' of https://github.com/memaynor/keyman into dark-mode-on-side-panel

This commit is contained in:
kongchanlina 2026-03-23 16:09:02 +07:00
commit bb8b8f01b3
13 changed files with 509 additions and 1183 deletions

View file

@ -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){

View file

@ -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);
}
}

View file

@ -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
*/

View file

@ -1,42 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:layout_above="@+id/KMKeyboard"
android:orientation="vertical"
android:background="@android:color/white"
tools:context=".MainActivity">
android:fitsSystemWindows="true">
<include layout="@layout/titlebar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<include
layout="@layout/check_chrome_webview_layout"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/titlebar"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/titlebar" />
android:layout_height="match_parent"
android:layout_above="@+id/KMKeyboard"
android:orientation="vertical"
android:background="@android:color/white"
tools:context=".MainActivity"
android:fitsSystemWindows="true">
<com.keyman.engine.KMTextView
android:id="@+id/kmTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/checkWebViewChromeLayout"
android:background="@drawable/textview_bg"
android:ems="10"
android:gravity="top"
android:hint="@string/textview_hint"
android:inputType="textMultiLine|textNoSuggestions"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="@id/checkWebViewChromeLayout">
<include layout="@layout/titlebar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<requestFocus />
</com.keyman.engine.KMTextView>
<include
layout="@layout/check_chrome_webview_layout"
android:layout_width="96dp"
android:layout_height="104dp"
android:layout_below="@id/titlebar"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/titlebar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.keyman.engine.KMTextView
android:id="@+id/kmTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/checkWebViewChromeLayout"
android:background="@drawable/textview_bg"
android:ems="10"
android:gravity="top"
android:hint="@string/textview_hint"
android:inputType="textMultiLine|textNoSuggestions"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="@id/checkWebViewChromeLayout">
<requestFocus />
</com.keyman.engine.KMTextView>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Sidebar — must be at the bottom of DrawerLayout -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="330dp"
android:layout_height="match_parent"
android:layout_gravity="end"
android:paddingHorizontal="10dp"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header"
app:menu="@menu/nav_menu" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatCheckBox xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/nav_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:focusable="false"
android:clickable="true"
android:minWidth="48dp"
android:minHeight="48dp" />

View file

@ -0,0 +1,10 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="2dp"
android:gravity="bottom"
android:padding="0dp"
android:orientation="vertical"
android:layout_marginBottom="2dp"
android:background="@color/keyman_orange">
</LinearLayout>

View file

@ -0,0 +1,19 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="bottom"
android:paddingVertical="10dp"
android:paddingHorizontal="5dp"
android:orientation="vertical"
android:layout_marginBottom="2dp"
>
<ImageView
android:layout_width="120dp"
android:layout_height="wrap_content"
android:src="@drawable/keyman_logo"
/>
</LinearLayout>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.SwitchCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/nav_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:focusable="false"
android:clickable="true"
android:minWidth="48dp"
android:minHeight="48dp" />

View file

@ -4,19 +4,24 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item android:id="@+id/action_share"
app:showAsAction="always"
android:title="@string/action_share"
android:icon="@drawable/ic_action_share_mode"
/>
<!-- <item android:id="@+id/action_share"-->
<!-- app:showAsAction="always"-->
<!-- android:title="@string/action_share"-->
<!-- android:icon="@drawable/ic_action_share_mode"
/>-->
<!-- Disable Web Browser to investigate Google sign-in -->
<!--item
<!--<item
android:id="@+id/action_web"
app:showAsAction="always"
android:title="@string/action_web"
android:icon="@drawable/ic_light_action_web" /> -->
<!-- <item android:id="@+id/action_share"-->
<!-- app:showAsAction="always"-->
<!-- android:title="@string/action_share"-->
<!-- android:icon="@drawable/ic_light_action_share" />-->
<!-- Set showAsAction="never" for overflow -->
<item
android:id="@+id/action_overflow"
@ -29,8 +34,11 @@
<!-- android:title="@string/action_share"-->
<!-- android:icon="@drawable/ic_light_action_share" />-->
<menu
>
<menu>
<item android:id="@+id/action_share"
app:showAsAction="always"
android:title="@string/action_share"
android:icon="@drawable/ic_light_action_share" />
<item
android:id="@+id/action_text_size"

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- <group android:checkableBehavior="single">-->
<!-- <item-->
<!-- android:id="@+id/nav_home"-->
<!-- android:title="Home" />-->
<!-- </group>-->
<item android:title="Setup"
>
<menu>
<item
android:id="@+id/nav_installed_languages"
android:icon="@drawable/ic_folder_open"
android:title="Installed Languages" />
<item
android:id="@+id/nav_install_keyboard"
android:icon="@drawable/ic_content_add"
android:title="Install Keyboard or Dictionary" />
<item
android:id="@+id/nav_checkbox_enable_system_keyboard"
android:icon="@drawable/ic_keyboard"
android:title="Enable System Keyboard"
app:actionLayout="@layout/nav_checkbox_item" />
<item
android:id="@+id/nav_checkbox_set_default_keyboard"
android:icon="@drawable/ic_settings"
android:title="Set Keyman As Default"
app:actionLayout="@layout/nav_checkbox_item" />
</menu>
</item>
<item
android:title="Preferences">
<menu>
<item
android:id="@+id/nav_display_language"
android:icon="@drawable/ic_translate"
android:title="Display Language" />
<item
android:id="@+id/nav_keyboard_height"
android:icon="@drawable/ic_height"
android:title="Adjust Keyboard Height" />
<item
android:id="@+id/nav_longpress_delay"
android:icon="@drawable/ic_action_timelapse"
android:title="Adjust Longpress Delay" />
<item
android:id="@+id/nav_toggle_show_osk"
android:icon="@drawable/ic_keyboard"
android:title="Show OSK"
app:actionLayout="@layout/nav_switch_item" />
<item
android:id="@+id/nav_toggle_haptic_feedback"
android:icon="@drawable/ic_navigation_refresh"
android:title="Haptic Feedback"
app:actionLayout="@layout/nav_switch_item" />
<item
android:id="@+id/nav_toggle_send_crash_report"
android:icon="@drawable/ic_info_outline"
android:title="Send Crash Report"
app:actionLayout="@layout/nav_switch_item" />
<item
android:id="@+id/nav_settings"
android:icon="@drawable/ic_settings"
android:title="More Settings…" />
</menu>
</item>
<item android:title="About">
<menu>
<item
android:id="@+id/nav_about"
android:icon="@drawable/ic_info_outline"
android:title="Keyman for Android" />
</menu>
</item>
</menu>

View file

@ -6,6 +6,9 @@
<!-- Context: Menu Action -->
<string name="action_share" comment="Menu action to send text content to another app">Share</string>
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>
<!-- Context: Menu Action -->
<string name="action_web" comment="Menu action to open Keyman browser">Web Browser</string>

View file

@ -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",

View file

@ -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.