Added signature editor

This commit is contained in:
M66B 2020-02-23 12:02:42 +01:00
parent 24a6a1fd8e
commit baab4cba36
8 changed files with 366 additions and 117 deletions

View File

@ -94,6 +94,12 @@
</intent-filter>
</activity>
<activity
android:name=".ActivitySignature"
android:exported="false"
android:launchMode="singleTask"
android:parentActivityName=".ActivitySetup" />
<activity
android:name=".ActivityWidget"
android:exported="true">

View File

@ -0,0 +1,232 @@
package eu.faircode.email;
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
Copyright 2018-2020 by Marcel Bokhorst (M66B)
*/
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ImageSpan;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import java.io.FileNotFoundException;
import java.io.InputStream;
public class ActivitySignature extends ActivityBase {
private EditTextCompose etText;
private BottomNavigationView style_bar;
private BottomNavigationView bottom_navigation;
private static final int REQUEST_IMAGE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setSubtitle(getString(R.string.title_edit_signature));
setContentView(R.layout.activity_signature);
etText = findViewById(R.id.etText);
style_bar = findViewById(R.id.style_bar);
bottom_navigation = findViewById(R.id.bottom_navigation);
etText.setSelectionListener(new EditTextCompose.ISelection() {
@Override
public void onSelected(boolean selection) {
style_bar.setVisibility(selection ? View.VISIBLE : View.GONE);
}
});
style_bar.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
return onActionStyle(item.getItemId());
}
});
bottom_navigation.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.action_insert_image:
insertImage();
return true;
case R.id.action_delete:
delete();
return true;
case R.id.action_save:
save();
return true;
default:
return false;
}
}
});
style_bar.setVisibility(View.GONE);
setResult(RESULT_CANCELED, new Intent());
load();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
load();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
try {
switch (requestCode) {
case REQUEST_IMAGE:
if (resultCode == RESULT_OK && data != null)
onImageSelected(data.getData());
break;
}
} catch (Throwable ex) {
Log.e(ex);
}
}
private void load() {
String html = getIntent().getStringExtra("html");
if (html == null)
etText.setText(null);
else
etText.setText(HtmlHelper.fromHtml(html, new Html.ImageGetter() {
@Override
public Drawable getDrawable(String source) {
return getDrawableByUri(Uri.parse(source));
}
}, null));
}
private void delete() {
Intent result = new Intent();
result.putExtra("html", (String) null);
setResult(RESULT_OK, result);
finish();
}
private void save() {
etText.clearComposingText();
String html = HtmlHelper.toHtml(etText.getText());
Intent result = new Intent();
result.putExtra("html", html);
setResult(RESULT_OK, result);
finish();
}
private void insertImage() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
Helper.openAdvanced(intent);
startActivityForResult(intent, REQUEST_IMAGE);
}
private boolean onActionStyle(int action) {
Log.i("Style action=" + action);
if (action == R.id.menu_link) {
Uri uri = null;
ClipboardManager cbm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
if (cbm != null && cbm.hasPrimaryClip()) {
String link = cbm.getPrimaryClip().getItemAt(0).coerceToText(this).toString();
uri = Uri.parse(link);
if (uri.getScheme() == null)
uri = null;
}
View view = LayoutInflater.from(this).inflate(R.layout.dialog_insert_link, null);
EditText etLink = view.findViewById(R.id.etLink);
TextView tvInsecure = view.findViewById(R.id.tvInsecure);
etLink.setText(uri == null ? "https://" : uri.toString());
tvInsecure.setVisibility(View.GONE);
new AlertDialog.Builder(this)
.setView(view)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String link = etLink.getText().toString();
StyleHelper.apply(R.id.menu_link, etText, link);
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return true;
} else
return StyleHelper.apply(action, etText);
}
private void onImageSelected(Uri uri) {
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
int start = etText.getSelectionStart();
SpannableStringBuilder ssb = new SpannableStringBuilder(etText.getText());
ssb.insert(start, " ");
ImageSpan is = new ImageSpan(getDrawableByUri(uri), uri.toString(), ImageSpan.ALIGN_BASELINE);
ssb.setSpan(is, start, start + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
etText.setText(ssb);
}
private Drawable getDrawableByUri(Uri uri) {
Drawable d;
try {
Log.i("Loading image source=" + uri);
InputStream inputStream = getContentResolver().openInputStream(uri);
d = Drawable.createFromStream(inputStream, uri.toString());
} catch (FileNotFoundException ex) {
Log.w(ex);
d = getResources().getDrawable(R.drawable.baseline_broken_image_24);
}
int w = Helper.dp2pixels(this, d.getIntrinsicWidth());
int h = Helper.dp2pixels(this, d.getIntrinsicHeight());
d.setBounds(0, 0, w, h);
return d;
}
}

View File

@ -21,17 +21,14 @@ package eu.faircode.email;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Dialog;
import android.app.NotificationManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
@ -57,7 +54,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.Group;
import androidx.lifecycle.Lifecycle;
@ -86,8 +82,7 @@ public class FragmentIdentity extends FragmentBase {
private EditText etDisplay;
private ViewButtonColor btnColor;
private TextView tvColorPro;
private EditText etSignature;
private Button btnHtml;
private Button btnSignature;
private Button btnAdvanced;
private Spinner spProvider;
@ -135,12 +130,13 @@ public class FragmentIdentity extends FragmentBase {
private int auth = EmailService.AUTH_TYPE_PASSWORD;
private String provider = null;
private String certificate = null;
private String signature = null;
private boolean saving = false;
private static final int REQUEST_COLOR = 1;
private static final int REQUEST_SAVE = 2;
private static final int REQUEST_DELETE = 3;
private static final int REQUEST_HTML = 4;
private static final int REQUEST_SIGNATURE = 4;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -172,8 +168,7 @@ public class FragmentIdentity extends FragmentBase {
etDisplay = view.findViewById(R.id.etDisplay);
btnColor = view.findViewById(R.id.btnColor);
tvColorPro = view.findViewById(R.id.tvColorPro);
etSignature = view.findViewById(R.id.etSignature);
btnHtml = view.findViewById(R.id.btnHtml);
btnSignature = view.findViewById(R.id.btnSignature);
btnAdvanced = view.findViewById(R.id.btnAdvanced);
spProvider = view.findViewById(R.id.spProvider);
@ -337,36 +332,12 @@ public class FragmentIdentity extends FragmentBase {
Helper.linkPro(tvColorPro);
etSignature.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable editable) {
SpannableStringBuilder ssb = new SpannableStringBuilder(editable);
Helper.clearComposingText(ssb);
if (TextUtils.isEmpty(editable.toString()))
etSignature.setTag(null);
else
etSignature.setTag(HtmlHelper.toHtml(ssb));
}
});
btnHtml.setOnClickListener(new View.OnClickListener() {
btnSignature.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Bundle args = new Bundle();
args.putString("html", (String) etSignature.getTag());
FragmentDialogHtml fragment = new FragmentDialogHtml();
fragment.setArguments(args);
fragment.setTargetFragment(FragmentIdentity.this, REQUEST_HTML);
fragment.show(getParentFragmentManager(), "identity:html");
Intent intent = new Intent(getContext(), ActivitySignature.class);
intent.putExtra("html", signature);
startActivityForResult(intent, REQUEST_SIGNATURE);
}
});
@ -589,8 +560,6 @@ public class FragmentIdentity extends FragmentBase {
name = hint.toString();
}
etSignature.clearComposingText();
Bundle args = new Bundle();
args.putLong("id", id);
args.putString("name", name);
@ -614,7 +583,7 @@ public class FragmentIdentity extends FragmentBase {
args.putString("realm", etRealm.getText().toString());
args.putString("fingerprint", cbTrust.isChecked() ? (String) cbTrust.getTag() : null);
args.putBoolean("use_ip", cbUseIp.isChecked());
args.putString("signature", (String) etSignature.getTag());
args.putString("signature", signature);
args.putBoolean("synchronize", cbSynchronize.isChecked());
args.putBoolean("primary", cbPrimary.isChecked());
@ -1006,7 +975,7 @@ public class FragmentIdentity extends FragmentBase {
outState.putInt("fair:advanced", grpAdvanced.getVisibility());
outState.putInt("fair:auth", auth);
outState.putString("fair:authprovider", provider);
outState.putString("fair:html", (String) etSignature.getTag());
outState.putString("fair:html", signature);
super.onSaveInstanceState(outState);
}
@ -1033,9 +1002,7 @@ public class FragmentIdentity extends FragmentBase {
etDisplay.setText(identity == null ? null : identity.display);
btnColor.setColor(identity == null ? null : identity.color);
String signature = (identity == null ? null : identity.signature);
etSignature.setText(TextUtils.isEmpty(signature) ? null : HtmlHelper.fromHtml(signature));
etSignature.setTag(signature);
signature = (identity == null ? null : identity.signature);
etHost.setText(identity == null ? null : identity.host);
rgEncryption.check(identity != null && identity.starttls ? R.id.radio_starttls : R.id.radio_ssl);
@ -1092,7 +1059,7 @@ public class FragmentIdentity extends FragmentBase {
grpAdvanced.setVisibility(savedInstanceState.getInt("fair:advanced"));
auth = savedInstanceState.getInt("fair:auth");
provider = savedInstanceState.getString("fair:authprovider");
etSignature.setTag(savedInstanceState.getString("fair:html"));
signature = savedInstanceState.getString("fair:html");
}
Helper.setViewsEnabled(view, true);
@ -1257,9 +1224,9 @@ public class FragmentIdentity extends FragmentBase {
if (resultCode == RESULT_OK)
onDelete();
break;
case REQUEST_HTML:
if (resultCode == RESULT_OK && data != null)
onHtml(data.getBundleExtra("args"));
case REQUEST_SIGNATURE:
if (resultCode == RESULT_OK)
onHtml(data.getExtras());
break;
}
} catch (Throwable ex) {
@ -1302,46 +1269,6 @@ public class FragmentIdentity extends FragmentBase {
}
private void onHtml(Bundle args) {
String html = args.getString("html");
etSignature.setText(HtmlHelper.fromHtml(html));
etSignature.setTag(html);
}
public static class FragmentDialogHtml extends FragmentDialogBase {
private EditText etHtml;
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putString("fair:html", etHtml.getText().toString());
super.onSaveInstanceState(outState);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
String html;
if (savedInstanceState == null)
html = getArguments().getString("html");
else
html = savedInstanceState.getString("fair:html");
View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_signature, null);
etHtml = dview.findViewById(R.id.etHtml);
etHtml.setText(html);
return new AlertDialog.Builder(getContext())
.setTitle(R.string.title_edit_html)
.setView(dview)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String html = etHtml.getText().toString();
getArguments().putString("html", html);
sendResult(RESULT_OK);
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
}
signature = args.getString("html");
}
}

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="eu.faircode.email.ActivitySetup">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="eu.faircode.email.BehaviorBottomPadding">
<eu.faircode.email.EditTextCompose
android:id="@+id/etText"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="6dp"
android:background="@null"
android:fontFamily="monospace"
android:gravity="top"
android:hint="@string/title_edit_signature_text"
android:imeOptions="actionDone"
android:inputType="textCapSentences|textMultiLine|textAutoCorrect"
android:isScrollContainer="true"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toTopOf="@+id/style_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/style_bar"
android:layout_width="0dp"
android:layout_height="36dp"
android:background="?attr/colorActionBackground"
app:itemIconTint="@color/action_foreground"
app:itemTextColor="@color/action_foreground"
app:labelVisibilityMode="unlabeled"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/action_signature_style" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:itemIconTint="@color/action_foreground"
app:itemTextColor="@color/action_foreground"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/action_signature" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -149,39 +149,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvColorHint" />
<TextView
android:id="@+id/tvSignature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_account_signature"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvColorPro" />
<EditText
android:id="@+id/etSignature"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:hint="@string/title_optional"
android:inputType="textCapSentences|textMultiLine|textAutoCorrect"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSignature" />
<Button
android:id="@+id/btnHtml"
android:id="@+id/btnSignature"
style="@style/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:minWidth="0dp"
android:minHeight="0dp"
android:tag="disable"
android:text="@string/title_edit_html"
android:text="@string/title_edit_signature"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etSignature" />
app:layout_constraintTop_toBottomOf="@id/tvColorPro" />
<Button
android:id="@+id/btnAdvanced"
@ -193,7 +172,7 @@
android:minHeight="0dp"
android:text="@string/title_setup_advanced"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnHtml" />
app:layout_constraintTop_toBottomOf="@id/btnSignature" />
<!--- provider -->
@ -727,7 +706,7 @@
app:constraint_referenced_ids="
tvName,etName,tvEmail,etEmail,tvDisplay,etDisplay,
tvColor,btnColor,tvColorHint,tvColorPro,
tvSignature,etSignature,btnHtml,
btnSignature,
btnAdvanced,btnSave" />
<androidx.constraintlayout.widget.Group

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_insert_image"
android:icon="@drawable/baseline_image_24"
android:title="@string/title_edit_signature_image" />
<item
android:id="@+id/action_delete"
android:icon="@drawable/baseline_delete_24"
android:title="@string/title_delete" />
<item
android:id="@+id/action_save"
android:icon="@drawable/baseline_save_alt_24"
android:title="@string/title_save" />
</menu>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_bold"
android:icon="@drawable/baseline_format_bold_24"
android:title="@string/title_style_bold" />
<item
android:id="@+id/menu_italic"
android:icon="@drawable/baseline_format_italic_24"
android:title="@string/title_style_italic" />
<item
android:id="@+id/menu_underline"
android:icon="@drawable/baseline_format_underlined_24"
android:title="@string/title_style_underline" />
<item
android:id="@+id/menu_size"
android:icon="@drawable/baseline_format_size_24"
android:title="@string/title_style_size" />
<item
android:id="@+id/menu_link"
android:icon="@drawable/baseline_insert_link_24"
android:title="@string/title_style_link" />
</menu>

View File

@ -501,7 +501,6 @@
<string name="title_account_name_hint">Used to differentiate folders</string>
<string name="title_account_interval_hint">Frequency of refreshing the connection for push messages or frequency of checking for new messages</string>
<string name="title_account_partial_fetch_hint">Disable this only in case of empty messages or corrupt attachments</string>
<string name="title_account_signature">Signature text</string>
<string name="title_color">Color</string>
<string name="title_account_ondemand">Synchronize manually</string>
<string name="title_account_notify">Separate notifications</string>
@ -867,6 +866,10 @@
<string name="title_previous">Previous</string>
<string name="title_next">Next</string>
<string name="title_edit_signature">Edit signature</string>
<string name="title_edit_signature_text">Signature text</string>
<string name="title_edit_signature_image">Insert image</string>
<string name="title_answer_caption">Edit template</string>
<string name="title_answer_reply">Reply template</string>
<string name="title_answer_name">Template name</string>