package com.ubergeek42.WeechatAndroid.fragments;
import java.util.Vector;
import android.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.ubergeek42.WeechatAndroid.adapters.ChatLinesAdapter;
import com.ubergeek42.WeechatAndroid.R;
import com.ubergeek42.WeechatAndroid.WeechatActivity;
import com.ubergeek42.WeechatAndroid.relay.Buffer;
import com.ubergeek42.WeechatAndroid.relay.Buffer.LINES;
import com.ubergeek42.WeechatAndroid.relay.BufferEye;
import com.ubergeek42.WeechatAndroid.relay.BufferList;
import com.ubergeek42.WeechatAndroid.relay.Line;
import com.ubergeek42.WeechatAndroid.service.P;
import com.ubergeek42.WeechatAndroid.utils.CopyPaste;
import com.ubergeek42.weechat.ColorScheme;
import static com.ubergeek42.WeechatAndroid.service.Events.*;
import static com.ubergeek42.WeechatAndroid.service.RelayService.STATE.*;
import de.greenrobot.event.EventBus;
public class BufferFragment extends Fragment implements BufferEye, OnKeyListener,
OnClickListener, TextWatcher, TextView.OnEditorActionListener {
final private static boolean DEBUG_TAB_COMPLETE = false;
final private static boolean DEBUG_LIFECYCLE = true;
final private static boolean DEBUG_VISIBILITY = false;
final private static boolean DEBUG_AUTOSCROLLING = false;
private final static String TAG = "tag";
private WeechatActivity activity = null;
private boolean started = false;
private ListView uiLines;
private EditText uiInput;
private ImageButton uiSend;
private ImageButton uiTab;
private ViewGroup uiMore;
private Button uiMoreButton;
private String fullName = "…";
private Buffer buffer;
private ChatLinesAdapter linesAdapter;
private Logger logger = LoggerFactory.getLogger(toString());
public static BufferFragment newInstance(String tag) {
BufferFragment fragment = new BufferFragment();
Bundle args = new Bundle();
args.putString(TAG, tag);
fragment.setArguments(args);
return fragment;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// life cycle
////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onAttach(Context context) {
if (DEBUG_LIFECYCLE) logger.debug("onAttach(...)");
super.onAttach(context);
this.activity = (WeechatActivity) context;
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG_LIFECYCLE) logger.debug("onCreate()");
super.onCreate(savedInstanceState);
fullName = getArguments().getString(TAG);
logger = LoggerFactory.getLogger(toString());
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (DEBUG_LIFECYCLE) logger.debug("onCreateView()");
View v = inflater.inflate(R.layout.chatview_main, container, false);
uiLines = (ListView) v.findViewById(R.id.chatview_lines);
uiInput = (EditText) v.findViewById(R.id.chatview_input);
uiSend = (ImageButton) v.findViewById(R.id.chatview_send);
uiTab = (ImageButton) v.findViewById(R.id.chatview_tab);
uiSend.setOnClickListener(this);
uiTab.setOnClickListener(this);
uiInput.setOnKeyListener(this); // listen for hardware keyboard
uiInput.addTextChangedListener(this); // listen for software keyboard through watching input box text
uiInput.setOnEditorActionListener(this); // listen for software keyboard's “send” click. see onEditorAction()
uiLines.setFocusable(false);
uiLines.setFocusableInTouchMode(false);
CopyPaste cp = new CopyPaste(activity, uiInput);
uiInput.setOnLongClickListener(cp);
uiLines.setOnItemLongClickListener(cp);
uiMore = (ViewGroup) inflater.inflate(R.layout.more_button, null);
uiMoreButton = (Button) uiMore.findViewById(R.id.button_more);
uiMoreButton.setOnClickListener(this);
uiLines.addHeaderView(uiMore);
status = Buffer.LINES.CAN_FETCH_MORE;
online = true;
return v;
}
@Override
public void onStart() {
if (DEBUG_LIFECYCLE) logger.debug("onStart()");
super.onStart();
started = true;
uiSend.setVisibility(P.showSend ? View.VISIBLE : View.GONE);
uiTab.setVisibility(P.showTab ? View.VISIBLE : View.GONE);
uiLines.setBackgroundColor(0xFF000000 | ColorScheme.get().defaul[ColorScheme.OPT_BG]);
EventBus.getDefault().registerSticky(this);
}
@Override
public void onStop() {
if (DEBUG_LIFECYCLE) logger.debug("onStop()");
super.onStop();
started = false;
detachFromBuffer();
EventBus.getDefault().unregister(this);
}
@Override
public void onDetach() {
if (DEBUG_LIFECYCLE) logger.debug("onDetach()");
activity = null;
super.onDetach();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// visibility (set by pager adapter)
////////////////////////////////////////////////////////////////////////////////////////////////
private boolean pagerVisible = false;
/** these are the highlight and private counts that we are supposed to scroll
** they are reset after the scroll has been completed */
private int highlights = 0;
// this method is using the following:
// lastVisibleLine last line that exists in the buffer. NOTE: "visible" here means line is not filtered in weechat
// readMarkerLine used for display. it is:
// * saved on app shutdown and restored on start
// * altered if a buffer has been read in weechat (see BufferList.saveLastReadLine)
// * set to the last displayed line when user navigates away from a buffer
// * shifted from invisible line to last visible line if buffer is filtered
private void maybeMoveReadMarker() {
if (DEBUG_VISIBILITY) logger.debug("maybeMoveReadMarker()");
if (buffer != null && buffer.readMarkerLine != buffer.lastVisibleLine) {
buffer.readMarkerLine = buffer.lastVisibleLine;
linesAdapter.needMoveLastReadMarker = true;
onLinesChanged();
}
}
private int privates = 0;
/** called when visibility of current fragment is (potentially) altered by
** * drawer being shown/hidden
** * whether buffer is shown in the pager (see MainPagerAdapter)
** * availability of buffer & activity
** * lifecycle */
public void maybeChangeVisibilityState() {
if (DEBUG_VISIBILITY) logger.debug("maybeChangeVisibilityState()");
if (activity == null || buffer == null)
return;
// see if visibility has changed. if it hasn't, do nothing
boolean obscured = activity.isPagerNoticeablyObscured();
boolean watched = started && pagerVisible && !obscured;
if (buffer.isWatched == watched) return;
// visibility has changed.
if (watched) {
highlights = buffer.highlights;
privates = (buffer.type == Buffer.PRIVATE) ? buffer.unreads : 0;
}
buffer.setWatched(watched);
scrollToHotLineIfNeeded();
// move the read marker in weechat (if preferences dictate)
if (!watched && P.hotlistSync) {
EventBus.getDefault().post(new SendMessageEvent(String.format("input %s /buffer set hotlist -1", buffer.hexPointer())));
EventBus.getDefault().post(new SendMessageEvent(String.format("input %s /input set_unread_current_buffer", buffer.hexPointer())));
}
}
/** called by MainPagerAdapter
** tells us that this page is visible, also used to lifecycle calls (must call super) */
@Override
public void setUserVisibleHint(boolean visible) {
if (DEBUG_VISIBILITY) logger.debug("setUserVisibleHint({})", visible);
super.setUserVisibleHint(visible);
this.pagerVisible = visible;
if (!visible) maybeMoveReadMarker();
maybeChangeVisibilityState();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// events
////////////////////////////////////////////////////////////////////////////////////////////////
private boolean online = true;
// this can be forced to always run in background, but then it would run after onStart()
// if the fragment hasn't been initialized yet, that would lead to a bit of flicker
@SuppressWarnings("unused")
public void onEvent(@NonNull StateChangedEvent event) {
logger.debug("onEvent({})", event);
boolean online = event.state.contains(LISTED);
if (buffer == null || online) {
buffer = BufferList.findByFullName(fullName);
if (online && buffer == null) {
onBufferClosed();
return;
}
if (buffer != null) attachToBuffer();
else logger.warn("...buffer is null"); // this should only happen after OOM kill
}
if (this.online != online) initUI(this.online = online);
}
//////////////////////////////////////////////////////////////////////////////////////////////// attach detach
private void attachToBuffer() {
logger.debug("attachToBuffer()");
buffer.setBufferEye(this);
linesAdapter = new ChatLinesAdapter(activity, buffer, uiLines);
linesAdapter.setFont(P.bufferFont);
linesAdapter.readLinesFromBuffer();
activity.runOnUiThread(new Runnable() {
@Override public void run() {
activity.updateCutePagerTitleStrip();
uiLines.setAdapter(linesAdapter);
maybeChangeHeader();
}
});
maybeChangeVisibilityState();
}
// buffer might be null if we are closing fragment that is not connected
private void detachFromBuffer() {
if (DEBUG_LIFECYCLE) logger.debug("detachFromBuffer()");
maybeChangeVisibilityState();
if (buffer != null) buffer.setBufferEye(null);
buffer = null;
}
//////////////////////////////////////////////////////////////////////////////////////////////// ui
public void initUI(final boolean online) {
if (DEBUG_LIFECYCLE) logger.debug("initUI({})", online);
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
uiInput.setEnabled(online);
uiSend.setEnabled(online);
uiTab.setEnabled(online);
if (!online)
activity.hideSoftwareKeyboard();
}
});
}
private LINES status = LINES.CAN_FETCH_MORE;
private void maybeChangeHeader() {
final LINES s = buffer.getLineStatus();
if (status == s) return;
status = s;
activity.runOnUiThread(new Runnable() {
@Override public void run() {
logger.debug("maybeChangeHeader(); {}", s);
if (s == LINES.EVERYTHING_FETCHED) {
uiMore.removeAllViews();
} else {
if (uiMore.getChildCount() == 0) uiMore.addView(uiMoreButton);
boolean more = s == LINES.CAN_FETCH_MORE;
uiMoreButton.setEnabled(more);
uiMoreButton.setTextColor(more ? 0xff80cbc4 : 0xff777777);
uiMoreButton.setText(getString(more ? R.string.more_button : R.string.more_button_fetching));
}
}
});
}
private void requestMoreLines() {
if (buffer.getLineStatus() == LINES.CAN_FETCH_MORE) {
buffer.requestMoreLines();
maybeChangeHeader();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// BufferEye stuff
////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onLinesChanged() {
if (linesAdapter != null) {
linesAdapter.onLinesChanged();
}
maybeChangeHeader();
}
@Override
public void onLinesListed() {
if (linesAdapter != null) {
linesAdapter.onLinesChanged();
}
maybeChangeHeader();
scrollToHotLineIfNeeded();
}
@Override
public void onPropertiesChanged() {
linesAdapter.onPropertiesChanged();
}
@Override
public void onBufferClosed() {
activity.runOnUiThread(new Runnable() {
@Override public void run() {
activity.closeBuffer(fullName);
}
});
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// scrolling
////////////////////////////////////////////////////////////////////////////////////////////////
/** scroll to the first hot line, if possible (that is, first unread line in a private buffer
** or the first unread highlight)
** can be called multiple times, will only run once
** posts to the listview to make sure it's fully completed loading the items
** after setting the adapter or updating lines */
public void scrollToHotLineIfNeeded() {
if (DEBUG_AUTOSCROLLING) logger.debug("scrollToHotLineIfNeeded()");
if (buffer != null && buffer.isWatched && buffer.holdsAllLines && (highlights > 0 || privates > 0)) {
uiLines.post(new Runnable() {
@Override
public void run() {
int count = linesAdapter.getCount(), idx = -2;
if (privates > 0) {
int p = 0;
for (idx = count - 1; idx >= 0; idx--) {
Line line = (Line) linesAdapter.getItem(idx);
if (line.type == Line.LINE_MESSAGE && ++p == privates) break;
}
} else if (highlights > 0) {
int h = 0;
for (idx = count - 1; idx >= 0; idx--) {
Line line = (Line) linesAdapter.getItem(idx);
if (line.highlighted && ++h == highlights) break;
}
}
if (idx == -1)
Toast.makeText(getActivity(), activity.getString(R.string.autoscroll_no_line), Toast.LENGTH_SHORT).show();
else if (idx > 0)
uiLines.smoothScrollToPosition(idx);
highlights = privates = 0;
}
});
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// keyboard / buttons
////////////////////////////////////////////////////////////////////////////////////////////////
/** the only OnKeyListener's method
** User pressed some key in the input box, check for what it was
** NOTE: this only applies to HARDWARE buttons */
@Override
public boolean onKey(View v, int keycode, KeyEvent event) {
if (DEBUG_TAB_COMPLETE) logger.debug("onKey(..., {}, ...)", keycode);
int action = event.getAction();
return checkSendMessage(keycode, action) ||
checkVolumeButtonResize(keycode, action) ||
checkForTabCompletion(keycode, action);
}
private boolean checkSendMessage(int keycode, int action) {
if (keycode == KeyEvent.KEYCODE_ENTER) {
if (action == KeyEvent.ACTION_UP) sendMessage();
return true;
}
return false;
}
private boolean checkForTabCompletion(int keycode, int action) {
if ((keycode == KeyEvent.KEYCODE_TAB || keycode == KeyEvent.KEYCODE_SEARCH) &&
action == KeyEvent.ACTION_DOWN) {
tryTabComplete();
return true;
}
return false;
}
private boolean checkVolumeButtonResize(int keycode, int action) {
if (keycode == KeyEvent.KEYCODE_VOLUME_DOWN || keycode == KeyEvent.KEYCODE_VOLUME_UP) {
if (P.volumeBtnSize) {
if (action == KeyEvent.ACTION_UP) {
float textSize = P.textSize;
switch (keycode) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (textSize < 30) textSize += 1;
break;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (textSize > 5) textSize -= 1;
break;
}
P.setTextSizeAndLetterWidth(textSize);
}
return true;
}
}
return false;
}
/** the only OnClickListener's method
** our own send button or tab button pressed */
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.chatview_send: sendMessage(); break;
case R.id.chatview_tab: tryTabComplete(); break;
case R.id.button_more: requestMoreLines(); break;
}
}
/** the only OnEditorActionListener's method
** listens to keyboard's “send” press (NOT our button) */
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_SEND) {
sendMessage();
return true;
}
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////////// send message
/** sends the message if there's anything to send */
private void sendMessage() {
String input = uiInput.getText().toString();
if (input.length() != 0)
P.addSentMessage(input);
String[] lines = input.split("\n");
for (String line : lines) {
if (line.length() != 0)
EventBus.getDefault().post(new SendMessageEvent(String.format("input %s %s", buffer.hexPointer(), line)));
}
uiInput.setText(""); // this will reset tab completion
}
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////// tab completion
////////////////////////////////////////////////////////////////////////////////////////////////
private boolean tcInProgress;
private Vector<String> tcMatches;
private int tcIndex;
private int tcWordStart;
private int tcWordEnd;
/** attempts to perform tab completion on the current input */
@SuppressLint("SetTextI18n") private void tryTabComplete() {
if (DEBUG_TAB_COMPLETE) logger.debug("tryTabComplete()");
if (buffer == null) return;
String txt = uiInput.getText().toString();
if (!tcInProgress) {
// find the end of the word to be completed
// blabla nick|
tcWordEnd = uiInput.getSelectionStart();
if (tcWordEnd <= 0)
return;
// find the beginning of the word to be completed
// blabla |nick
tcWordStart = tcWordEnd;
while (tcWordStart > 0 && txt.charAt(tcWordStart - 1) != ' ')
tcWordStart--;
// get the word to be completed, lowercase
if (tcWordStart == tcWordEnd)
return;
String prefix = txt.substring(tcWordStart, tcWordEnd).toLowerCase();
// compute a list of possible matches
// nicks is ordered in last used comes first way, so we just pick whatever comes first
// if computed list is empty, abort
tcMatches = new Vector<>();
for (String nick : buffer.getLastUsedNicksCopy())
if (nick.toLowerCase().startsWith(prefix))
tcMatches.add(nick.trim());
if (tcMatches.size() == 0)
return;
tcIndex = 0;
} else {
tcIndex = (tcIndex + 1) % tcMatches.size();
}
// get new nickname, adjust the end of the word marker
// and finally set the text and place the cursor on the end of completed word
String nick = tcMatches.get(tcIndex);
if (tcWordStart == 0)
nick += ": ";
uiInput.setText(txt.substring(0, tcWordStart) + nick + txt.substring(tcWordEnd));
tcWordEnd = tcWordStart + nick.length();
uiInput.setSelection(tcWordEnd);
// altering text in the input box sets tcInProgress to false,
// so this is the last thing we do in this function:
tcInProgress = true;
}
public void setText(String text) {
String oldText = uiInput.getText().toString();
if (oldText.length() > 0 && oldText.charAt(oldText.length() - 1) != ' ') oldText += ' ';
uiInput.setText(oldText + text);
uiInput.setSelection(uiInput.getText().length());
}
//////////////////////////////////////////////////////////////////////////////////////////////// text watcher
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {}
/** invalidate tab completion progress on input box text change
** tryTabComplete() will set it back if it modified the text causing this function to run */
@Override
public void afterTextChanged(Editable s) {
if (DEBUG_TAB_COMPLETE) logger.debug("afterTextChanged(...)");
tcInProgress = false;
}
////////////////////////////////////////////////////////////////////////////////////////////////
@Override public String toString() {
return "BF [" + fullName + "]";
}
}