Compose autocomplete improvements
This commit is contained in:
@@ -28,6 +28,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
@@ -52,6 +53,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
@@ -96,6 +98,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private static final int MEDIA_RESULT=717;
|
||||
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
private static final int AUTOCOMPLETE_ACCOUNT_RESULT=779;
|
||||
private static final String TAG="ComposeFragment";
|
||||
|
||||
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
|
||||
@@ -262,6 +265,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
@Override
|
||||
public void onIconChanged(int icon){
|
||||
emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN);
|
||||
if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){
|
||||
contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
|
||||
if(icon==PopupKeyboard.ICON_HIDDEN)
|
||||
showAutocomplete();
|
||||
else
|
||||
hideAutocomplete();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -294,7 +304,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
updateVisibilityIcon();
|
||||
|
||||
autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID);
|
||||
autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected);
|
||||
autocompleteViewController.setCompletionSelectedListener(new ComposeAutocompleteViewController.AutocompleteListener(){
|
||||
@Override
|
||||
public void onCompletionSelected(String completion){
|
||||
onAutocompleteOptionSelected(completion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetEmojiPanelOpen(boolean open){
|
||||
if(open!=emojiKeyboard.isVisible())
|
||||
emojiKeyboard.toggleKeyboardPopup(mainEditText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLaunchAccountSearch(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
}
|
||||
});
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
autocompleteView.setVisibility(View.INVISIBLE);
|
||||
bottomBar.addView(autocompleteView, 0, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(56)));
|
||||
@@ -315,6 +343,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
mediaViewController.onSaveInstanceState(outState);
|
||||
outState.putBoolean("hasSpoiler", hasSpoiler);
|
||||
outState.putSerializable("visibility", statusVisibility);
|
||||
if(currentAutocompleteSpan!=null){
|
||||
Editable e=mainEditText.getText();
|
||||
outState.putInt("autocompleteStart", e.getSpanStart(currentAutocompleteSpan));
|
||||
outState.putInt("autocompleteEnd", e.getSpanEnd(currentAutocompleteSpan));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -471,6 +504,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
updateMediaPollStates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewStateRestored(Bundle savedInstanceState){
|
||||
super.onViewStateRestored(savedInstanceState);
|
||||
if(savedInstanceState!=null && savedInstanceState.containsKey("autocompleteStart")){
|
||||
int start=savedInstanceState.getInt("autocompleteStart"), end=savedInstanceState.getInt("autocompleteEnd");
|
||||
currentAutocompleteSpan=new ComposeAutocompleteSpan();
|
||||
mainEditText.getText().setSpan(currentAutocompleteSpan, start, end, Editable.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
startAutocomplete(currentAutocompleteSpan);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(editingStatus==null ? R.menu.compose : R.menu.compose_edit, menu);
|
||||
@@ -537,6 +581,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
private void onCustomEmojiClick(Emoji emoji){
|
||||
if(getActivity().getCurrentFocus() instanceof EditText edit){
|
||||
if(edit==mainEditText && currentAutocompleteSpan!=null && autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){
|
||||
Editable text=mainEditText.getText();
|
||||
int start=text.getSpanStart(currentAutocompleteSpan);
|
||||
int end=text.getSpanEnd(currentAutocompleteSpan);
|
||||
finishAutocomplete();
|
||||
text.replace(start, end, ':'+emoji.shortcode+':');
|
||||
return;
|
||||
}
|
||||
int start=edit.getSelectionStart();
|
||||
String prefix=start>0 && !Character.isWhitespace(edit.getText().charAt(start-1)) ? " :" : ":";
|
||||
edit.getText().replace(start, edit.getSelectionEnd(), prefix+emoji.shortcode+':');
|
||||
@@ -549,6 +601,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Background, R.attr.colorM3Primary, 0.11f);
|
||||
getToolbar().setBackgroundColor(color);
|
||||
setStatusBarColor(color);
|
||||
setNavigationBarColor(color);
|
||||
bottomBar.setBackgroundColor(color);
|
||||
}
|
||||
|
||||
@@ -694,6 +747,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
String attID=result.getString("attachment");
|
||||
String text=result.getString("text");
|
||||
mediaViewController.setAltTextByID(attID, text);
|
||||
}else if(reqCode==AUTOCOMPLETE_ACCOUNT_RESULT && success){
|
||||
Account acc=Parcels.unwrap(result.getParcelable("selectedAccount"));
|
||||
if(currentAutocompleteSpan==null)
|
||||
return;
|
||||
Editable e=mainEditText.getText();
|
||||
int start=e.getSpanStart(currentAutocompleteSpan);
|
||||
int end=e.getSpanEnd(currentAutocompleteSpan);
|
||||
e.removeSpan(currentAutocompleteSpan);
|
||||
e.replace(start, end, '@'+acc.acct+' ');
|
||||
finishAutocomplete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,6 +968,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Editable e=mainEditText.getText();
|
||||
String spanText=e.toString().substring(e.getSpanStart(span), e.getSpanEnd(span));
|
||||
autocompleteViewController.setText(spanText);
|
||||
showAutocomplete();
|
||||
}
|
||||
|
||||
private void finishAutocomplete(){
|
||||
if(currentAutocompleteSpan==null)
|
||||
return;
|
||||
autocompleteViewController.setText(null);
|
||||
currentAutocompleteSpan=null;
|
||||
hideAutocomplete();
|
||||
}
|
||||
|
||||
private void showAutocomplete(){
|
||||
UiUtils.beginLayoutTransition(bottomBar);
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
bottomBar.getLayoutParams().height=ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
@@ -913,11 +988,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
autocompleteDivider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void finishAutocomplete(){
|
||||
if(currentAutocompleteSpan==null)
|
||||
return;
|
||||
autocompleteViewController.setText(null);
|
||||
currentAutocompleteSpan=null;
|
||||
private void hideAutocomplete(){
|
||||
UiUtils.beginLayoutTransition(bottomBar);
|
||||
bottomBar.getLayoutParams().height=V.dp(48);
|
||||
bottomBar.requestLayout();
|
||||
@@ -930,8 +1001,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
int start=e.getSpanStart(currentAutocompleteSpan);
|
||||
int end=e.getSpanEnd(currentAutocompleteSpan);
|
||||
e.replace(start, end, text+" ");
|
||||
mainEditText.setSelection(start+text.length()+1);
|
||||
finishAutocomplete();
|
||||
InputConnection conn=mainEditText.getCurrentInputConnection();
|
||||
if(conn!=null)
|
||||
conn.finishComposingText();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -136,6 +136,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom());
|
||||
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(16));
|
||||
@@ -143,6 +145,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
super(imgLoader);
|
||||
@@ -151,7 +155,9 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
|
||||
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
|
||||
onConfigureViewHolder(holder);
|
||||
return holder;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
private LinearLayout searchLayout;
|
||||
private EditText searchEdit;
|
||||
private ImageButton clearSearchButton;
|
||||
private String currentQuery;
|
||||
private Runnable debouncer=()->{
|
||||
currentQuery=searchEdit.getText().toString();
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
if(!TextUtils.isEmpty(currentQuery))
|
||||
loadData();
|
||||
};
|
||||
private boolean resultDelivered;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRefreshEnabled(false);
|
||||
setEmptyText("");
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
searchLayout=new LinearLayout(view.getContext());
|
||||
searchLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
searchEdit=new EditText(view.getContext());
|
||||
searchEdit.setHint(R.string.search_hint);
|
||||
searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
|
||||
searchEdit.setBackground(null);
|
||||
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
searchEdit.postDelayed(debouncer, 300);
|
||||
}));
|
||||
searchEdit.setImeActionLabel(null, EditorInfo.IME_ACTION_SEARCH);
|
||||
searchEdit.setOnEditorActionListener((v, actionId, event)->{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
debouncer.run();
|
||||
return true;
|
||||
});
|
||||
searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
|
||||
clearSearchButton=new ImageButton(view.getContext());
|
||||
clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24);
|
||||
clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant)));
|
||||
clearSearchButton.setBackground(UiUtils.getThemeDrawable(getToolbarContext(), android.R.attr.actionBarItemBackground));
|
||||
clearSearchButton.setOnClickListener(v->searchEdit.setText(""));
|
||||
searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
view.setBackgroundResource(R.drawable.bg_m3_surface3);
|
||||
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
|
||||
setStatusBarColor(color);
|
||||
setNavigationBarColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
refreshing=true;
|
||||
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.accounts.stream().map(AccountViewModel::new).collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
if(searchLayout.getParent()!=null)
|
||||
((ViewGroup) searchLayout.getParent()).removeView(searchLayout);
|
||||
getToolbar().addView(searchLayout, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
getToolbar().setBackgroundResource(R.drawable.bg_m3_surface3);
|
||||
searchEdit.requestFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsElevationOnScrollEffect(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
holder.setOnClickListener(this::onItemClick);
|
||||
}
|
||||
|
||||
private void onItemClick(AccountViewHolder holder){
|
||||
if(resultDelivered)
|
||||
return;
|
||||
|
||||
resultDelivered=true;
|
||||
Bundle res=new Bundle();
|
||||
res.putParcelable("selectedAccount", Parcels.wrap(holder.getItem().account));
|
||||
setResult(true, res);
|
||||
Nav.finish(this, false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user