Release: 1.1.6

This commit is contained in:
zt515 2017-07-11 02:57:33 +08:00
parent c9de9541bf
commit d7ea4352ac
43 changed files with 1472 additions and 855 deletions

View File

@ -17,8 +17,8 @@ android {
applicationId "io.neoterm"
minSdkVersion 21
targetSdkVersion 25
versionCode 6
versionName "1.1.4"
versionCode 8
versionName "1.1.6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resConfigs "zh"
externalNativeBuild {

View File

@ -6,21 +6,21 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:name=".App"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_config"
android:extractNativeLibs="true"
android:fullBackupContent="@xml/backup_config"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".ui.NeoTermActivity"
android:name=".ui.term.NeoTermActivity"
android:configChanges="orientation|keyboardHidden"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden">
<intent-filter>
@ -29,9 +29,18 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.crash.CrashActivity"
android:exported="false"
android:label="@string/error"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".ui.setup.SetupActivity"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".ui.bonus.BonusActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:configChanges="orientation|keyboardHidden"/>
<activity
android:name=".ui.pm.PackageManagerActivity"
android:label="@string/package_settings"
@ -39,8 +48,7 @@
<activity
android:name=".ui.customization.CustomizationActivity"
android:label="@string/customization_settings"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden" />
android:theme="@style/Theme.AppCompat.NoActionBar" />
<activity
android:name=".ui.settings.SettingActivity"
android:theme="@style/Theme.AppCompat" />
@ -53,11 +61,12 @@
<activity-alias
android:name=".NeoLotMainActivity"
android:targetActivity="io.neoterm.ui.NeoTermActivity">
android:targetActivity="io.neoterm.ui.term.NeoTermActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.IOT_LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.IOT_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>
@ -65,7 +74,9 @@
android:name=".services.NeoTermService"
android:enabled="true" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true" />
</application>
</manifest>

View File

@ -3,6 +3,7 @@ package io.neoterm
import android.app.Application
import io.neoterm.customize.color.ColorSchemeManager
import io.neoterm.customize.font.FontManager
import io.neoterm.utils.CrashHandler
/**
* @author kiva
@ -11,13 +12,15 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
app = this
CrashHandler.init()
// ensure that we can access these any time
ColorSchemeManager.init(this)
FontManager.init(this)
}
companion object {
var app: App? = null
private var app: App? = null
fun get(): App {
return app!!

View File

@ -8,7 +8,7 @@ import java.io.*
*/
class EksConfigParser {
companion object {
const val PARSER_VERSION = 2
const val PARSER_VERSION = 4
}
private lateinit var source: BufferedReader

View File

@ -16,9 +16,11 @@ import java.io.File
*/
object NeoPreference {
const val KEY_HAPPY_EGG = "neoterm_fun_happy"
const val KEY_FONT_SIZE = "neoterm_general_font_size"
const val KEY_CURRENT_SESSION = "neoterm_service_current_session"
const val VALUE_HAPPY_EGG_TRIGGER = 8
const val VALUE_NEOTERM_ONLY = "NeoTermOnly"
const val VALUE_NEOTERM_FIRST = "NeoTermFirst"
const val VALUE_SYSTEM_FIRST = "SystemFirst"

View File

@ -1,22 +1,26 @@
package io.neoterm.services
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.wifi.WifiManager
import android.os.Binder
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.content.WakefulBroadcastReceiver
import android.util.Log
import io.neoterm.R
import io.neoterm.backend.EmulatorDebug
import io.neoterm.backend.TerminalSession
import io.neoterm.ui.NeoTermActivity
import io.neoterm.ui.term.NeoTermActivity
import io.neoterm.utils.TerminalUtils
import java.util.*
/**
* @author kiva
*/
@ -28,6 +32,8 @@ class NeoTermService : Service() {
private val neoTermBinder = NeoTermBinder()
private val mTerminalSessions = ArrayList<TerminalSession>()
private var mWakeLock: PowerManager.WakeLock? = null
private var mWifiLock: WifiManager.WifiLock? = null
override fun onCreate() {
super.onCreate()
@ -40,12 +46,18 @@ class NeoTermService : Service() {
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val action = intent.action
if (ACTION_SERVICE_STOP == action) {
for (i in mTerminalSessions.indices)
mTerminalSessions[i].finishIfRunning()
stopSelf()
} else if (action != null) {
Log.e(EmulatorDebug.LOG_TAG, "Unknown NeoTermService action: '$action'")
when (action) {
ACTION_SERVICE_STOP -> {
for (i in mTerminalSessions.indices)
mTerminalSessions[i].finishIfRunning()
stopSelf()
}
ACTION_ACQUIRE_LOCK -> acquireLock()
ACTION_RELEASE_LOCK -> releaseLock()
null -> Log.e(EmulatorDebug.LOG_TAG, "Unknown NeoTermService action: '$action'")
}
if (flags and Service.START_FLAG_REDELIVERY == 0) {
@ -76,8 +88,10 @@ class NeoTermService : Service() {
fun removeTermSession(sessionToRemove: TerminalSession): Int {
val indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove)
mTerminalSessions.removeAt(indexOfRemoved)
updateNotification()
if (indexOfRemoved >= 0) {
mTerminalSessions.removeAt(indexOfRemoved)
updateNotification()
}
return indexOfRemoved
}
@ -93,7 +107,10 @@ class NeoTermService : Service() {
val pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0)
val sessionCount = mTerminalSessions.size
val contentText = sessionCount.toString() + " session" + if (sessionCount == 1) "" else "s"
var contentText = getString(R.string.service_status_text, sessionCount)
val lockAcquired = mWakeLock != null
if (lockAcquired) contentText += getString(R.string.service_lock_acquired)
val builder = Notification.Builder(this)
builder.setContentTitle(getText(R.string.app_name))
@ -104,14 +121,54 @@ class NeoTermService : Service() {
builder.setShowWhen(false)
builder.setColor(0xFF000000.toInt())
builder.setPriority(if (lockAcquired) Notification.PRIORITY_HIGH else Notification.PRIORITY_MIN)
val exitIntent = Intent(this, NeoTermService::class.java).setAction(ACTION_SERVICE_STOP)
builder.addAction(android.R.drawable.ic_delete, "Exit", PendingIntent.getService(this, 0, exitIntent, 0))
val newWakeAction = if (lockAcquired) ACTION_RELEASE_LOCK else ACTION_ACQUIRE_LOCK
val toggleWakeLockIntent = Intent(this, NeoTermService::class.java).setAction(newWakeAction)
val actionTitle = getString(if (lockAcquired)
R.string.service_release_lock
else
R.string.service_acquire_lock)
val actionIcon = if (lockAcquired) android.R.drawable.ic_lock_idle_lock else android.R.drawable.ic_lock_lock
builder.addAction(actionIcon, actionTitle, PendingIntent.getService(this, 0, toggleWakeLockIntent, 0))
return builder.build()
}
@SuppressLint("WakelockTimeout")
private fun acquireLock() {
if (mWakeLock == null) {
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG)
mWakeLock!!.acquire()
val wm = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG)
mWifiLock!!.acquire()
updateNotification()
}
}
private fun releaseLock() {
if (mWakeLock != null) {
mWakeLock!!.release()
mWakeLock = null
mWifiLock!!.release()
mWifiLock = null
updateNotification()
}
}
companion object {
val ACTION_SERVICE_STOP = "neoterm.action.service.stop"
val ACTION_ACQUIRE_LOCK = "neoterm.action.service.lock.acquire"
val ACTION_RELEASE_LOCK = "neoterm.action.service.lock.release"
private val NOTIFICATION_ID = 52019
}
}

View File

@ -0,0 +1,193 @@
package io.neoterm.ui.bonus;
import android.animation.ObjectAnimator;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.RippleDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowManager;
import android.view.animation.PathInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import io.neoterm.R;
/**
* @author kiva
*/
public class BonusActivity extends AppCompatActivity {
final static int[] FLAVORS = {
0xFF9C27B0, 0xFFBA68C8, // grape
0xFFFF9800, 0xFFFFB74D, // orange
0xFFF06292, 0xFFF8BBD0, // bubblegum
0xFFAFB42B, 0xFFCDDC39, // lime
0xFF795548, 0xFFA1887F, // mystery flavor
};
FrameLayout mLayout;
int mTapCount;
int mKeyCount;
PathInterpolator mInterpolator = new PathInterpolator(0f, 0f, 0.5f, 1f);
static int newColorIndex() {
return 2 * ((int) (Math.random() * FLAVORS.length / 2));
}
Drawable makeRipple() {
final int idx = newColorIndex();
final ShapeDrawable popbg = new ShapeDrawable(new OvalShape());
popbg.getPaint().setColor(FLAVORS[idx]);
final RippleDrawable ripple = new RippleDrawable(
ColorStateList.valueOf(FLAVORS[idx + 1]),
popbg, null);
return ripple;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLayout = new FrameLayout(this);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(mLayout);
}
@Override
public void onAttachedToWindow() {
final DisplayMetrics dm = getResources().getDisplayMetrics();
final float dp = dm.density;
final int size = (int)
(Math.min(Math.min(dm.widthPixels, dm.heightPixels), 600 * dp) - 100 * dp);
final View stick = new View(this) {
Paint mPaint = new Paint();
Path mShadow = new Path();
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
setWillNotDraw(false);
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRect(0, getHeight() / 2, getWidth(), getHeight());
}
});
}
@Override
public void onDraw(Canvas c) {
final int w = c.getWidth();
final int h = c.getHeight() / 2;
c.translate(0, h);
final GradientDrawable g = new GradientDrawable();
g.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT);
g.setGradientCenter(w * 0.75f, 0);
g.setColors(new int[]{0xFFFFFFFF, 0xFFAAAAAA});
g.setBounds(0, 0, w, h);
g.draw(c);
mPaint.setColor(0xFFAAAAAA);
mShadow.reset();
mShadow.moveTo(0, 0);
mShadow.lineTo(w, 0);
mShadow.lineTo(w, size / 2 + 1.5f * w);
mShadow.lineTo(0, size / 2);
mShadow.close();
c.drawPath(mShadow, mPaint);
}
};
mLayout.addView(stick, new FrameLayout.LayoutParams((int) (32 * dp),
ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER_HORIZONTAL));
stick.setAlpha(0f);
final ImageView im = new ImageView(this);
im.setTranslationZ(20);
im.setScaleX(0);
im.setScaleY(0);
final Drawable platlogo = getDrawable(R.drawable.plat_logo);
platlogo.setAlpha(0);
im.setImageDrawable(platlogo);
im.setBackground(makeRipple());
im.setClickable(true);
final ShapeDrawable highlight = new ShapeDrawable(new OvalShape());
highlight.getPaint().setColor(0x10FFFFFF);
highlight.setBounds((int) (size * .15f), (int) (size * .15f),
(int) (size * .6f), (int) (size * .6f));
im.getOverlay().add(highlight);
im.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mTapCount == 0) {
im.animate()
.translationZ(40)
.scaleX(1)
.scaleY(1)
.setInterpolator(mInterpolator)
.setDuration(700)
.setStartDelay(500)
.start();
final ObjectAnimator a = ObjectAnimator.ofInt(platlogo, "alpha", 0, 255);
a.setInterpolator(mInterpolator);
a.setStartDelay(1000);
a.start();
stick.animate()
.translationZ(20)
.alpha(1)
.setInterpolator(mInterpolator)
.setDuration(700)
.setStartDelay(750)
.start();
} else {
im.setBackground(makeRipple());
}
mTapCount++;
}
});
// Enable hardware keyboard input for TV compatibility.
im.setFocusable(true);
im.requestFocus();
im.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode != KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
++mKeyCount;
if (mKeyCount > 2) {
if (mTapCount > 5) {
im.performLongClick();
} else {
im.performClick();
}
}
return true;
} else {
return false;
}
}
});
mLayout.addView(im, new FrameLayout.LayoutParams(size, size, Gravity.CENTER));
im.animate().scaleX(0.3f).scaleY(0.3f)
.setInterpolator(mInterpolator)
.setDuration(500)
.setStartDelay(800)
.start();
}
}

View File

@ -0,0 +1,46 @@
package io.neoterm.ui.crash
import android.os.Build
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.widget.TextView
import io.neoterm.R
/**
* @author kiva
*/
class CrashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.ui_crash)
setSupportActionBar(findViewById(R.id.crash_toolbar) as Toolbar)
(findViewById(R.id.crash_model) as TextView).text = getString(R.string.crash_model, collectModelInfo())
(findViewById(R.id.crash_app_version) as TextView).text = getString(R.string.crash_app, collectAppInfo())
(findViewById(R.id.crash_details) as TextView).text = collectExceptionInfo()
}
private fun collectExceptionInfo(): String {
val extra = intent.getSerializableExtra("exception")
if (extra != null && extra is Throwable) {
val s = StringBuilder(extra.toString() + "\n\n")
for ((index, trace) in extra.stackTrace.withIndex()) {
s.append(String.format(" #%02d ", index))
s.append("${trace.className}(${trace.fileName}:${trace.lineNumber})(native=${trace.isNativeMethod})")
return s.toString()
}
}
return "are.you.kidding.me.NoExceptionFoundException: This is a bug, please contact me!"
}
fun collectAppInfo(): String {
val pm = packageManager
val info = pm.getPackageInfo(packageName, 0)
return "${info.versionName} (${info.versionCode})"
}
private fun collectModelInfo(): String {
return "${Build.MODEL} (Android ${Build.VERSION.RELEASE})"
}
}

View File

@ -0,0 +1,6 @@
package io.neoterm.ui.floating
/**
* @author kiva
*/
class FloatingNeoTerm

View File

@ -18,8 +18,34 @@ class SettingActivity : AppCompatPreferenceActivity() {
addPreferencesFromResource(R.xml.settings_main)
findPreference(getString(R.string.about)).setOnPreferenceClickListener {
AlertDialog.Builder(this@SettingActivity)
.setTitle(R.string.about)
.setMessage("Hello World!")
.setTitle("为什么我们选择开发NeoTerm?")
.setMessage("安卓上的终端,一直以来就有这样的诟病:" +
"只能用安卓自带的程序,要想用用户体验更好的终端程序," +
"你需要额外配置一堆烦琐的环境," +
"甚至需要自己动手编译符合安卓设备的版本。" +
"就在这时Termux出现了" +
"很完美,一行命令就能安装一个原来想都不敢想的软件," +
"比如MySQL, clang。\n" +
"但用着用着,感觉这样的终端还差点什么," +
"仅仅有丰富的软件包是不够的," +
"Termux在部分功能上可以说是欠妥甚至缺乏" +
"再者安卓并非自带键盘," +
"在小小的屏幕上触摸虚拟键盘来跟命令行打交道实属残忍," +
"但我们又没法强迫所有用户在使用终端的时候再额外接一个键盘," +
"我们只能从终端的层面来解决问题," +
"于是我们开发了这款app" +
"并取名Neo Term(Neo正是new的意思)" +
"并希望它可以改善用户在终端下的体验," +
"让软件包与终端功能两不误。" +
"不可否认开发过程中Termux给了我们很大帮助" +
"不仅提供了很好的terminal-view" +
"还提供了庞大的软件包仓库," +
"这一点我们对原作者表示忠诚的感谢。" +
"日后的开发中,我们会紧跟用户反馈," +
"努力打造安卓上最好用的终端。\n" +
"\n" +
"NeoTerm 开发者们\n" +
"2017.6.19 00:16")
.setPositiveButton(android.R.string.yes, null)
.show()
return@setOnPreferenceClickListener true

View File

@ -1,5 +1,6 @@
package io.neoterm.ui.setup
import android.annotation.SuppressLint
import android.app.Activity
import android.os.Bundle
import android.support.v4.content.ContextCompat
@ -7,6 +8,7 @@ import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.Button
import android.widget.Toast
import com.igalata.bubblepicker.BubblePickerListener
import com.igalata.bubblepicker.adapter.BubblePickerAdapter
import com.igalata.bubblepicker.model.BubbleGradient
@ -29,13 +31,14 @@ import java.util.*
class SetupActivity : AppCompatActivity() {
companion object {
private val DEFAULT_PACKAGES = arrayOf(
"zsh", "aria2", "tmux", "lua", "netcat", "nodejs",
"fish", "make", "gdb", "unrar", "clang", "vim", "emacs",
"curl", "git", "python", "perl", "zip", "p7zip")
"zsh", "neoterm-core", "tmux", "nodejs",
"fish", "make", "gdb", "clang", "vim", "emacs", "nano",
"curl", "git", "python", "p7zip", "oh-my-zsh")
}
lateinit var picker: BubblePicker
lateinit var nextButton: Button
lateinit var toast: Toast
var aptUpdated = false
override fun onCreate(savedInstanceState: Bundle?) {
@ -125,6 +128,7 @@ class SetupActivity : AppCompatActivity() {
.show("apt update")
}
@SuppressLint("ShowToast")
private fun setupBubbles() {
val titles =
if (intent.getBooleanExtra("setup", false))
@ -133,10 +137,11 @@ class SetupActivity : AppCompatActivity() {
randomPackageList()
val colors = resources.obtainTypedArray(R.array.bubble_colors)
toast = Toast.makeText(this, null, Toast.LENGTH_LONG)
picker.bubbleSize = 25
picker.adapter = object : BubblePickerAdapter {
override val totalCount = titles.size
override fun getItem(position: Int): PickerItem {
return PickerItem().apply {
title = titles[position]
@ -148,9 +153,17 @@ class SetupActivity : AppCompatActivity() {
}
picker.listener = object : BubblePickerListener {
override fun onBubbleSelected(item: PickerItem) {
val packageName = item.title
val pm = NeoPackageManager.get()
val packageInfo = pm.getPackageInfo(packageName)
val packageDesc = packageInfo.description
toast.cancel()
toast.setText(packageDesc)
toast.show()
}
override fun onBubbleDeselected(item: PickerItem) {
toast.cancel()
}
}
colors.recycle()

View File

@ -1,5 +1,6 @@
package io.neoterm.ui
package io.neoterm.ui.term
import android.animation.ObjectAnimator
import android.app.Activity
import android.app.AlertDialog
import android.content.*
@ -13,8 +14,10 @@ import android.support.v4.view.ViewCompat
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.*
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.inputmethod.InputMethodManager
import android.widget.ImageButton
import android.widget.Toast
import de.mrapp.android.tabswitcher.*
import io.neoterm.R
import io.neoterm.backend.TerminalSession
@ -25,12 +28,18 @@ import io.neoterm.customize.setup.BaseFileInstaller
import io.neoterm.preference.NeoPermission
import io.neoterm.preference.NeoPreference
import io.neoterm.services.NeoTermService
import io.neoterm.ui.bonus.BonusActivity
import io.neoterm.ui.pm.PackageManagerActivity
import io.neoterm.ui.settings.SettingActivity
import io.neoterm.ui.setup.SetupActivity
import io.neoterm.ui.term.tab.TermSessionChangedCallback
import io.neoterm.ui.term.tab.TermTab
import io.neoterm.ui.term.tab.TermTabDecorator
import io.neoterm.ui.term.tab.TermViewClient
import io.neoterm.ui.term.tab.event.TabCloseEvent
import io.neoterm.ui.term.tab.event.ToggleFullScreenEvent
import io.neoterm.utils.FullScreenHelper
import io.neoterm.view.eks.StatedControlButton
import io.neoterm.view.tab.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -70,31 +79,25 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
setSupportActionBar(toolbar)
fullScreenHelper = FullScreenHelper.injectActivity(this, fullscreen, peekRecreating())
fullScreenHelper.setKeyBoardListener({ isShow, _ ->
var tab: TermTab? = null
fullScreenHelper.setKeyBoardListener(object : FullScreenHelper.KeyBoardListener {
override fun onKeyboardChange(isShow: Boolean, keyboardHeight: Int) {
var tab: TermTab? = null
if (tabSwitcher.selectedTab is TermTab) {
tab = tabSwitcher.selectedTab as TermTab
}
if (tabSwitcher.selectedTab is TermTab) {
tab = tabSwitcher.selectedTab as TermTab
}
if (NeoPreference.loadBoolean(R.string.key_ui_fullscreen, false)
|| NeoPreference.loadBoolean(R.string.key_ui_hide_toolbar, false)) {
tab?.toolbar?.visibility = if (isShow) View.GONE else View.VISIBLE
if (NeoPreference.loadBoolean(R.string.key_ui_fullscreen, false)
|| NeoPreference.loadBoolean(R.string.key_ui_hide_toolbar, false)) {
tab?.toolbar?.visibility = if (isShow) View.GONE else View.VISIBLE
}
}
})
fullScreenToggleButton = object : StatedControlButton("FS", fullscreen) {
override fun onClick(view: View?) {
override fun onClick(view: View) {
super.onClick(view)
if (tabSwitcher.selectedTab is TermTab) {
val tab = tabSwitcher.selectedTab as TermTab
tab.hideIme()
}
NeoPreference.store(R.string.key_ui_fullscreen, super.toggleButton.isChecked)
// FIXME: Cannot toggle full screen mode
// FIXME: We still need to recreate the activity.
// setFullScreenMode(super.toggleButton.isChecked)
this@NeoTermActivity.recreate()
setFullScreenMode(super.toggleButton?.isChecked ?: false)
}
}
@ -116,11 +119,11 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
if (!tabSwitcher.isSwitcherShown) {
if (imm.isActive && tabSwitcher.selectedTab is TermTab) {
val tab = tabSwitcher.selectedTab as TermTab
tab.hideIme()
tab.requireHideIme()
}
tabSwitcher.showSwitcher()
toggleSwitcher(showSwitcher = true)
} else {
tabSwitcher.hideSwitcher()
toggleSwitcher(showSwitcher = false)
}
})
return true
@ -149,7 +152,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
R.id.menu_item_new_session -> {
if (!tabSwitcher.isSwitcherShown) {
tabSwitcher.showSwitcher()
toggleSwitcher(showSwitcher = true)
}
val index = tabSwitcher.count
addNewSession("NeoTerm #" + index, systemShell, createRevealAnimation())
@ -232,7 +235,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
when (keyCode) {
KeyEvent.KEYCODE_BACK -> {
if (event?.action == KeyEvent.ACTION_DOWN && tabSwitcher.isSwitcherShown && tabSwitcher.count > 0) {
tabSwitcher.hideSwitcher()
toggleSwitcher(showSwitcher = false)
return true
}
}
@ -311,7 +314,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
private fun enterSystemShell() {
tabSwitcher.showSwitcher()
toggleSwitcher(showSwitcher = true)
addNewSession("NeoTerm #0", systemShell, createRevealAnimation())
}
@ -324,7 +327,7 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
switchToSession(getStoredCurrentSessionOrLast())
} else {
tabSwitcher.showSwitcher()
toggleSwitcher(showSwitcher = true)
addNewSession("NeoTerm #0", systemShell, createRevealAnimation())
}
}
@ -353,12 +356,13 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
private fun setFullScreenMode(fullScreen: Boolean) {
fullScreenHelper.setFullScreen(fullScreen)
if (fullScreen) {
tabSwitcher.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
} else {
tabSwitcher.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
fullScreenHelper.fullScreen = fullScreen
if (tabSwitcher.selectedTab is TermTab) {
val tab = tabSwitcher.selectedTab as TermTab
tab.requireHideIme()
}
NeoPreference.store(R.string.key_ui_fullscreen, fullScreen)
this@NeoTermActivity.recreate()
}
private fun addNewSession(session: TerminalSession?) {
@ -366,6 +370,14 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
return
}
// Do not add the same session again
// Or app will crash when rotate
val tabCount = tabSwitcher.count
(0..(tabCount - 1))
.map { tabSwitcher.getTab(it) }
.filter { it is TermTab && it.termSession == session }
.forEach { return }
val tab = createTab(session.mSessionName) as TermTab
tab.sessionCallback = session.sessionChangedCallback as TermSessionChangedCallback
tab.viewClient = TermViewClient(this)
@ -490,12 +502,39 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
}
}
private fun toggleSwitcher(showSwitcher: Boolean) {
if (tabSwitcher.count > 0) {
val transparentAnimator = ObjectAnimator.ofFloat(toolbar, View.ALPHA, 1.0f, 0.0f, 1.0f)
transparentAnimator.interpolator = AccelerateDecelerateInterpolator()
transparentAnimator.start()
} else {
val happyCount = NeoPreference.loadInt(NeoPreference.KEY_HAPPY_EGG, 0) + 1
NeoPreference.store(NeoPreference.KEY_HAPPY_EGG, happyCount)
val trigger = NeoPreference.VALUE_HAPPY_EGG_TRIGGER
if (happyCount == trigger / 2) {
val toast = Toast.makeText(this, "Emm...", Toast.LENGTH_LONG)
toast.setGravity(Gravity.CENTER, 0, 0)
toast.show()
} else if (happyCount > trigger) {
NeoPreference.store(NeoPreference.KEY_HAPPY_EGG, 0)
startActivity(Intent(this, BonusActivity::class.java))
}
return
}
if (showSwitcher) {
tabSwitcher.showSwitcher()
} else {
tabSwitcher.hideSwitcher()
}
}
@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun onTabCloseEvent(tabCloseEvent: TabCloseEvent) {
val tab = tabCloseEvent.termTab
tabSwitcher.showSwitcher()
toggleSwitcher(showSwitcher = true)
tabSwitcher.removeTab(tab)
if (tabSwitcher.count > 1) {
@ -510,4 +549,11 @@ class NeoTermActivity : AppCompatActivity(), ServiceConnection, SharedPreference
switchToSession(tabSwitcher.getTab(index))
}
}
@Suppress("unused", "UNUSED_PARAMETER")
@Subscribe(threadMode = ThreadMode.MAIN)
fun onToggleFullScreenEvent(toggleFullScreenEvent: ToggleFullScreenEvent) {
val fullScreen = fullScreenHelper.fullScreen
setFullScreenMode(!fullScreen)
}
}

View File

@ -1,4 +1,4 @@
package io.neoterm.view.tab
package io.neoterm.ui.term.tab
import android.content.ClipData
import android.content.ClipboardManager

View File

@ -1,15 +1,15 @@
package io.neoterm.view.tab
package io.neoterm.ui.term.tab
import android.content.Context
import android.graphics.Color
import android.support.v7.widget.Toolbar
import android.view.inputmethod.InputMethodManager
import de.mrapp.android.tabswitcher.Tab
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.customize.color.ColorSchemeManager
import io.neoterm.customize.color.NeoColorScheme
import io.neoterm.preference.NeoPreference
import io.neoterm.ui.term.tab.event.TabCloseEvent
import io.neoterm.ui.term.tab.event.ToggleFullScreenEvent
import org.greenrobot.eventbus.EventBus
/**
@ -38,12 +38,14 @@ class TermTab(title: CharSequence) : Tab(title) {
}
fun updateTitle(title: String) {
this.title = title
toolbar?.title = title
if (NeoPreference.loadBoolean(R.string.key_ui_suggestions, true)) {
viewClient?.updateSuggestions(title)
} else {
viewClient?.removeSuggestions()
if (title.isNotEmpty()) {
this.title = title
toolbar?.title = title
if (NeoPreference.loadBoolean(R.string.key_ui_suggestions, true)) {
viewClient?.updateSuggestions(title)
} else {
viewClient?.removeSuggestions()
}
}
}
@ -51,12 +53,12 @@ class TermTab(title: CharSequence) : Tab(title) {
viewClient?.sessionFinished = true
}
fun requiredCloseTab() {
hideIme()
fun requireCloseTab() {
requireHideIme()
EventBus.getDefault().post(TabCloseEvent(this))
}
fun hideIme() {
fun requireHideIme() {
val terminalView = viewClient?.termView
if (terminalView != null) {
val imm = terminalView.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
@ -65,4 +67,12 @@ class TermTab(title: CharSequence) : Tab(title) {
}
}
}
fun requireToggleFullScreen() {
EventBus.getDefault().post(ToggleFullScreenEvent())
}
fun requirePaste() {
viewClient?.termView?.pasteFromClipboard()
}
}

View File

@ -1,4 +1,4 @@
package io.neoterm.view.tab
package io.neoterm.ui.term.tab
import android.content.Context
import android.os.Bundle
@ -11,7 +11,7 @@ import de.mrapp.android.tabswitcher.TabSwitcherDecorator
import io.neoterm.R
import io.neoterm.customize.color.ColorSchemeManager
import io.neoterm.preference.NeoPreference
import io.neoterm.ui.NeoTermActivity
import io.neoterm.ui.term.NeoTermActivity
import io.neoterm.utils.TerminalUtils
import io.neoterm.view.ExtraKeysView
import io.neoterm.view.TerminalView

View File

@ -0,0 +1,204 @@
package io.neoterm.ui.term.tab
import android.content.Context
import android.media.AudioManager
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.inputmethod.InputMethodManager
import io.neoterm.R
import io.neoterm.backend.KeyHandler
import io.neoterm.backend.TerminalSession
import io.neoterm.customize.eks.EksKeysManager
import io.neoterm.preference.NeoPreference
import io.neoterm.view.ExtraKeysView
import io.neoterm.view.TerminalView
import io.neoterm.view.TerminalViewClient
/**
* @author kiva
*/
class TermViewClient(val context: Context) : TerminalViewClient {
private var mVirtualControlKeyDown: Boolean = false
private var mVirtualFnKeyDown: Boolean = false
private var lastTitle: String = ""
var sessionFinished: Boolean = false
var termTab: TermTab? = null
var termView: TerminalView? = null
var extraKeysView: ExtraKeysView? = null
override fun onScale(scale: Float): Float {
if (scale < 0.9f || scale > 1.1f) {
val increase = scale > 1f
changeFontSize(increase)
return 1.0f
}
return scale
}
override fun onSingleTapUp(e: MotionEvent?) {
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.showSoftInput(termView, InputMethodManager.SHOW_IMPLICIT)
}
override fun shouldBackButtonBeMappedToEscape(): Boolean {
return NeoPreference.loadBoolean(R.string.key_generaL_backspace_map_to_esc, false)
}
override fun copyModeChanged(copyMode: Boolean) {
// TODO
}
override fun onKeyDown(keyCode: Int, e: KeyEvent?, session: TerminalSession?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_ENTER -> {
if (e?.action == KeyEvent.ACTION_DOWN && sessionFinished) {
termTab?.requireCloseTab()
return true
}
return false
}
}
if (e != null && e.isCtrlPressed && e.isAltPressed) {
// Get the unmodified code point:
val unicodeChar = e.getUnicodeChar(0).toChar()
if (unicodeChar == 'f'/* full screen */) {
termTab?.requireToggleFullScreen()
} else if (unicodeChar == 'v') {
termTab?.requirePaste()
} else if (unicodeChar == '+' || e.getUnicodeChar(KeyEvent.META_SHIFT_ON).toChar() == '+') {
// We also check for the shifted char here since shift may be required to produce '+',
// see https://github.com/termux/termux-api/issues/2
changeFontSize(true)
} else if (unicodeChar == '-') {
changeFontSize(false)
}
}
return false
}
override fun onKeyUp(keyCode: Int, e: KeyEvent?): Boolean {
return handleVirtualKeys(keyCode, e, false)
}
override fun readControlKey(): Boolean {
return (extraKeysView != null && extraKeysView!!.readControlButton()) || mVirtualControlKeyDown
}
override fun readAltKey(): Boolean {
return (extraKeysView != null && extraKeysView!!.readAltButton()) || mVirtualFnKeyDown
}
override fun onCodePoint(codePoint: Int, ctrlDown: Boolean, session: TerminalSession?): Boolean {
if (mVirtualFnKeyDown) {
var resultingKeyCode: Int = -1
var resultingCodePoint: Int = -1
var altDown = false
val lowerCase = Character.toLowerCase(codePoint)
when (lowerCase.toChar()) {
// Arrow keys.
'w' -> resultingKeyCode = KeyEvent.KEYCODE_DPAD_UP
'a' -> resultingKeyCode = KeyEvent.KEYCODE_DPAD_LEFT
's' -> resultingKeyCode = KeyEvent.KEYCODE_DPAD_DOWN
'd' -> resultingKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT
// Page up and down.
'p' -> resultingKeyCode = KeyEvent.KEYCODE_PAGE_UP
'n' -> resultingKeyCode = KeyEvent.KEYCODE_PAGE_DOWN
// Some special keys:
't' -> resultingKeyCode = KeyEvent.KEYCODE_TAB
'i' -> resultingKeyCode = KeyEvent.KEYCODE_INSERT
'h' -> resultingCodePoint = '~'.toInt()
// Special characters to input.
'u' -> resultingCodePoint = '_'.toInt()
'l' -> resultingCodePoint = '|'.toInt()
// Function keys.
'1', '2', '3', '4', '5', '6', '7', '8', '9' -> resultingKeyCode = codePoint - '1'.toInt() + KeyEvent.KEYCODE_F1
'0' -> resultingKeyCode = KeyEvent.KEYCODE_F10
// Other special keys.
'e' -> resultingCodePoint = 27 /*Escape*/
'.' -> resultingCodePoint = 28 /*^.*/
'b' // alt+b, jumping backward in readline.
, 'f' // alf+f, jumping forward in readline.
, 'x' // alt+x, common in emacs.
-> {
resultingCodePoint = lowerCase
altDown = true
}
// Volume control.
'v' -> {
resultingCodePoint = -1
val audio = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
audio.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, AudioManager.USE_DEFAULT_STREAM_TYPE, AudioManager.FLAG_SHOW_UI)
}
}
if (resultingKeyCode != -1) {
if (session != null) {
val term = session.emulator
session.write(KeyHandler.getCode(resultingKeyCode, 0, term.isCursorKeysApplicationMode, term.isKeypadApplicationMode))
}
} else if (resultingCodePoint != -1) {
session?.writeCodePoint(altDown, resultingCodePoint)
}
return true
}
return false
}
override fun onLongPress(event: MotionEvent?): Boolean {
// TODO
return false
}
private fun handleVirtualKeys(keyCode: Int, event: KeyEvent?, down: Boolean): Boolean {
if (event == null) {
return false
}
val inputDevice = event.device
if (inputDevice != null && inputDevice.keyboardType == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
return false
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
mVirtualControlKeyDown = down
return true
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mVirtualFnKeyDown = down
return true
}
return false
}
fun updateSuggestions(title: String?, force: Boolean = false) {
if (extraKeysView == null || title == null || title.isEmpty()) {
return
}
if (lastTitle != title || force) {
removeSuggestions()
EksKeysManager.showShortcutKeys(title, extraKeysView)
extraKeysView?.updateButtons()
lastTitle = title
}
}
fun removeSuggestions() {
extraKeysView?.clearUserDefinedButton()
}
private fun changeFontSize(increase: Boolean) {
val changedSize = (if (increase) 1 else -1) * 2
val fontSize = termView!!.textSize + changedSize
termView!!.textSize = fontSize
NeoPreference.store(NeoPreference.KEY_FONT_SIZE, fontSize)
}
}

View File

@ -0,0 +1,9 @@
package io.neoterm.ui.term.tab.event
import io.neoterm.ui.term.tab.TermTab
/**
* @author kiva
*/
class TabCloseEvent(var termTab: TermTab)

View File

@ -0,0 +1,6 @@
package io.neoterm.ui.term.tab.event
/**
* @author kiva
*/
class ToggleFullScreenEvent()

View File

@ -0,0 +1,26 @@
package io.neoterm.utils
import android.content.Intent
import android.os.Process
import io.neoterm.App
import io.neoterm.ui.crash.CrashActivity
/**
* @author kiva
*/
object CrashHandler : Thread.UncaughtExceptionHandler {
private lateinit var defaultHandler: Thread.UncaughtExceptionHandler
fun init() {
defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(t: Thread?, e: Throwable?) {
val intent = Intent(App.get(), CrashActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra("exception", e)
App.get().startActivity(intent)
Process.killProcess(Process.myPid())
}
}

View File

@ -1,126 +0,0 @@
package io.neoterm.utils;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
/**
* Helper class to "adjustResize" Activity when we are in full screen mode and check IME status.
* Android Bug 5497: https://code.google.com/p/android/issues/detail?id=5497
*/
public class FullScreenHelper {
public static FullScreenHelper injectActivity(Activity activity, boolean fullScreen, boolean recreate) {
return new FullScreenHelper(activity, fullScreen, recreate);
}
public interface KeyBoardListener {
/**
* call back
*
* @param isShow true is show else hidden
* @param keyboardHeight keyboard height
*/
void onKeyboardChange(boolean isShow, int keyboardHeight);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private int mOriginHeight;
private int mPreHeight;
private KeyBoardListener mKeyBoardListener;
private boolean fullScreen;
private boolean shouldSkipFirst;
public void setKeyBoardListener(KeyBoardListener mKeyBoardListener) {
this.mKeyBoardListener = mKeyBoardListener;
}
private FullScreenHelper(Activity activity, final boolean fullScreen, boolean recreate) {
this.fullScreen = fullScreen;
this.shouldSkipFirst = recreate;
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
if (FullScreenHelper.this.fullScreen) {
possiblyResizeChildOfContent();
}
monitorImeStatus();
}
});
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
}
private void monitorImeStatus() {
int currHeight = mChildOfContent.getHeight();
if (currHeight == 0 && shouldSkipFirst) {
// First time
return;
}
shouldSkipFirst = false;
boolean hasChange = false;
if (mPreHeight == 0) {
mPreHeight = currHeight;
mOriginHeight = currHeight;
} else {
if (mPreHeight != currHeight) {
hasChange = true;
mPreHeight = currHeight;
} else {
hasChange = false;
}
}
if (hasChange) {
int keyboardHeight = 0;
boolean keyBoardIsShowing;
if (Math.abs(mOriginHeight - currHeight) < 100) {
//hidden
keyBoardIsShowing = false;
} else {
//show
keyboardHeight = mOriginHeight - currHeight;
keyBoardIsShowing = true;
}
if (mKeyBoardListener != null) {
mKeyBoardListener.onKeyboardChange(keyBoardIsShowing, keyboardHeight);
}
}
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
int currentHeightLayoutHeight;
if (usableHeightNow != usableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard / 4)) {
// keyboard probably just became visible
currentHeightLayoutHeight = usableHeightSansKeyboard - heightDifference;
} else {
// keyboard probably just became hidden
currentHeightLayoutHeight = usableHeightSansKeyboard;
}
frameLayoutParams.height = currentHeightLayoutHeight;
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return r.bottom - r.top;
}
public void setFullScreen(boolean fullScreen) {
this.fullScreen = fullScreen;
}
}

View File

@ -0,0 +1,120 @@
package io.neoterm.utils
import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.view.View
import android.view.ViewTreeObserver
import android.widget.FrameLayout
/**
* Helper class to "adjustResize" Activity when we are in full screen mode and check IME status.
* Android Bug 5497: https://code.google.com/p/android/issues/detail?id=5497
*/
class FullScreenHelper private constructor(activity: Activity, var fullScreen: Boolean, private var shouldSkipFirst: Boolean) {
interface KeyBoardListener {
/**
* call back
* @param isShow true is show else hidden
* *
* @param keyboardHeight keyboard height
*/
fun onKeyboardChange(isShow: Boolean, keyboardHeight: Int)
}
private val mChildOfContent: View
private var usableHeightPrevious: Int = 0
private val frameLayoutParams: FrameLayout.LayoutParams
private var mOriginHeight: Int = 0
private var mPreHeight: Int = 0
private var mKeyBoardListener: KeyBoardListener? = null
fun setKeyBoardListener(mKeyBoardListener: KeyBoardListener) {
this.mKeyBoardListener = mKeyBoardListener
}
init {
val content = activity.findViewById(android.R.id.content) as FrameLayout
mChildOfContent = content.getChildAt(0)
mChildOfContent.viewTreeObserver.addOnGlobalLayoutListener {
if (this@FullScreenHelper.fullScreen) {
possiblyResizeChildOfContent()
}
monitorImeStatus()
}
frameLayoutParams = mChildOfContent.layoutParams as FrameLayout.LayoutParams
}
private fun monitorImeStatus() {
val currHeight = mChildOfContent.height
if (currHeight == 0 && shouldSkipFirst) {
// First time
return
}
shouldSkipFirst = false
var hasChange = false
if (mPreHeight == 0) {
mPreHeight = currHeight
mOriginHeight = currHeight
} else {
if (mPreHeight != currHeight) {
hasChange = true
mPreHeight = currHeight
} else {
hasChange = false
}
}
if (hasChange) {
var keyboardHeight = 0
val keyBoardIsShowing: Boolean
if (Math.abs(mOriginHeight - currHeight) < 100) {
//hidden
keyBoardIsShowing = false
} else {
//show
keyboardHeight = mOriginHeight - currHeight
keyBoardIsShowing = true
}
if (mKeyBoardListener != null) {
mKeyBoardListener!!.onKeyboardChange(keyBoardIsShowing, keyboardHeight)
}
}
}
private fun possiblyResizeChildOfContent() {
val usableHeightNow = computeUsableHeight()
val currentHeightLayoutHeight: Int
if (usableHeightNow != usableHeightPrevious) {
val usableHeightSansKeyboard = mChildOfContent.rootView.height
val heightDifference = usableHeightSansKeyboard - usableHeightNow
if (heightDifference > usableHeightSansKeyboard / 4) {
// keyboard probably just became visible
currentHeightLayoutHeight = usableHeightSansKeyboard - heightDifference
} else {
// keyboard probably just became hidden
currentHeightLayoutHeight = usableHeightSansKeyboard
}
frameLayoutParams.height = currentHeightLayoutHeight
mChildOfContent.requestLayout()
usableHeightPrevious = usableHeightNow
}
}
private fun computeUsableHeight(): Int {
val r = Rect()
mChildOfContent.getWindowVisibleDisplayFrame(r)
return r.bottom - r.top
}
companion object {
fun injectActivity(activity: Activity, fullScreen: Boolean, recreate: Boolean): FullScreenHelper {
return FullScreenHelper(activity, fullScreen, recreate)
}
}
}

View File

@ -0,0 +1,52 @@
package io.neoterm.utils
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
class Shaker(context: Context) : SensorEventListener {
companion object {
private val SHAKE_SENSITIVITY = 14
}
private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private var mOnShakeListener: OnShakeListener? = null
fun setOnShakeListener(onShakeListener: OnShakeListener) {
mOnShakeListener = onShakeListener
}
fun onResume() {
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_NORMAL)
}
fun onPause() {
mSensorManager.unregisterListener(this)
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
}
override fun onSensorChanged(event: SensorEvent) {
val sensorType = event.sensor.type
//values[0]:X, values[1]Y, values[2]Z
val values = event.values
if (sensorType == Sensor.TYPE_ACCELEROMETER) {
//这里可以调节摇一摇的灵敏度
if (Math.abs(values[0]) > SHAKE_SENSITIVITY || Math.abs(values[1]) > SHAKE_SENSITIVITY || Math.abs(values[2]) > SHAKE_SENSITIVITY) {
if (mOnShakeListener != null) {
mOnShakeListener!!.onShake()
}
}
}
}
interface OnShakeListener {
fun onShake()
}
}

View File

@ -1,214 +0,0 @@
package io.neoterm.view;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.ToggleButton;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import io.neoterm.customize.eks.EksConfig;
import io.neoterm.customize.eks.EksConfigParser;
import io.neoterm.customize.font.FontManager;
import io.neoterm.preference.NeoTermPath;
import io.neoterm.utils.FileUtils;
import io.neoterm.view.eks.ControlButton;
import io.neoterm.view.eks.ExtraButton;
import io.neoterm.view.eks.StatedControlButton;
import static io.neoterm.view.eks.ExtraButton.KEY_ARROW_DOWN;
import static io.neoterm.view.eks.ExtraButton.KEY_ARROW_LEFT;
import static io.neoterm.view.eks.ExtraButton.KEY_ARROW_RIGHT;
import static io.neoterm.view.eks.ExtraButton.KEY_ARROW_UP;
import static io.neoterm.view.eks.ExtraButton.KEY_CTRL;
import static io.neoterm.view.eks.ExtraButton.KEY_END;
import static io.neoterm.view.eks.ExtraButton.KEY_ESC;
import static io.neoterm.view.eks.ExtraButton.KEY_HOME;
import static io.neoterm.view.eks.ExtraButton.KEY_PAGE_DOWN;
import static io.neoterm.view.eks.ExtraButton.KEY_PAGE_UP;
import static io.neoterm.view.eks.ExtraButton.KEY_TAB;
/**
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboard.
*/
public final class ExtraKeysView extends LinearLayout {
public static final StatedControlButton CTRL = new StatedControlButton(KEY_CTRL);
public static final ControlButton ESC = new ControlButton(KEY_ESC);
public static final ControlButton TAB = new ControlButton(KEY_TAB);
public static final ControlButton PAGE_UP = new ControlButton(KEY_PAGE_UP);
public static final ControlButton PAGE_DOWN = new ControlButton(KEY_PAGE_DOWN);
public static final ControlButton HOME = new ControlButton(KEY_HOME);
public static final ControlButton END = new ControlButton(KEY_END);
public static final ControlButton ARROW_UP = new ControlButton(KEY_ARROW_UP);
public static final ControlButton ARROW_DOWN = new ControlButton(KEY_ARROW_DOWN);
public static final ControlButton ARROW_LEFT = new ControlButton(KEY_ARROW_LEFT);
public static final ControlButton ARROW_RIGHT = new ControlButton(KEY_ARROW_RIGHT);
public static final String DEFAULT_FILE_CONTENT = "version " + EksConfigParser.PARSER_VERSION + "\n" +
"program default\n" +
"define - false\n" +
"define / false\n" +
"define | false\n";
public static int NORMAL_TEXT_COLOR = 0xFFFFFFFF;
public static int SELECTED_TEXT_COLOR = 0xFF80DEEA;
private List<ExtraButton> builtinExtraKeys;
private List<ExtraButton> userDefinedExtraKeys;
private LinearLayout lineOne;
private LinearLayout lineTwo;
private Typeface typeface;
public ExtraKeysView(Context context, AttributeSet attrs) {
super(context, attrs);
setGravity(Gravity.TOP);
setOrientation(VERTICAL);
HorizontalScrollView scrollOne = new HorizontalScrollView(context);
HorizontalScrollView scrollTwo = new HorizontalScrollView(context);
loadDefaultBuiltinExtraKeys();
loadDefaultUserDefinedExtraKeys();
lineOne = initLine(scrollOne);
lineTwo = initLine(scrollTwo);
addView(scrollOne);
addView(scrollTwo);
updateButtons();
}
private LinearLayout initLine(HorizontalScrollView scroll) {
scroll.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
scroll.setFillViewport(true);
scroll.setHorizontalScrollBarEnabled(false);
LinearLayout line = new LinearLayout(getContext());
line.setGravity(Gravity.START);
line.setOrientation(LinearLayout.HORIZONTAL);
scroll.addView(line);
return line;
}
public boolean readControlButton() {
return CTRL.readState();
}
public boolean readAltButton() {
return false;
}
public void addUserDefinedButton(ExtraButton button) {
addButton(userDefinedExtraKeys, button);
}
public void addBuiltinButton(ExtraButton button) {
addButton(builtinExtraKeys, button);
}
private void addButton(List<ExtraButton> buttons, ExtraButton button) {
if (!buttons.contains(button)) {
buttons.add(button);
}
}
public void clearUserDefinedButton() {
userDefinedExtraKeys.clear();
}
public void loadDefaultUserDefinedExtraKeys() {
userDefinedExtraKeys = new ArrayList<>(7);
File defaultFile = new File(NeoTermPath.EKS_DEFAULT_FILE);
if (!defaultFile.exists()) {
generateDefaultFile(defaultFile);
}
clearUserDefinedButton();
try {
EksConfigParser parser = new EksConfigParser();
parser.setInput(defaultFile);
EksConfig config = parser.parse();
userDefinedExtraKeys.addAll(config.getShortcutKeys());
} catch (Exception e) {
e.printStackTrace();
}
}
private void generateDefaultFile(File defaultFile) {
FileUtils.INSTANCE.writeFile(defaultFile, DEFAULT_FILE_CONTENT.getBytes());
}
void loadDefaultBuiltinExtraKeys() {
builtinExtraKeys = new ArrayList<>(7);
builtinExtraKeys.clear();
builtinExtraKeys.add(ESC);
builtinExtraKeys.add(CTRL);
builtinExtraKeys.add(TAB);
builtinExtraKeys.add(ARROW_UP);
builtinExtraKeys.add(ARROW_DOWN);
builtinExtraKeys.add(ARROW_LEFT);
builtinExtraKeys.add(ARROW_RIGHT);
builtinExtraKeys.add(PAGE_UP);
builtinExtraKeys.add(PAGE_DOWN);
builtinExtraKeys.add(HOME);
builtinExtraKeys.add(END);
}
public void updateButtons() {
lineOne.removeAllViews();
lineTwo.removeAllViews();
for (final ExtraButton extraButton : userDefinedExtraKeys) {
addExtraButton(lineOne, extraButton);
}
for (final ExtraButton extraButton : builtinExtraKeys) {
addExtraButton(lineTwo, extraButton);
}
}
private void addExtraButton(LinearLayout contentView, final ExtraButton extraButton) {
final Button button;
if (extraButton instanceof StatedControlButton) {
StatedControlButton btn = ((StatedControlButton) extraButton);
button = btn.toggleButton = new ToggleButton(getContext(), null, android.R.attr.buttonBarButtonStyle);
button.setClickable(true);
if (btn.initState) {
btn.toggleButton.setChecked(true);
btn.toggleButton.setTextColor(SELECTED_TEXT_COLOR);
}
} else {
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
}
button.setTypeface(typeface);
button.setText(extraButton.buttonText);
button.setTextColor(NORMAL_TEXT_COLOR);
button.setAllCaps(false);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
button.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
View root = getRootView();
extraButton.onClick(root);
}
});
contentView.addView(button);
}
public void setTextColor(int textColor) {
NORMAL_TEXT_COLOR = textColor;
updateButtons();
}
public void setTypeface(Typeface typeface) {
this.typeface = typeface;
updateButtons();
}
}

View File

@ -0,0 +1,223 @@
package io.neoterm.view
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import android.widget.ToggleButton
import java.io.File
import java.util.ArrayList
import io.neoterm.customize.eks.EksConfig
import io.neoterm.customize.eks.EksConfigParser
import io.neoterm.customize.font.FontManager
import io.neoterm.preference.NeoTermPath
import io.neoterm.utils.FileUtils
import io.neoterm.view.eks.ControlButton
import io.neoterm.view.eks.ExtraButton
import io.neoterm.view.eks.StatedControlButton
import io.neoterm.view.eks.ExtraButton.Companion.KEY_ARROW_DOWN
import io.neoterm.view.eks.ExtraButton.Companion.KEY_ARROW_LEFT
import io.neoterm.view.eks.ExtraButton.Companion.KEY_ARROW_RIGHT
import io.neoterm.view.eks.ExtraButton.Companion.KEY_ARROW_UP
import io.neoterm.view.eks.ExtraButton.Companion.KEY_CTRL
import io.neoterm.view.eks.ExtraButton.Companion.KEY_END
import io.neoterm.view.eks.ExtraButton.Companion.KEY_ESC
import io.neoterm.view.eks.ExtraButton.Companion.KEY_HOME
import io.neoterm.view.eks.ExtraButton.Companion.KEY_PAGE_DOWN
import io.neoterm.view.eks.ExtraButton.Companion.KEY_PAGE_UP
import io.neoterm.view.eks.ExtraButton.Companion.KEY_TAB
/**
* A view showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
* keyboard.
*/
class ExtraKeysView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var builtinExtraKeys: MutableList<ExtraButton>? = null
private var userDefinedExtraKeys: MutableList<ExtraButton>? = null
private val lineOne: LinearLayout
private val lineTwo: LinearLayout
private var typeface: Typeface? = null
init {
gravity = Gravity.TOP
orientation = LinearLayout.VERTICAL
val scrollOne = HorizontalScrollView(context)
val scrollTwo = HorizontalScrollView(context)
loadDefaultBuiltinExtraKeys()
loadDefaultUserDefinedExtraKeys()
lineOne = initLine(scrollOne)
lineTwo = initLine(scrollTwo)
addView(scrollOne)
addView(scrollTwo)
updateButtons()
}
private fun initLine(scroll: HorizontalScrollView): LinearLayout {
scroll.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
scroll.isFillViewport = true
scroll.isHorizontalScrollBarEnabled = false
val line = LinearLayout(context)
line.gravity = Gravity.START
line.orientation = LinearLayout.HORIZONTAL
scroll.addView(line)
return line
}
fun readControlButton(): Boolean {
return CTRL.readState()
}
fun readAltButton(): Boolean {
return ALT.readState()
}
fun addUserDefinedButton(button: ExtraButton) {
addButton(userDefinedExtraKeys, button)
}
fun addBuiltinButton(button: ExtraButton) {
addButton(builtinExtraKeys, button)
}
private fun addButton(buttons: MutableList<ExtraButton>?, button: ExtraButton) {
if (buttons != null && !buttons.contains(button)) {
buttons.add(button)
}
}
fun clearUserDefinedButton() {
userDefinedExtraKeys!!.clear()
}
fun loadDefaultUserDefinedExtraKeys() {
userDefinedExtraKeys = ArrayList<ExtraButton>(7)
val defaultFile = File(NeoTermPath.EKS_DEFAULT_FILE)
if (!defaultFile.exists()) {
generateDefaultFile(defaultFile)
}
clearUserDefinedButton()
try {
val parser = EksConfigParser()
parser.setInput(defaultFile)
val config = parser.parse()
userDefinedExtraKeys!!.addAll(config.shortcutKeys)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun generateDefaultFile(defaultFile: File) {
FileUtils.writeFile(defaultFile, DEFAULT_FILE_CONTENT.toByteArray())
}
internal fun loadDefaultBuiltinExtraKeys() {
builtinExtraKeys = ArrayList<ExtraButton>(7)
builtinExtraKeys!!.clear()
builtinExtraKeys!!.add(ESC)
builtinExtraKeys!!.add(CTRL)
builtinExtraKeys!!.add(ALT)
builtinExtraKeys!!.add(TAB)
builtinExtraKeys!!.add(ARROW_UP)
builtinExtraKeys!!.add(ARROW_DOWN)
builtinExtraKeys!!.add(ARROW_LEFT)
builtinExtraKeys!!.add(ARROW_RIGHT)
builtinExtraKeys!!.add(PAGE_UP)
builtinExtraKeys!!.add(PAGE_DOWN)
builtinExtraKeys!!.add(HOME)
builtinExtraKeys!!.add(END)
}
fun updateButtons() {
lineOne.removeAllViews()
lineTwo.removeAllViews()
for (extraButton in userDefinedExtraKeys!!) {
addExtraButton(lineOne, extraButton)
}
for (extraButton in builtinExtraKeys!!) {
addExtraButton(lineTwo, extraButton)
}
}
private fun addExtraButton(contentView: LinearLayout, extraButton: ExtraButton) {
val button: Button
if (extraButton is StatedControlButton) {
val btn = extraButton
val toggleButton = ToggleButton(context, null, android.R.attr.buttonBarButtonStyle)
btn.toggleButton = toggleButton
button = toggleButton
button.setClickable(true)
if (btn.initState) {
btn.toggleButton!!.isChecked = true
btn.toggleButton!!.setTextColor(SELECTED_TEXT_COLOR)
}
} else {
button = Button(context, null, android.R.attr.buttonBarButtonStyle)
}
button.typeface = typeface
button.text = extraButton.buttonText
button.setTextColor(NORMAL_TEXT_COLOR)
button.setAllCaps(false)
button.setOnClickListener {
button.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
val root = rootView
extraButton.onClick(root)
}
contentView.addView(button)
}
fun setTextColor(textColor: Int) {
NORMAL_TEXT_COLOR = textColor
updateButtons()
}
fun setTypeface(typeface: Typeface?) {
this.typeface = typeface
updateButtons()
}
companion object {
@SuppressLint("StaticFieldLeak")
val CTRL = StatedControlButton(ExtraButton.KEY_CTRL)
@SuppressLint("StaticFieldLeak")
val ALT = StatedControlButton(ExtraButton.KEY_ALT)
val ESC = ControlButton(ExtraButton.KEY_ESC)
val TAB = ControlButton(ExtraButton.KEY_TAB)
val PAGE_UP = ControlButton(ExtraButton.KEY_PAGE_UP)
val PAGE_DOWN = ControlButton(ExtraButton.KEY_PAGE_DOWN)
val HOME = ControlButton(ExtraButton.KEY_HOME)
val END = ControlButton(ExtraButton.KEY_END)
val ARROW_UP = ControlButton(ExtraButton.KEY_ARROW_UP)
val ARROW_DOWN = ControlButton(ExtraButton.KEY_ARROW_DOWN)
val ARROW_LEFT = ControlButton(ExtraButton.KEY_ARROW_LEFT)
val ARROW_RIGHT = ControlButton(ExtraButton.KEY_ARROW_RIGHT)
val DEFAULT_FILE_CONTENT = "version " + EksConfigParser.PARSER_VERSION + "\n" +
"program default\n" +
"define - false\n" +
"define / false\n" +
"define | false\n" +
"define $ false\n" +
"define < false\n" +
"define > false\n"
var NORMAL_TEXT_COLOR = 0xFFFFFFFF.toInt()
var SELECTED_TEXT_COLOR = 0xFF80DEEA.toInt()
}
}

View File

@ -1,111 +0,0 @@
package io.neoterm.view;
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
/** A combination of {@link GestureDetector} and {@link ScaleGestureDetector}. */
final class GestureAndScaleRecognizer {
public interface Listener {
boolean onSingleTapUp(MotionEvent e);
boolean onDoubleTap(MotionEvent e);
boolean onScroll(MotionEvent e2, float dx, float dy);
boolean onFling(MotionEvent e, float velocityX, float velocityY);
boolean onScale(float focusX, float focusY, float scale);
boolean onDown(float x, float y);
boolean onUp(MotionEvent e);
void onLongPress(MotionEvent e);
}
private final GestureDetector mGestureDetector;
private final ScaleGestureDetector mScaleDetector;
final Listener mListener;
boolean isAfterLongPress;
public GestureAndScaleRecognizer(Context context, Listener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) {
return mListener.onScroll(e2, dx, dy);
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return mListener.onFling(e2, velocityX, velocityY);
}
@Override
public boolean onDown(MotionEvent e) {
return mListener.onDown(e.getX(), e.getY());
}
@Override
public void onLongPress(MotionEvent e) {
mListener.onLongPress(e);
isAfterLongPress = true;
}
}, null, true /* ignoreMultitouch */);
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return mListener.onSingleTapUp(e);
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return mListener.onDoubleTap(e);
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return true;
}
});
mScaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
return mListener.onScale(detector.getFocusX(), detector.getFocusY(), detector.getScaleFactor());
}
});
}
public void onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mScaleDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isAfterLongPress = false;
break;
case MotionEvent.ACTION_UP:
if (!isAfterLongPress) {
// This behaviour is desired when in e.g. vim with mouse events, where we do not
// want to move the cursor when lifting finger after a long press.
mListener.onUp(event);
}
break;
}
}
public boolean isInProgress() {
return mScaleDetector.isInProgress();
}
}

View File

@ -0,0 +1,95 @@
package io.neoterm.view
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
/** A combination of [GestureDetector] and [ScaleGestureDetector]. */
internal class GestureAndScaleRecognizer(context: Context, val mListener: Listener) {
interface Listener {
fun onSingleTapUp(e: MotionEvent): Boolean
fun onDoubleTap(e: MotionEvent): Boolean
fun onScroll(e2: MotionEvent, dx: Float, dy: Float): Boolean
fun onFling(e: MotionEvent, velocityX: Float, velocityY: Float): Boolean
fun onScale(focusX: Float, focusY: Float, scale: Float): Boolean
fun onDown(x: Float, y: Float): Boolean
fun onUp(e: MotionEvent): Boolean
fun onLongPress(e: MotionEvent)
}
private val mGestureDetector: GestureDetector
private val mScaleDetector: ScaleGestureDetector
var isAfterLongPress: Boolean = false
init {
mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, dx: Float, dy: Float): Boolean {
return mListener.onScroll(e2, dx, dy)
}
override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
return mListener.onFling(e2, velocityX, velocityY)
}
override fun onDown(e: MotionEvent): Boolean {
return mListener.onDown(e.x, e.y)
}
override fun onLongPress(e: MotionEvent) {
mListener.onLongPress(e)
isAfterLongPress = true
}
}, null, true /* ignoreMultitouch */)
mGestureDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
return mListener.onSingleTapUp(e)
}
override fun onDoubleTap(e: MotionEvent): Boolean {
return mListener.onDoubleTap(e)
}
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
return true
}
})
mScaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
return mListener.onScale(detector.focusX, detector.focusY, detector.scaleFactor)
}
})
}
fun onTouchEvent(event: MotionEvent) {
mGestureDetector.onTouchEvent(event)
mScaleDetector.onTouchEvent(event)
when (event.action) {
MotionEvent.ACTION_DOWN -> isAfterLongPress = false
MotionEvent.ACTION_UP -> if (!isAfterLongPress) {
// This behaviour is desired when in e.g. vim with mouse events, where we do not
// want to move the cursor when lifting finger after a long press.
mListener.onUp(event)
}
}
}
val isInProgress: Boolean
get() = mScaleDetector.isInProgress
}

View File

@ -565,12 +565,7 @@ public final class TerminalView extends View {
if (action == MotionEvent.ACTION_DOWN) showContextMenu();
return true;
} else if (ev.isButtonPressed(MotionEvent.BUTTON_TERTIARY)) {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
}
pasteFromClipboard();
} else if (mEmulator.isMouseTrackingActive()) { // BUTTON_PRIMARY.
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
@ -589,6 +584,15 @@ public final class TerminalView extends View {
return true;
}
public void pasteFromClipboard() {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
}
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (LOG_KEY_EVENTS)
@ -892,12 +896,7 @@ public final class TerminalView extends View {
mTermSession.clipboardText(selectedText);
break;
case 2:
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
CharSequence paste = clipData.getItemAt(0).coerceToText(getContext());
if (!TextUtils.isEmpty(paste)) mEmulator.paste(paste.toString());
}
pasteFromClipboard();
break;
case 3:
showContextMenu();

View File

@ -1,20 +0,0 @@
package io.neoterm.view.eks;
import android.view.View;
import io.neoterm.view.ExtraKeysView;
/**
* @author kiva
*/
public class ControlButton extends ExtraButton {
public ControlButton(String text) {
buttonText = text;
}
@Override
public void onClick(View view) {
sendKey(view, buttonText);
}
}

View File

@ -0,0 +1,19 @@
package io.neoterm.view.eks
import android.view.View
import io.neoterm.view.ExtraKeysView
/**
* @author kiva
*/
open class ControlButton(text: String) : ExtraButton() {
init {
buttonText = text
}
override fun onClick(view: View) {
ExtraButton.Companion.sendKey(view, buttonText!!)
}
}

View File

@ -1,82 +0,0 @@
package io.neoterm.view.eks;
import android.view.KeyEvent;
import android.view.View;
import io.neoterm.R;
import io.neoterm.backend.TerminalSession;
import io.neoterm.view.TerminalView;
/**
* @author kiva
*/
public abstract class ExtraButton implements View.OnClickListener {
public static final String KEY_ESC = "Esc";
public static final String KEY_TAB = "Tab";
public static final String KEY_CTRL = "Ctrl";
public static final String KEY_PAGE_UP = "PgUp";
public static final String KEY_PAGE_DOWN = "PgDn";
public static final String KEY_HOME = "Home";
public static final String KEY_END = "End";
public static final String KEY_ARROW_UP = "";
public static final String KEY_ARROW_DOWN = "";
public static final String KEY_ARROW_LEFT = "";
public static final String KEY_ARROW_RIGHT = "";
public String buttonText;
@Override
public abstract void onClick(View view);
public static void sendKey(View view, String keyName) {
int keyCode = 0;
String chars = null;
switch (keyName) {
case KEY_ESC:
keyCode = KeyEvent.KEYCODE_ESCAPE;
break;
case KEY_TAB:
keyCode = KeyEvent.KEYCODE_TAB;
break;
case KEY_ARROW_UP:
keyCode = KeyEvent.KEYCODE_DPAD_UP;
break;
case KEY_ARROW_LEFT:
keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case KEY_ARROW_RIGHT:
keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
break;
case KEY_ARROW_DOWN:
keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
break;
case KEY_PAGE_UP:
keyCode = KeyEvent.KEYCODE_PAGE_UP;
break;
case KEY_PAGE_DOWN:
keyCode = KeyEvent.KEYCODE_PAGE_DOWN;
break;
case KEY_HOME:
keyCode = KeyEvent.KEYCODE_MOVE_HOME;
break;
case KEY_END:
keyCode = KeyEvent.KEYCODE_MOVE_END;
break;
case "":
chars = "-";
break;
default:
chars = keyName;
}
if (keyCode > 0) {
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyCode));
} else {
TerminalView terminalView = (TerminalView) view.findViewById(R.id.terminal_view);
TerminalSession session = terminalView.getCurrentSession();
if (session != null) session.write(chars);
}
}
}

View File

@ -0,0 +1,62 @@
package io.neoterm.view.eks
import android.view.KeyEvent
import android.view.View
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.view.TerminalView
/**
* @author kiva
*/
abstract class ExtraButton : View.OnClickListener {
var buttonText: String? = null
abstract override fun onClick(view: View)
companion object {
val KEY_ESC = "Esc"
val KEY_TAB = "Tab"
val KEY_CTRL = "Ctrl"
val KEY_ALT = "Alt"
val KEY_PAGE_UP = "PgUp"
val KEY_PAGE_DOWN = "PgDn"
val KEY_HOME = "Home"
val KEY_END = "End"
val KEY_ARROW_UP = ""
val KEY_ARROW_DOWN = ""
val KEY_ARROW_LEFT = ""
val KEY_ARROW_RIGHT = ""
fun sendKey(view: View, keyName: String) {
var keyCode = 0
var chars: String? = null
when (keyName) {
KEY_ESC -> keyCode = KeyEvent.KEYCODE_ESCAPE
KEY_TAB -> keyCode = KeyEvent.KEYCODE_TAB
KEY_ARROW_UP -> keyCode = KeyEvent.KEYCODE_DPAD_UP
KEY_ARROW_LEFT -> keyCode = KeyEvent.KEYCODE_DPAD_LEFT
KEY_ARROW_RIGHT -> keyCode = KeyEvent.KEYCODE_DPAD_RIGHT
KEY_ARROW_DOWN -> keyCode = KeyEvent.KEYCODE_DPAD_DOWN
KEY_PAGE_UP -> keyCode = KeyEvent.KEYCODE_PAGE_UP
KEY_PAGE_DOWN -> keyCode = KeyEvent.KEYCODE_PAGE_DOWN
KEY_HOME -> keyCode = KeyEvent.KEYCODE_MOVE_HOME
KEY_END -> keyCode = KeyEvent.KEYCODE_MOVE_END
"" -> chars = "-"
else -> chars = keyName
}
if (keyCode > 0) {
view.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
view.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode))
} else {
val terminalView = view.findViewById(R.id.terminal_view) as TerminalView
val session = terminalView.currentSession
session?.write(chars)
}
}
}
}

View File

@ -1,44 +0,0 @@
package io.neoterm.view.eks;
import android.view.View;
import android.widget.ToggleButton;
import io.neoterm.view.ExtraKeysView;
/**
* @author kiva
*/
public class StatedControlButton extends ControlButton {
public ToggleButton toggleButton;
public boolean initState;
public StatedControlButton(String text, boolean initState) {
super(text);
this.initState = initState;
}
public StatedControlButton(String text) {
this(text, false);
}
@Override
public void onClick(View view) {
setStatus(toggleButton.isChecked());
}
public void setStatus(boolean status) {
toggleButton.setChecked(status);
toggleButton.setTextColor(status ? ExtraKeysView.SELECTED_TEXT_COLOR : ExtraKeysView.NORMAL_TEXT_COLOR);
}
public boolean readState() {
if (toggleButton.isPressed()) return true;
boolean result = toggleButton.isChecked();
if (result) {
toggleButton.setChecked(false);
toggleButton.setTextColor(ExtraKeysView.NORMAL_TEXT_COLOR);
}
return result;
}
}

View File

@ -0,0 +1,33 @@
package io.neoterm.view.eks
import android.view.View
import android.widget.ToggleButton
import io.neoterm.view.ExtraKeysView
/**
* @author kiva
*/
open class StatedControlButton @JvmOverloads constructor(text: String, var initState: Boolean = false) : ControlButton(text) {
var toggleButton: ToggleButton? = null
override fun onClick(view: View) {
setStatus(toggleButton!!.isChecked)
}
fun setStatus(status: Boolean) {
toggleButton!!.isChecked = status
toggleButton!!.setTextColor(if (status) ExtraKeysView.SELECTED_TEXT_COLOR else ExtraKeysView.NORMAL_TEXT_COLOR)
}
fun readState(): Boolean {
if (toggleButton!!.isPressed) return true
val result = toggleButton!!.isChecked
if (result) {
toggleButton!!.isChecked = false
toggleButton!!.setTextColor(ExtraKeysView.NORMAL_TEXT_COLOR)
}
return result
}
}

View File

@ -1,30 +0,0 @@
package io.neoterm.view.eks;
import android.view.View;
import io.neoterm.view.ExtraKeysView;
/**
* @author kiva
*/
public class TextButton extends ExtraButton {
private boolean withEnter = false;
public TextButton(String text) {
this(text, false);
}
public TextButton(String text, boolean withEnter) {
this.buttonText = text;
this.withEnter = withEnter;
}
@Override
public void onClick(View view) {
sendKey(view, buttonText);
if (withEnter) {
sendKey(view, "\n");
}
}
}

View File

@ -0,0 +1,25 @@
package io.neoterm.view.eks
import android.view.View
import io.neoterm.view.ExtraKeysView
/**
* @author kiva
*/
class TextButton @JvmOverloads constructor(text: String, withEnter: Boolean = false) : ExtraButton() {
private var withEnter = false
init {
this.buttonText = text
this.withEnter = withEnter
}
override fun onClick(view: View) {
ExtraButton.sendKey(view, buttonText!!)
if (withEnter) {
ExtraButton.sendKey(view, "\n")
}
}
}

View File

@ -1,7 +0,0 @@
package io.neoterm.view.tab
/**
* @author kiva
*/
class TabCloseEvent(var termTab: TermTab)

View File

@ -1,124 +0,0 @@
package io.neoterm.view.tab
import android.content.Context
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.inputmethod.InputMethodManager
import io.neoterm.R
import io.neoterm.backend.TerminalSession
import io.neoterm.customize.eks.EksKeysManager
import io.neoterm.preference.NeoPreference
import io.neoterm.view.ExtraKeysView
import io.neoterm.view.TerminalView
import io.neoterm.view.TerminalViewClient
/**
* @author kiva
*/
class TermViewClient(val context: Context) : TerminalViewClient {
private var mVirtualControlKeyDown: Boolean = false
private var mVirtualFnKeyDown: Boolean = false
private var lastTitle: String = ""
var sessionFinished: Boolean = false
var termTab: TermTab? = null
var termView: TerminalView? = null
var extraKeysView: ExtraKeysView? = null
override fun onScale(scale: Float): Float {
if (scale < 0.9f || scale > 1.1f) {
val increase = scale > 1f
val changedSize = (if (increase) 1 else -1) * 2
val fontSize = termView!!.textSize + changedSize
termView!!.textSize = fontSize
NeoPreference.store(NeoPreference.KEY_FONT_SIZE, fontSize)
return 1.0f
}
return scale
}
override fun onSingleTapUp(e: MotionEvent?) {
(context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.showSoftInput(termView, InputMethodManager.SHOW_IMPLICIT)
}
override fun shouldBackButtonBeMappedToEscape(): Boolean {
return NeoPreference.loadBoolean(R.string.key_generaL_backspace_map_to_esc, false)
}
override fun copyModeChanged(copyMode: Boolean) {
// TODO
}
override fun onKeyDown(keyCode: Int, e: KeyEvent?, session: TerminalSession?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_ENTER -> {
if (e?.action == KeyEvent.ACTION_DOWN && sessionFinished) {
termTab?.requiredCloseTab()
return true
}
return false
}
else -> return false
}
}
override fun onKeyUp(keyCode: Int, e: KeyEvent?): Boolean {
return handleVirtualKeys(keyCode, e, false)
}
override fun readControlKey(): Boolean {
return (extraKeysView != null && extraKeysView!!.readControlButton()) || mVirtualControlKeyDown
}
override fun readAltKey(): Boolean {
return (extraKeysView != null && extraKeysView!!.readAltButton()) || mVirtualFnKeyDown
}
override fun onCodePoint(codePoint: Int, ctrlDown: Boolean, session: TerminalSession?): Boolean {
// TODO
return false
}
override fun onLongPress(event: MotionEvent?): Boolean {
// TODO
return false
}
private fun handleVirtualKeys(keyCode: Int, event: KeyEvent?, down: Boolean): Boolean {
if (event == null) {
return false
}
val inputDevice = event.device
if (inputDevice != null && inputDevice.keyboardType == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
return false
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
mVirtualControlKeyDown = down
return true
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mVirtualFnKeyDown = down
return true
}
return false
}
fun updateSuggestions(title: String?, force: Boolean = false) {
if (extraKeysView == null || title == null || title.isEmpty()) {
return
}
if (lastTitle != title || force) {
removeSuggestions()
EksKeysManager.showShortcutKeys(title, extraKeysView)
extraKeysView?.updateButtons()
lastTitle = title
}
}
fun removeSuggestions() {
extraKeysView?.clearUserDefinedButton()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/terminal_background"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/crash_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimaryDark"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
app:popupTheme="@style/ThemeOverlay.AppCompat.Dark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/text_margin"
android:orientation="vertical">
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/crash_tips"
android:textColor="@color/colorAccent" />
<TextView
android:id="@+id/crash_model"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/textColorSecondary" />
<TextView
android:id="@+id/crash_app_version"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/textColorSecondary" />
<TextView
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/text_margin"
android:gravity="start|top"
android:text="@string/crash_stack_trace"
android:textColor="@color/textColor" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/crash_details"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/text_margin"
android:layout_marginStart="@dimen/text_margin"
android:gravity="start|top"
android:textColor="@color/textColor" />
</ScrollView>
</LinearLayout>
</LinearLayout>

View File

@ -18,7 +18,7 @@
android:id="@+id/package_loading_progress_bar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_height="4dp"
android:visibility="gone" />
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Neo Term</string>
<string name="app_name">NeoTerm</string>
<string name="about">关于</string>
<string name="copy_text">复制</string>
<string name="general_settings">通用设置</string>
@ -71,6 +71,14 @@
<string name="setup_hello">发现</string>
<string name="setup_info">你好NeoTerm</string>
<string name="setup_info2">轻触以选择你的最爱</string>
<string name="setup_next">下一步</string>
<string name="setup_next">安装</string>
<string name="discovery">发现</string>
<string name="crash_model">设备: %s</string>
<string name="crash_app">应用: %s</string>
<string name="crash_stack_trace">堆栈信息</string>
<string name="crash_tips">我们正努力让这个 Activity 永不见天日…</string>
<string name="service_status_text">%d 个实例</string>
<string name="service_lock_acquired"> (永不休眠)</string>
<string name="service_acquire_lock">取得休眠锁</string>
<string name="service_release_lock">释放休眠锁</string>
</resources>

View File

@ -1,16 +1,20 @@
<resources>
<string name="app_name">Neo Term</string>
<string name="app_name">NeoTerm</string>
<string name="copy_text">Copy</string>
<string name="paste_text">Paste</string>
<string name="text_selection_more">More</string>
<string name="toggle_tab_switcher_menu_item">Toggle switcher</string>
<string name="new_session">New session</string>
<string name="service_status_text">%d session(s)</string>
<string name="service_lock_acquired"> (Wake Locked)</string>
<string name="service_acquire_lock">Acquire Lock</string>
<string name="service_release_lock">Release Lock</string>
<string name="setup_hello">Discovery</string>
<string name="setup_info">Hello, NeoTerm</string>
<string name="setup_info2">Tap to choose your favorites</string>
<string name="setup_next">Next</string>
<string name="setup_next">Install</string>
<string name="about">About</string>
<string name="discovery">Discovery</string>
@ -28,7 +32,7 @@
<string name="shell_not_found">Shell %s not found, please install it first.</string>
<string name="fullscreen_mode_changed">FullScreen mode changed, please restart NeoTerm.</string>
<string name="permission_denied">NeoTerm cannot get essential permissions, exiting.</string>
<string name="error">Error</string>
<string name="error">Oops!</string>
<string name="use_system_shell">System Shell</string>
<string name="retry">Retry</string>
<string name="source_changed">APT source changed, you may get it updated by executing: apt update</string>
@ -41,6 +45,11 @@
<string name="install_font">Install Font</string>
<string name="install_color">Install Color Scheme</string>
<string name="crash_model">Model: %s</string>
<string name="crash_app">App: %s</string>
<string name="crash_tips">We are trying to deprecate this Activity…</string>
<string name="crash_stack_trace">Stack Trace</string>
<string name="installer_message">Installing</string>
<string name="installer_install_zsh_required">We are about to install oh-my-zsh and switch your login shell to zsh</string>

View File

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.1.2-4'
ext.kotlin_version = '1.1.3'
repositories {
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
jcenter()