package de.jeisfeld.augendiagnoselib.fragments; import android.app.Fragment; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.text.SpannableString; import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.text.style.ImageSpan; import android.text.style.StyleSpan; import android.util.SparseArray; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.widget.Button; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.SeekBar; import android.widget.TextView; import android.widget.ToggleButton; import java.io.IOException; import java.util.ArrayList; import de.jeisfeld.augendiagnoselib.Application; import de.jeisfeld.augendiagnoselib.Application.AuthorizationLevel; import de.jeisfeld.augendiagnoselib.R; import de.jeisfeld.augendiagnoselib.activities.DisplayHtmlActivity; import de.jeisfeld.augendiagnoselib.activities.DisplayImageActivity; import de.jeisfeld.augendiagnoselib.activities.DisplayOneActivity; import de.jeisfeld.augendiagnoselib.activities.DisplayTwoActivity; import de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView; import de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView.GuiElementUpdater; import de.jeisfeld.augendiagnoselib.components.OverlayPinchImageView.PinchMode; import de.jeisfeld.augendiagnoselib.components.colorpicker.ColorPickerConstants; import de.jeisfeld.augendiagnoselib.components.colorpicker.ColorPickerDialog; import de.jeisfeld.augendiagnoselib.components.colorpicker.ColorPickerSwatch.OnColorSelectedListener; import de.jeisfeld.augendiagnoselib.util.DialogUtil; import de.jeisfeld.augendiagnoselib.util.PreferenceUtil; import de.jeisfeld.augendiagnoselib.util.SystemUtil; import de.jeisfeld.augendiagnoselib.util.TrackingUtil; import de.jeisfeld.augendiagnoselib.util.TrackingUtil.Category; import de.jeisfeld.augendiagnoselib.util.imagefile.EyePhoto.RightLeft; import de.jeisfeld.augendiagnoselib.util.imagefile.JpegMetadataUtil; /** * Variant of DisplayOneFragment that includes overlay handling. */ public class DisplayImageFragment extends Fragment implements GuiElementUpdater, OnColorSelectedListener { /** * The resource key for the image type (TYPE_FILENAME or TYPE_FILERESOURCE). */ private static final String STRING_TYPE = "de.jeisfeld.augendiagnoselib.TYPE"; /** * The resource key for the file path. */ private static final String STRING_FILE = "de.jeisfeld.augendiagnoselib.FILE"; /** * The resource key for the file resource. */ private static final String STRING_FILERESOURCE = "de.jeisfeld.augendiagnoselib.FILERESOURCE"; /** * The resource kay for the image index (in case of multiple images). */ private static final String STRING_IMAGEINDEX = "de.jeisfeld.augendiagnoselib.IMAGEINDEX"; /** * The resource kay for the rightleft information (if not contained in the image). */ private static final String STRING_RIGHTLEFT = "de.jeisfeld.augendiagnoselib.RIGHTLEFT"; /** * Type value set if the fragment shows an image by filename. */ private static final int TYPE_FILENAME = 1; /** * Type value set if the fragment shows an image by resource id. */ private static final int TYPE_FILERESOURCE = 2; /** * The size of the circle overlay bitmap. */ private static final int CIRCLE_BITMAP_SIZE = OverlayPinchImageView.OVERLAY_SIZE; /** * The radius of the iris circle displayed. */ private static final int CIRCLE_RADIUS_IRIS = 448; /** * The radius of the pupil circle displayed. */ private static final int CIRCLE_RADIUS_PUPIL = 384; /** * Type (TYPE_FILENAME or TYPE_FILERESOURCE). */ private int mType; /** * The file resource id. */ private int mFileResource; /** * The file path. */ private String mFile; /** * The image index. */ private int mImageIndex; /** * Information if right or left image. */ private RightLeft mRightLeft; /** * Flag indicating if overlays are allowed. */ private OverlayStatus mOverlayStatus; protected final OverlayStatus getOverlayStatus() { return mOverlayStatus; } /** * Flag holding information if fragment is shown in landscape mode. */ private boolean mIsLandscape; private boolean isLandscape() { return mIsLandscape; } protected final void setLandscape(final boolean newIsLandscape) { this.mIsLandscape = newIsLandscape; } /** * The view displaying the image. */ private OverlayPinchImageView mImageView; /** * The number of overlay buttons, excluding the pupil overlay. */ public static final int OVERLAY_BUTTON_COUNT; /** * The button for showing the image in full resolution. */ private Button mClarityButton; /** * The button for showing the image comment. */ private Button mCommentButton; /** * The button for showing the image info. */ private Button mInfoButton; /** * The button for saving image metadata. */ private Button mSaveButton; /** * The button for sharing the image. */ private Button mShareButton; /** * The button for showing or hiding the tools. */ private Button mToolsButton; /** * The button for showing or hiding the tools. */ private Button mHelpButton; /** * The array of overlay buttons. */ private ToggleButton[] mToggleOverlayButtons; /** * The lock button. */ private ToggleButton mLockButton; /** * The button for setting pupil position. */ private ToggleButton mPupilButton; /** * The color selector button. */ private Button mSelectColorButton; /** * The button for the guided setup of iris and pupil position. */ private Button mGuidedTopoSetupButton; /** * The brightness SeekBar. */ private SeekBar mSeekbarBrightness; /** * The contrast SeekBar. */ private SeekBar mSeekbarContrast; /** * The saturation SeekBar. */ private SeekBar mSeekbarSaturation; /** * The color remperature SeekBar. */ private SeekBar mSeekbarColorTemperature; /** * A flag indicating which utilities (seekbars, buttons) should be displayed. */ private UtilitiyStatus mShowUtilities = UtilitiyStatus.SHOW_NOTHING; /** * The overlay color. */ private int mOverlayColor = Color.RED; /** * The status of the pupil button. */ private PupilButtonStatus mPupilButtonStatus; /** * Timestamp for measuring the tracking duration. */ private long mTrackingTimestamp = 0; static { TypedArray overlayButtonResources = Application.getAppContext().getResources().obtainTypedArray(R.array.overlay_buttons); OVERLAY_BUTTON_COUNT = Math.min(overlayButtonResources.length(), OverlayPinchImageView.OVERLAY_COUNT) - 1; overlayButtonResources.recycle(); } /** * Initialize the fragment with the file name. * * @param initialFile the file path. * @param initialImageIndex The index of the view (required if there are multiple such fragments) * @param initialRightLeft Information if it is the right or left eye (if not in image metadata) */ public final void setParameters(final String initialFile, final int initialImageIndex, @Nullable final RightLeft initialRightLeft) { Bundle args = new Bundle(); args.putString(STRING_FILE, initialFile); args.putInt(STRING_TYPE, TYPE_FILENAME); args.putInt(STRING_IMAGEINDEX, initialImageIndex); if (initialRightLeft != null) { args.putSerializable(STRING_RIGHTLEFT, initialRightLeft); } setArguments(args); } /** * Initialize the fragment with the file resource. * * @param initialFileResource The file resource. * @param initialImageIndex The index of the view (required if there are multiple such fragments) */ public final void setParameters(final int initialFileResource, final int initialImageIndex) { Bundle args = new Bundle(); args.putInt(STRING_FILERESOURCE, initialFileResource); args.putInt(STRING_TYPE, TYPE_FILERESOURCE); args.putInt(STRING_IMAGEINDEX, initialImageIndex); setArguments(args); } /* * Retrieve parameters. */ @Override public final void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); mType = getArguments().getInt(STRING_TYPE, -1); mFile = getArguments().getString(STRING_FILE); mFileResource = getArguments().getInt(STRING_FILERESOURCE, -1); mImageIndex = getArguments().getInt(STRING_IMAGEINDEX, 0); mRightLeft = (RightLeft) getArguments().getSerializable(STRING_RIGHTLEFT); } @Override public final void onResume() { super.onResume(); mTrackingTimestamp = System.currentTimeMillis(); } @Override public final void onPause() { super.onPause(); TrackingUtil.sendTiming(Category.TIME_USAGE, "View Images", null, System.currentTimeMillis() - mTrackingTimestamp); } /* * Inflate View. */ // OVERRIDABLE @Nullable @Override public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { if (SystemUtil.isLandscape()) { setLandscape(true); return inflater.inflate(R.layout.fragment_display_image_landscape, container, false); } else { setLandscape(false); return inflater.inflate(R.layout.fragment_display_image_portrait, container, false); } } /* * Update data from view. */ @Override public final void onActivityCreated(@Nullable final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (getView() == null) { return; } mImageView = (OverlayPinchImageView) getView().findViewById(R.id.mainImage); mImageView.setGuiElementUpdater(this); mImageView.allowFullResolution(hasAutoFullResolution()); mLockButton = (ToggleButton) getView().findViewById(R.id.toggleButtonLink); TypedArray overlayButtonResources = getResources().obtainTypedArray(R.array.overlay_buttons); mToggleOverlayButtons = new ToggleButton[OVERLAY_BUTTON_COUNT]; for (int i = 0; i < OVERLAY_BUTTON_COUNT; i++) { mToggleOverlayButtons[i] = (ToggleButton) getView().findViewById(overlayButtonResources.getResourceId(i, -1)); mToggleOverlayButtons[i].setVisibility(View.VISIBLE); } overlayButtonResources.recycle(); mPupilButton = (ToggleButton) getView().findViewById(R.id.toggleButtonPupil); mSelectColorButton = (Button) getView().findViewById(R.id.buttonSelectColor); mClarityButton = (Button) getView().findViewById(R.id.buttonClarity); mInfoButton = (Button) getView().findViewById(R.id.buttonInfo); mCommentButton = (Button) getView().findViewById(R.id.buttonComment); mSaveButton = (Button) getView().findViewById(R.id.buttonSave); mShareButton = (Button) getView().findViewById(R.id.buttonShare); mToolsButton = (Button) getView().findViewById(R.id.buttonTools); mHelpButton = (Button) getView().findViewById(R.id.buttonHelp); mGuidedTopoSetupButton = (Button) getView().findViewById(R.id.buttonGuidedTopoSetup); // Layout for circle button ImageSpan imageSpan = new ImageSpan(getActivity(), R.drawable.ic_btn_wheel); SpannableString content = new SpannableString("X"); content.setSpan(imageSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mToggleOverlayButtons[0].setText(content); mToggleOverlayButtons[0].setTextOn(content); mToggleOverlayButtons[0].setTextOff(content); // Layout for guided topo setup button mGuidedTopoSetupButton.setText(content); if (savedInstanceState != null) { mShowUtilities = (UtilitiyStatus) savedInstanceState.getSerializable("showUtilities"); mOverlayColor = savedInstanceState.getInt("overlayColor", Color.RED); mPupilButtonStatus = (PupilButtonStatus) savedInstanceState.getSerializable("pupilButtonStatus"); mLockButton.setChecked(savedInstanceState.getBoolean("lockButtonIsChecked")); mOverlayStatus = (OverlayStatus) savedInstanceState.getSerializable("overlayStatus"); if (mOverlayStatus == OverlayStatus.GUIDED || mOverlayStatus == OverlayStatus.GUIDE_IRIS || mOverlayStatus == OverlayStatus.GUIDE_PUPIL) { drawOverlayCircle(); } } else { mShowUtilities = getDefaultShowUtilitiesValue(); mOverlayColor = PreferenceUtil.getSharedPreferenceInt(R.string.key_overlay_color, Color.RED); mPupilButtonStatus = PupilButtonStatus.OFF; mOverlayStatus = PreferenceUtil.getSharedPreferenceBoolean(R.string.key_guided_topo_setup) ? OverlayStatus.GUIDED : OverlayStatus.ALLOWED; } showUtilities(); // Initialize the onClick listeners for the buttons setButtonListeners(); // Special handling for non-JPEG images checkJpeg(); configureSeekbars(); // The following also updates the selectColorButton mImageView.setOverlayColor(mOverlayColor); // Layout for pupil button mPupilButton.setEnabled(mLockButton.isChecked()); setPupilButtonBitmap(); } /** * Configure the seekbars for brightness, contrast, saturation and color temperature. */ private void configureSeekbars() { mSeekbarBrightness = (SeekBar) getView().findViewById(R.id.seekBarBrightness); mSeekbarBrightness.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress, final boolean fromUser) { if (fromUser) { mImageView.updateColorSettings(((float) seekBar.getProgress()) / seekBar.getMax() * 2 - 1, null, null, null, true); } } }); mSeekbarContrast = (SeekBar) getView().findViewById(R.id.seekBarContrast); mSeekbarContrast.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress, final boolean fromUser) { if (fromUser) { mImageView.updateColorSettings(null, ((float) seekBar.getProgress()) / seekBar.getMax() * 2 - 1, null, null, true); } } }); mSeekbarSaturation = (SeekBar) getView().findViewById(R.id.seekBarSaturation); mSeekbarSaturation.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress, final boolean fromUser) { if (fromUser) { mImageView.updateColorSettings(null, null, ((float) seekBar.getProgress()) / seekBar.getMax() * 2 - 1, null, true); } } }); mSeekbarColorTemperature = (SeekBar) getView().findViewById(R.id.seekBarColorTemperature); mSeekbarColorTemperature.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onProgressChanged(@NonNull final SeekBar seekBar, final int progress, final boolean fromUser) { if (fromUser) { mImageView.updateColorSettings(null, null, null, ((float) seekBar.getProgress()) / seekBar.getMax() * 2 - 1, true); } } }); OnTouchListener onIconTouchListener1 = new OnTouchListener() { @Override public boolean onTouch(final View v, final MotionEvent event) { if (mImageView == null) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mImageView.updateColorSettings(0f, 0f, 0f, 0f, false); return true; case MotionEvent.ACTION_UP: mImageView.updateColorSettings(((float) mSeekbarBrightness.getProgress()) / mSeekbarBrightness.getMax() * 2 - 1, ((float) mSeekbarContrast.getProgress()) / mSeekbarContrast.getMax() * 2 - 1, ((float) mSeekbarSaturation.getProgress()) / mSeekbarSaturation.getMax() * 2 - 1, ((float) mSeekbarColorTemperature.getProgress()) / mSeekbarColorTemperature.getMax() * 2 - 1, false); return true; default: return false; } } }; getView().findViewById(R.id.iconBrightness).setOnTouchListener(onIconTouchListener1); getView().findViewById(R.id.iconContrast).setOnTouchListener(onIconTouchListener1); OnTouchListener onIconTouchListener2 = new OnTouchListener() { @Override public boolean onTouch(final View v, final MotionEvent event) { if (mImageView == null) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mImageView.updateColorSettings(null, null, 0f, 0f, false); return true; case MotionEvent.ACTION_UP: mImageView.updateColorSettings(null, null, ((float) mSeekbarSaturation.getProgress()) / mSeekbarSaturation.getMax() * 2 - 1, ((float) mSeekbarColorTemperature.getProgress()) / mSeekbarColorTemperature.getMax() * 2 - 1, false); return true; default: return false; } } }; getView().findViewById(R.id.iconSaturation).setOnTouchListener(onIconTouchListener2); getView().findViewById(R.id.iconColorTemperature).setOnTouchListener(onIconTouchListener2); } /** * Initialize the on-click actions for the buttons. */ private void setButtonListeners() { for (int i = 0; i < OVERLAY_BUTTON_COUNT; i++) { final int index = i; mToggleOverlayButtons[i].setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { onToggleOverlayClicked(index); } }); } String[] overlayButtonStrings = getResources().getStringArray(R.array.overlay_button_strings); for (int i = 1; i < OVERLAY_BUTTON_COUNT; i++) { mToggleOverlayButtons[i].setText(overlayButtonStrings[i]); mToggleOverlayButtons[i].setTextOn(overlayButtonStrings[i]); mToggleOverlayButtons[i].setTextOff(overlayButtonStrings[i]); final int index = i; mToggleOverlayButtons[i].setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(final View v) { return displayOverlaySelectionPopup(index, false); } }); if (PreferenceUtil.getIndexedSharedPreferenceIntString(R.string.key_indexed_overlaytype, i, -1) < 0) { mToggleOverlayButtons[i].setVisibility(View.GONE); } } mLockButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { onToggleLockClicked(); } }); mSelectColorButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { onButtonSelectColorClicked(v); } }); if (!hasAutoFullResolution()) { mClarityButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { mImageView.showFullResolutionSnapshot(false); DialogUtil.displayTip(getActivity(), R.string.message_tip_clarity, R.string.key_tip_clarity); } }); } else { mClarityButton.setVisibility(View.GONE); } mInfoButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { DialogUtil.displayImageInfo(getActivity(), mImageView.getEyePhoto()); } }); mCommentButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { if (mImageView.getMetadata() != null) { ((DisplayImageActivity) getActivity()).startEditComment(DisplayImageFragment.this, mImageView.getMetadata().getComment()); DialogUtil.displayTip(getActivity(), R.string.message_tip_editcomment, R.string.key_tip_editcomment); } } }); mShareButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { showShareMenu(v); } }); mSaveButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { showSaveMenu(v); DialogUtil.displayTip(getActivity(), R.string.message_tip_saveview, R.string.key_tip_saveview); } }); mToolsButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { mShowUtilities = mShowUtilities.getNextStatus(alwaysShowOverlayBar(), allowAllBars()); showUtilities(); updateDefaultShowUtilities(mShowUtilities); } }); mHelpButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { DisplayHtmlActivity.startActivity(getActivity(), R.string.html_display_photos); } }); mPupilButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { onTogglePupilClicked(); } }); if (PreferenceUtil.getSharedPreferenceBoolean(R.string.key_guided_topo_setup)) { mGuidedTopoSetupButton.setOnClickListener(new OnClickListener() { @Override public void onClick(final View v) { switch (mOverlayStatus) { case GUIDED: DialogUtil.displayTip(getActivity(), R.string.message_tip_overlay_guided, R.string.key_tip_overlay_guided); mOverlayStatus = OverlayStatus.GUIDE_IRIS; mImageView.setVisibility(View.INVISIBLE); drawOverlayCircle(); requestLayout(); break; case GUIDE_IRIS: mImageView.setOverlayPosition((float) CIRCLE_RADIUS_IRIS / CIRCLE_BITMAP_SIZE); mImageView.lockOverlay(true, true); mOverlayStatus = OverlayStatus.GUIDE_PUPIL; drawOverlayCircle(); break; case GUIDE_PUPIL: mImageView.setPupilPosition((float) CIRCLE_RADIUS_PUPIL / CIRCLE_BITMAP_SIZE); mImageView.storePupilPosition(); mOverlayStatus = OverlayStatus.ALLOWED; setLockChecked(true); drawOverlayCircle(); mImageView.post(new Runnable() { @Override public void run() { mImageView.redoInitialScaling(); } }); break; default: break; } } }); } } /** * Method indicating if the overlay bar should always be shown. * * @return the indicator if the overlay bar should always be shown. */ // OVERRIDABLE protected boolean alwaysShowOverlayBar() { return true; } /** * Method indicating if all bars may be shown at once. * * @return the indicator if all bars may be shown at once. */ // OVERRIDABLE protected boolean allowAllBars() { return true; } /** * Check if the image is JPEG - otherwise disable some functionality. */ private void checkJpeg() { try { JpegMetadataUtil.checkJpeg(mFile); } catch (IOException e) { mOverlayStatus = OverlayStatus.NON_JPEG; showUtilities(); DialogUtil.displayTip(getActivity(), R.string.message_tip_jpeg, R.string.key_tip_jpeg); } } /** * Draw the overlay circle and print the corresponding guide text. */ private void drawOverlayCircle() { if (getView() == null) { return; } mGuidedTopoSetupButton.setEnabled(true); ImageView overlayView = (ImageView) getView().findViewById(R.id.circleOverlay); TextView textViewGuide = (TextView) getView().findViewById(R.id.textViewGuide); mImageView.updatePosition(mOverlayStatus, mOverlayStatus == OverlayStatus.GUIDE_IRIS ? (float) CIRCLE_RADIUS_IRIS / CIRCLE_BITMAP_SIZE : (float) CIRCLE_RADIUS_PUPIL / CIRCLE_BITMAP_SIZE); if (mOverlayStatus != OverlayStatus.GUIDE_IRIS && mOverlayStatus != OverlayStatus.GUIDE_PUPIL) { overlayView.setVisibility(View.GONE); textViewGuide.setVisibility(View.GONE); mGuidedTopoSetupButton.setText(mToggleOverlayButtons[0].getText()); return; } mGuidedTopoSetupButton.setText(getString(R.string.button_ok)); Bitmap overlayBitmap = Bitmap.createBitmap(CIRCLE_BITMAP_SIZE, CIRCLE_BITMAP_SIZE, Bitmap.Config.ARGB_8888); Paint paint = new Paint(); paint.setAntiAlias(true); int overlayColor = PreferenceUtil.getSharedPreferenceInt(R.string.key_overlay_color, Color.RED); paint.setColor(overlayColor); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(5); // MAGIC_NUMBER Canvas canvas = new Canvas(overlayBitmap); int circleRadius = mOverlayStatus == OverlayStatus.GUIDE_IRIS ? CIRCLE_RADIUS_IRIS : CIRCLE_RADIUS_PUPIL; canvas.drawCircle(CIRCLE_BITMAP_SIZE / 2, CIRCLE_BITMAP_SIZE / 2, circleRadius, paint); overlayView.setImageBitmap(overlayBitmap); overlayView.setVisibility(View.VISIBLE); textViewGuide.setText(mOverlayStatus == OverlayStatus.GUIDE_IRIS ? getString(R.string.message_guide_resize_iris) : getString(R.string.message_guide_resize_pupil)); textViewGuide.setVisibility(View.VISIBLE); } /** * Helper method for onClick actions for overlay buttons. * * @param position The number of the overlay button. */ private void onToggleOverlayClicked(final int position) { boolean isChecked = mToggleOverlayButtons[position].isChecked(); boolean buttonGetsUnchecked = false; int overlayPosition = PreferenceUtil.getIndexedSharedPreferenceIntString(R.string.key_indexed_overlaytype, position, -1); if (Application.getAuthorizationLevel() == AuthorizationLevel.TRIAL_ACCESS && isChecked && overlayPosition > Integer.parseInt(Application.getResourceString(R.string.overlay_trial_count))) { DialogUtil.displayAuthorizationError(getActivity(), R.string.message_dialog_trial_overlays); mToggleOverlayButtons[position].setChecked(false); return; } for (int i = 0; i < OVERLAY_BUTTON_COUNT; i++) { if (position != i) { if (mToggleOverlayButtons[i].isChecked()) { mToggleOverlayButtons[i].setChecked(false); buttonGetsUnchecked = true; } } } if (mPupilButtonStatus != PupilButtonStatus.OFF) { mPupilButtonStatus = PupilButtonStatus.OFF; setPupilButtonBitmap(); mImageView.storePupilPosition(); } if (overlayPosition >= 0 || !isChecked) { mImageView.triggerOverlay(overlayPosition, isChecked ? PinchMode.OVERLAY : PinchMode.ALL); if (isChecked) { String overlayName = getResources().getStringArray(R.array.overlay_names)[overlayPosition]; TrackingUtil.sendEvent(Category.EVENT_USER, "Display overlay", overlayName); } } if (isChecked && !buttonGetsUnchecked) { DialogUtil.displayTip(getActivity(), R.string.message_tip_overlay_buttons, R.string.key_tip_overlay_buttons); } } /** * Display a popup for the selection of an overlay type. * * @param position The number of the overlay button. * @param forNewButton Flag indicating the popup is displayed for a new button. Then it is enforced to select a new overlay. * @return true if long click was processed. */ private boolean displayOverlaySelectionPopup(final int position, final boolean forNewButton) { PopupMenu popupMenu; View anchorView = isLandscape() ? getActivity().findViewById(R.id.anchorForContextMenu) : mToggleOverlayButtons[position]; if (SystemUtil.isAtLeastVersion(Build.VERSION_CODES.KITKAT)) { popupMenu = getPopupMenuKitkat(anchorView); } else { popupMenu = new PopupMenu(getActivity(), anchorView); } Menu menu = popupMenu.getMenu(); if (position == getHighestOverlayButtonIndex()) { if (position > 1) { MenuItem menuItemRemove = menu.add(DialogUtil.fromHtml("<b>" + getString(R.string.menu_remove_overlay_button) + "</b>")); menuItemRemove.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(final MenuItem item) { mToggleOverlayButtons[position].setChecked(false); onToggleOverlayClicked(position); PreferenceUtil.setIndexedSharedPreferenceIntString(R.string.key_indexed_overlaytype, position, -1); mToggleOverlayButtons[position].setVisibility(View.GONE); return true; } }); } if (position < OVERLAY_BUTTON_COUNT - 1) { MenuItem menuItemAdd = menu.add(DialogUtil.fromHtml("<b>" + getString(R.string.menu_add_overlay_button) + "</b>")); menuItemAdd.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(final MenuItem item) { displayOverlaySelectionPopup(position + 1, true); return true; } }); } } String[] overlayNames = getResources().getStringArray(R.array.overlay_names); for (int i = 1; i < overlayNames.length - 1; i++) { final int index = i; final Integer oldButtonPosition = buttonForOverlayWithIndex(i); MenuItem menuItem = null; if (oldButtonPosition == null) { menuItem = menu.add(overlayNames[i]); } else { if (!forNewButton) { SpannableString itemName = new SpannableString( getResources().getStringArray(R.array.overlay_button_strings)[oldButtonPosition] + " " + overlayNames[i]); if (oldButtonPosition == position) { itemName.setSpan(new StyleSpan(Typeface.BOLD), 0, itemName.length(), 0); } else { itemName.setSpan(new ForegroundColorSpan(Color.LTGRAY), 0, itemName.length(), 0); } menuItem = menu.add(Menu.NONE, Menu.NONE, OverlayPinchImageView.OVERLAY_COUNT + oldButtonPosition, itemName); } } if (menuItem != null) { menuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(final MenuItem item) { if (Application.getAuthorizationLevel() == AuthorizationLevel.TRIAL_ACCESS && index > Integer.parseInt(Application.getResourceString(R.string.overlay_trial_count))) { DialogUtil.displayAuthorizationError(getActivity(), R.string.message_dialog_trial_overlays); return true; } if (oldButtonPosition != null && oldButtonPosition != position) { // If the same overlay is already used, switch overlays int currentOverlay = PreferenceUtil.getIndexedSharedPreferenceIntString(R.string.key_indexed_overlaytype, position, -1); PreferenceUtil.setIndexedSharedPreferenceIntString(R.string.key_indexed_overlaytype, oldButtonPosition, currentOverlay); } PreferenceUtil.setIndexedSharedPreferenceIntString(R.string.key_indexed_overlaytype, position, index); mToggleOverlayButtons[position].setChecked(true); mToggleOverlayButtons[position].setVisibility(View.VISIBLE); onToggleOverlayClicked(position); return true; } }); } } popupMenu.show(); return true; } /** * Get popup menu with gravity right - only supported for Kitkat or later. * * @param anchorView The anchor view. * @return The popup menu. */ @NonNull @RequiresApi(Build.VERSION_CODES.KITKAT) private PopupMenu getPopupMenuKitkat(final View anchorView) { return new PopupMenu(getActivity(), anchorView, Gravity.END); } /** * Get the index of the highest active overlay button. * * @return The index of the highest active overlay button. */ public static int getHighestOverlayButtonIndex() { int maxIndex = -1; for (int i = 0; i < OVERLAY_BUTTON_COUNT; i++) { if (PreferenceUtil.getIndexedSharedPreferenceIntString(R.string.key_indexed_overlaytype, i, -1) >= 0) { maxIndex = i; } } return maxIndex; } /** * Check if there is already a button configured for a certain overlay type. * * @param index The index of the overlay type. * @return The button index configured for this overlay type. */ public static Integer buttonForOverlayWithIndex(final int index) { if (index < 0) { return null; } for (int i = 0; i < OVERLAY_BUTTON_COUNT; i++) { if (PreferenceUtil.getIndexedSharedPreferenceIntString(R.string.key_indexed_overlaytype, i, -1) == index) { return i; } } return null; } /** * onClick action for Button to switch link between overlay and image. */ private void onToggleLockClicked() { mImageView.lockOverlay(mLockButton.isChecked(), true); mPupilButton.setEnabled(mLockButton.isChecked()); } /** * onClick action for Button to change pupil size and position. */ private void onTogglePupilClicked() { switch (mPupilButtonStatus) { case OFF: mPupilButtonStatus = PupilButtonStatus.CENTER; break; case CENTER: mPupilButtonStatus = PupilButtonStatus.MOVE; break; case MOVE: mPupilButtonStatus = PupilButtonStatus.OFF; break; default: break; } setPupilButtonBitmap(); if (mPupilButtonStatus == PupilButtonStatus.OFF) { mImageView.triggerOverlay(OverlayPinchImageView.OVERLAY_PUPIL_INDEX, PinchMode.ALL); mImageView.storePupilPosition(); } else { for (int i = 0; i < OVERLAY_BUTTON_COUNT; i++) { if (mToggleOverlayButtons[i].isChecked()) { mToggleOverlayButtons[i].setChecked(false); } } PinchMode pinchMode = mPupilButtonStatus == PupilButtonStatus.CENTER ? PinchMode.PUPIL_CENTER : PinchMode.PUPIL; mImageView.triggerOverlay(OverlayPinchImageView.OVERLAY_PUPIL_INDEX, pinchMode); } } /** * onClick action for Button to select color of overlays. * * @param view The view of the select color button. */ private void onButtonSelectColorClicked(final View view) { ColorPickerDialog dialog = ColorPickerDialog .newInstance(R.string.color_picker_title, ColorPickerConstants.COLOR_PICKER_COLORS, mImageView.getOverlayColor(), ColorPickerConstants.COLOR_PICKER_COLUMNS, ColorPickerConstants.COLOR_PICKER_SIZE); dialog.setTargetFragment(this, 0); dialog.show(getFragmentManager(), ColorPickerDialog.class.toString()); } /** * Create the popup menu for saving metadata. * * @param view The view opening the menu. */ private void showSaveMenu(final View view) { PopupMenu popup = new PopupMenu(getActivity(), view); popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(@NonNull final MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_store_color_settings) { mImageView.storeColorSettings(false); return true; } else if (itemId == R.id.action_reset_color_settings) { mImageView.storeColorSettings(true); return true; } else if (itemId == R.id.action_store_position) { mImageView.storePositionZoom(false); return true; } else if (itemId == R.id.action_reset_position) { mImageView.storePositionZoom(true); return true; } else if (itemId == R.id.action_store_overlay_color) { mImageView.storeOverlayColor(false); return true; } else if (itemId == R.id.action_reset_overlay_color) { mImageView.storeOverlayColor(true); return true; } else if (itemId == R.id.action_delete_overlay_position) { mImageView.resetOverlayPosition(true); return true; } else { return true; } } }); popup.inflate(R.menu.menu_image_save); if (mShowUtilities == UtilitiyStatus.SHOW_NOTHING) { popup.getMenu().removeGroup(R.id.group_overlay); } if (mShowUtilities == UtilitiyStatus.SHOW_NOTHING || mShowUtilities == UtilitiyStatus.ONLY_OVERLAY) { popup.getMenu().removeGroup(R.id.group_color_settings); } popup.show(); } /** * Create the popup menu for sharing the image. * * @param view The view opening the menu. */ private void showShareMenu(final View view) { final DisplayImageFragment otherFragment; if (getActivity() instanceof DisplayTwoActivity) { otherFragment = ((DisplayTwoActivity) getActivity()).getOtherFragment(DisplayImageFragment.this); } else { otherFragment = null; } PopupMenu popup = new PopupMenu(getActivity(), view); popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(@NonNull final MenuItem item) { int itemId = item.getItemId(); boolean currentView = itemId == R.id.action_share_this_image_view || itemId == R.id.action_share_both_image_views; boolean bothImages = itemId == R.id.action_share_both_images || itemId == R.id.action_share_both_image_views; if (bothImages && otherFragment != null) { Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); ArrayList<Uri> uris = new ArrayList<>(); uris.add(mImageView.getBitmapUri(currentView, "image")); uris.add(otherFragment.getImageView().getBitmapUri(currentView, "image2")); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); intent.setType("image/*"); startActivity(intent); return true; } else { Intent intent = new Intent(Intent.ACTION_SEND); Uri uri = mImageView.getBitmapUri(currentView, "image"); intent.putExtra(Intent.EXTRA_STREAM, uri); intent.setType("image/*"); startActivity(intent); return true; } } }); popup.inflate(R.menu.menu_image_share); if (otherFragment == null) { popup.getMenu().removeItem(R.id.action_share_both_images); popup.getMenu().removeItem(R.id.action_share_both_image_views); } popup.show(); } /** * Get the displayed EyePhoto. * * @return The displayed EyePhoto. */ protected OverlayPinchImageView getImageView() { return mImageView; } /** * Store the comment in the image. * * @param comment The comment text to be stored. */ public final void storeComment(final String comment) { mImageView.storeComment(comment); } /** * Show or hide the utilities (overlay bar, scrollbars). */ private void showUtilities() { View fragmentView = getView(); if (fragmentView == null) { return; } // Icon if (isLandscape()) { mToolsButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, mShowUtilities.mArrowUp ? R.drawable.ic_tools_left : R.drawable.ic_tools_right, 0); } else { mToolsButton.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, mShowUtilities.mArrowUp ? R.drawable.ic_tools_up : R.drawable.ic_tools_down); } // Separator bar fragmentView.findViewById(R.id.separatorTools).setVisibility(mShowUtilities == UtilitiyStatus.SHOW_NOTHING ? View.GONE : View.VISIBLE); // Brightness/contrast seekbars int brightnessContrastVisibility = mShowUtilities == UtilitiyStatus.OVERLAY_BRIGHTNESS_CONTRAST || mShowUtilities == UtilitiyStatus.SHOW_EVERYTHING ? View.VISIBLE : View.GONE; fragmentView.findViewById(R.id.seekBarBrightnessLayout).setVisibility(brightnessContrastVisibility); fragmentView.findViewById(R.id.seekBarContrastLayout).setVisibility(brightnessContrastVisibility); // Saturation/color temperature seekbars int saturationColorTemperatureVisibility = mShowUtilities == UtilitiyStatus.OVERLAY_SATURATION_COLOR_TEMPERATURE || mShowUtilities == UtilitiyStatus.SHOW_EVERYTHING ? View.VISIBLE : View.GONE; fragmentView.findViewById(R.id.seekBarSaturationLayout).setVisibility(saturationColorTemperatureVisibility); fragmentView.findViewById(R.id.seekBarColorTemperatureLayout).setVisibility(saturationColorTemperatureVisibility); if (mShowUtilities == UtilitiyStatus.SHOW_NOTHING) { fragmentView.findViewById(R.id.buttonOverlayLayout).setVisibility(View.GONE); mGuidedTopoSetupButton.setVisibility(View.GONE); if (mOverlayStatus == OverlayStatus.NON_JPEG) { mCommentButton.setVisibility(View.GONE); mSaveButton.setVisibility(View.GONE); } } else { if (mOverlayStatus == OverlayStatus.ALLOWED) { fragmentView.findViewById(R.id.buttonOverlayLayout).setVisibility(View.VISIBLE); mGuidedTopoSetupButton.setVisibility(View.GONE); } else if (mOverlayStatus == OverlayStatus.FORBIDDEN) { fragmentView.findViewById(R.id.buttonOverlayLayout).setVisibility(View.GONE); mGuidedTopoSetupButton.setVisibility(View.GONE); } else if (mOverlayStatus == OverlayStatus.NON_JPEG) { fragmentView.findViewById(R.id.buttonOverlayLayout).setVisibility(View.VISIBLE); mGuidedTopoSetupButton.setVisibility(View.GONE); mCommentButton.setVisibility(View.GONE); mSaveButton.setVisibility(View.GONE); mLockButton.setVisibility(View.GONE); mPupilButton.setVisibility(View.GONE); } else { fragmentView.findViewById(R.id.buttonOverlayLayout).setVisibility(View.GONE); mGuidedTopoSetupButton.setVisibility(View.VISIBLE); } } requestLayout(); } /** * Get the value indicating what utilities should be shown. * * @return The value indicating if utilities should be shown. */ // OVERRIDABLE protected UtilitiyStatus getDefaultShowUtilitiesValue() { UtilitiyStatus level = UtilitiyStatus.fromResourceValue( PreferenceUtil.getSharedPreferenceInt(R.string.key_internal_show_utilities_fullscreen, -1)); if (level == null) { // call this method only if no value is set level = UtilitiyStatus.SHOW_EVERYTHING; } return level; } /** * Update default for showing utilities. * * @param utilityStatus the new default value for showing utilities. */ // OVERRIDABLE protected void updateDefaultShowUtilities(final UtilitiyStatus utilityStatus) { PreferenceUtil.setSharedPreferenceInt(R.string.key_internal_show_utilities_fullscreen, utilityStatus.getNumericValue()); } @Override public final void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable("showUtilities", mShowUtilities); outState.putInt("overlayColor", mOverlayColor); outState.putBoolean("lockButtonIsChecked", mLockButton.isChecked()); outState.putSerializable("pupilButtonStatus", mPupilButtonStatus); outState.putSerializable("overlayStatus", mOverlayStatus); } /** * Initialize images - to be called after the views have restored instance state. */ public final void initializeImages() { if (mType == TYPE_FILERESOURCE) { mImageView.setImage(mFileResource, getActivity(), mImageIndex); } else { mImageView.setImage(mFile, getActivity(), mImageIndex); } if (mImageView.getEyePhoto().getRightLeft() == null && mRightLeft != null) { mImageView.getEyePhoto().setRightLeft(mRightLeft); } } /** * Trigger redrawing of the imageView from outside. */ public final void requestLayout() { mImageView.post(new Runnable() { @Override public void run() { mImageView.requestLayout(); mImageView.invalidate(); mImageView.setVisibility(View.VISIBLE); } }); } /** * Method indicating if images should be displayed in full resolution automatically. * * @return true if should be shown in full resolution. */ private boolean hasAutoFullResolution() { int fullResolutionFlag = PreferenceUtil.getSharedPreferenceIntString(R.string.key_full_resolution, null); if (getActivity().getClass().equals(DisplayOneActivity.class)) { return fullResolutionFlag > 0; } else { return fullResolutionFlag == 2; } } /** * Implementation of OnColorSelectedListener, to react on selected overlay color. * * @param color the selected color. */ @Override public final void onColorSelected(final int color) { mImageView.setOverlayColor(color); mOverlayColor = color; } // Implementation of GuiElementUpdater @Override public final void setLockChecked(final boolean checked) { mLockButton.setChecked(checked); mPupilButton.setEnabled(checked); // Set overlay status according to the lock. if (mOverlayStatus != OverlayStatus.GUIDE_IRIS && mOverlayStatus != OverlayStatus.GUIDE_PUPIL && mOverlayStatus != OverlayStatus.NON_JPEG) { if (mImageView.canHandleOverlays() || mRightLeft != null) { if (PreferenceUtil.getSharedPreferenceBoolean(R.string.key_guided_topo_setup) && !checked) { mOverlayStatus = OverlayStatus.GUIDED; mGuidedTopoSetupButton.setEnabled(true); } else { mOverlayStatus = OverlayStatus.ALLOWED; } } else { mOverlayStatus = OverlayStatus.FORBIDDEN; } } showUtilities(); } @Override public final void updateSeekbarBrightness(final float brightness) { float progress = (brightness + 1) * mSeekbarBrightness.getMax() / 2; mSeekbarBrightness.setProgress(Float.valueOf(progress).intValue()); } @Override public final void updateSeekbarContrast(final float contrast) { float progress = (contrast + 1) * mSeekbarContrast.getMax() / 2; mSeekbarContrast.setProgress(Float.valueOf(progress).intValue()); } @Override public final void updateSeekbarSaturation(final float saturation) { float progress = (saturation + 1) * mSeekbarSaturation.getMax() / 2; mSeekbarSaturation.setProgress(Float.valueOf(progress).intValue()); } @Override public final void updateSeekbarColorTemperature(final float colorTemperature) { float progress = (colorTemperature + 1) * mSeekbarColorTemperature.getMax() / 2; mSeekbarColorTemperature.setProgress(Float.valueOf(progress).intValue()); } @Override public final void updateOverlayColorButton(final int color) { mSelectColorButton.setTextColor(color); } @Override public final int getOverlayDefaultColor() { return mOverlayColor; } @Override public final void resetOverlays() { for (int i = 0; i < OVERLAY_BUTTON_COUNT; i++) { mToggleOverlayButtons[i].setChecked(false); } } /* * Ensure that the full size bitmap is cleaned from memory if memory is low. */ @Override public final void onTrimMemory(final int level) { mImageView.cleanFullBitmap(); super.onTrimMemory(level); } /** * Set the bitmap of the pupil button. */ private void setPupilButtonBitmap() { int pupilButtonResource = 0; switch (mPupilButtonStatus) { case OFF: pupilButtonResource = R.drawable.ic_btn_pupil_0; break; case CENTER: pupilButtonResource = R.drawable.ic_btn_pupil_1; break; case MOVE: pupilButtonResource = R.drawable.ic_btn_pupil_2; break; default: break; } ImageSpan imageSpan = new ImageSpan(getActivity(), pupilButtonResource); SpannableString content = new SpannableString("X"); content.setSpan(imageSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); mPupilButton.setText(content); mPupilButton.setTextOn(content); mPupilButton.setTextOff(content); mPupilButton.setChecked(mPupilButtonStatus != PupilButtonStatus.OFF); } /** * Base implementation of OnSeekBarChangeListener, used for brightness and contrast seekbars. */ private abstract class OnSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener { @Override public void onStopTrackingTouch(final SeekBar seekBar) { mImageView.refresh(); } @Override public void onStartTrackingTouch(final SeekBar seekBar) { // do nothing } } /** * Status of the button to move the pupil. */ private enum PupilButtonStatus { /** * Button is off. */ OFF, /** * Button is configured for keeping pupil in the center. */ CENTER, /** * Button allows to move the pupil. */ MOVE } /** * Status of the overlay usage. */ public enum OverlayStatus { /** * Overlays cannot be used. */ FORBIDDEN, /** * Overlay guided setup can be used. */ GUIDED, /** * The guide is active to set the iris size. */ GUIDE_IRIS, /** * The guide is active to set the pupil size. */ GUIDE_PUPIL, /** * Overlays can be used. */ ALLOWED, /** * Special handling for non-JPEG images. */ NON_JPEG } /** * The display status of utilities. */ public enum UtilitiyStatus { /** * Show nothing. */ SHOW_NOTHING(1, true), /** * Show only the overlay pane. */ ONLY_OVERLAY(2, true), /** * Show the overlay pane and the brightness/contrast sliders. */ OVERLAY_BRIGHTNESS_CONTRAST(3, true), /** * Show the overlay pane and the saturation/color temparature sliders. */ SHOW_EVERYTHING(4, false), /** * Show the overlay pane and the saturation/color temparature sliders. */ OVERLAY_SATURATION_COLOR_TEMPERATURE(5, false); /** * The numeric value. */ private final int mNumericValue; /** * Flag indicating if the "up" or "down" arrow should be displayed on the "show utilities" button. */ private final boolean mArrowUp; protected int getNumericValue() { return mNumericValue; } /** * A map from the resourceValue to the color. */ private static final SparseArray<UtilitiyStatus> UTILITY_STATUS_MAP = new SparseArray<>(); static { for (UtilitiyStatus utilityStatus : UtilitiyStatus.values()) { UTILITY_STATUS_MAP.put(utilityStatus.mNumericValue, utilityStatus); } } /** * Constructor giving the resourceValue and the color value. * * @param numericValue The numeric value. * @param arrowUp Flag indicating if the "up" or "down" arrow should be displayed on the "show utilities" button. */ UtilitiyStatus(final int numericValue, final boolean arrowUp) { mNumericValue = numericValue; mArrowUp = arrowUp; } /** * Get the utility status from its numeric value. * * @param numericValue The resource value. * @return The corresponding UtilityStatus. */ protected static UtilitiyStatus fromResourceValue(final int numericValue) { return UTILITY_STATUS_MAP.get(numericValue); } /** * The next status after pressing the "show utilities" button. * * @param alwaysShowOverlayBar Flag indicating if the overlay bar should always be shown. * @param allowAllBars Flag indicating if all bars may be shown at once. * @return the next status. */ private UtilitiyStatus getNextStatus(final boolean alwaysShowOverlayBar, final boolean allowAllBars) { switch (this) { case SHOW_NOTHING: return ONLY_OVERLAY; case ONLY_OVERLAY: return OVERLAY_BRIGHTNESS_CONTRAST; case OVERLAY_BRIGHTNESS_CONTRAST: return allowAllBars ? SHOW_EVERYTHING : OVERLAY_SATURATION_COLOR_TEMPERATURE; case SHOW_EVERYTHING: return OVERLAY_SATURATION_COLOR_TEMPERATURE; case OVERLAY_SATURATION_COLOR_TEMPERATURE: default: return alwaysShowOverlayBar ? ONLY_OVERLAY : SHOW_NOTHING; } } } }