commit a742d66bd4393fce3aba9f23f5bec27bfd0e0849
Author: leszek <leszek@koltunski.pl>
Date:   Sun Dec 29 11:58:15 2024 +0100

    If an object has more than 8 colors, in the solver's SetupPosition screen display the bitmaps in two rows.

diff --git a/src/main/java/org/distorted/dialogs/RubikDialogSolverView.java b/src/main/java/org/distorted/dialogs/RubikDialogSolverView.java
index d4f81a37..a9d269f2 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogSolverView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogSolverView.java
@@ -21,7 +21,7 @@ import org.distorted.objectlib.main.TwistyObject;
 import org.distorted.objectlib.solvers.verifiers.SolverAbstract;
 import org.distorted.objectlib.solvers.verifiers.SolvingList;
 import org.distorted.solverui.ScreenList;
-import org.distorted.solverui.ScreenSolver;
+import org.distorted.solverui.ScreenSetupPosition;
 import org.distorted.solverui.SolverActivity;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -59,7 +59,7 @@ public class RubikDialogSolverView
         OperatingSystemInterface os = act.getInterface();
         TwistyObject object = act.getObject();
         SolverAbstract solver = list.create(os,object);
-        ScreenSolver screen = (ScreenSolver)ScreenList.SVER.getScreenClass();
+        ScreenSetupPosition screen = (ScreenSetupPosition)ScreenList.SVER.getScreenClass();
 
         if( solver!=null )
           {
diff --git a/src/main/java/org/distorted/solverui/ScreenList.java b/src/main/java/org/distorted/solverui/ScreenList.java
index 73fed0e2..6033426c 100644
--- a/src/main/java/org/distorted/solverui/ScreenList.java
+++ b/src/main/java/org/distorted/solverui/ScreenList.java
@@ -18,9 +18,9 @@ import android.content.SharedPreferences;
 
 public enum ScreenList
   {
-  SVER ( null , MODE_REPLACE, new ScreenSolver()         ),
-  SOLU ( SVER , MODE_DRAG   , new ScreenSolution()       ),
-  PHAS ( SVER , MODE_DRAG   , new ScreenPhasedSolution() ),
+  SVER ( null , MODE_REPLACE, new ScreenSetupPosition()        ),
+  SOLU ( SVER , MODE_DRAG   , new ScreenSolutionSinglephased() ),
+  PHAS ( SVER , MODE_DRAG   , new ScreenSolutionMultiphased()  ),
   ;
 
   public static final int LENGTH = values().length;
diff --git a/src/main/java/org/distorted/solverui/ScreenPhasedSolution.java b/src/main/java/org/distorted/solverui/ScreenPhasedSolution.java
deleted file mode 100644
index 59f17312..00000000
--- a/src/main/java/org/distorted/solverui/ScreenPhasedSolution.java
+++ /dev/null
@@ -1,578 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2024 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import android.content.SharedPreferences;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.library.effect.PostprocessEffectGlow;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.type.Dynamic2D;
-import org.distorted.library.type.Dynamic4D;
-import org.distorted.library.type.Static2D;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.R;
-import org.distorted.objectlib.helpers.MovesFinished;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObject;
-
-import java.lang.ref.WeakReference;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenPhasedSolution extends ScreenAbstract implements MovesFinished, EffectListener
-  {
-  private static final int MOVES_PLACE_0 = 100;
-  private static final int MOVES_PLACE_1 = 101;
-  private static final int FLASH_TIME = 1200;
-  private static final int MILLIS_PER_DEGREE = 6;
-
-  private WeakReference<SolverActivity> mAct;
-  private TransparentImageButton mPrevButton, mNextButton, mBackButton, mPrevPhase, mNextPhase;
-  private float mButtonSize;
-
-  private TextView mMovesText, mMovesPhase;
-  private String[] mPhaseNames;
-  private int mNumPhases;
-  private int[][][] mMoves;
-  private int[][] mCubitsNotInvolved;
-  private int mNumMoves,mCurrMove,mCurrPhase;
-  private boolean mCanMove;
-
-  private Dynamic2D mHaloAndRadiusDyn;
-  private Dynamic4D mColorDyn;
-  private PostprocessEffectGlow mGlow;
-  private boolean mEffectWorking;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(SolverActivity act)
-    {
-    ObjectControl control = act.getControl();
-    control.solveOnly();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final SolverActivity act)
-    {
-    mAct = new WeakReference<>(act);
-
-    mHaloAndRadiusDyn = new Dynamic2D(FLASH_TIME,1.0f);
-    mHaloAndRadiusDyn.add(new Static2D( 0,0));
-    mHaloAndRadiusDyn.add(new Static2D(10,5));
-
-    mColorDyn = new Dynamic4D(FLASH_TIME,1.0f);
-
-    final int[] colors  = new int[] {1,1,1}; // white
-
-    Static4D P1 = new Static4D(colors[0],colors[1],colors[2], 0.0f);
-    Static4D P2 = new Static4D(colors[0],colors[1],colors[2], 1.0f);
-    mColorDyn.add(P1);
-    mColorDyn.add(P2);
-
-    mGlow = new PostprocessEffectGlow(mHaloAndRadiusDyn,mColorDyn);
-
-    float width = act.getScreenWidthInPixels();
-    mButtonSize = width*SolverActivity.BUTTON_TEXT_SIZE;
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-
-    setupPrevPhase(act);
-    setupNextPhase(act);
-    setupTextPhase(act,width);
-
-    layoutTop.addView(mPrevPhase);
-    layoutTop.addView(mMovesPhase);
-    layoutTop.addView(mNextPhase);
-
-    // BOT ////////////////////////////
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
-
-    LinearLayout layoutLeft = new LinearLayout(act);
-    layoutLeft.setLayoutParams(paramsL);
-    LinearLayout layoutMid = new LinearLayout(act);
-    layoutMid.setLayoutParams(paramsM);
-    LinearLayout layoutRight = new LinearLayout(act);
-    layoutRight.setLayoutParams(paramsR);
-
-    setupPrevButton(act);
-    setupNextButton(act);
-    setupTextView(act,width);
-
-    layoutLeft.addView(mPrevButton);
-    layoutLeft.addView(mMovesText);
-    layoutLeft.addView(mNextButton);
-
-    setupBackButton(act);
-
-    layoutRight.addView(mBackButton);
-
-    layoutBot.addView(layoutLeft);
-    layoutBot.addView(layoutMid);
-    layoutBot.addView(layoutRight);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override public void onClick(View v) { backMove(); }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupNextButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
-
-    mNextButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override public void onClick(View v) { nextMove(); }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupTextView(final SolverActivity act, final float width)
-    {
-    int padding = (int)(width*SolverActivity.PADDING);
-    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
-    params.topMargin    = margin;
-    params.bottomMargin = margin;
-    params.leftMargin   = margin;
-    params.rightMargin  = margin;
-
-    mMovesText = new TextView(act);
-    mMovesText.setTextSize(20);
-    mMovesText.setLayoutParams(params);
-    mMovesText.setPadding(padding,0,padding,0);
-    mMovesText.setGravity(Gravity.CENTER);
-    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevPhase(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mPrevPhase = new TransparentImageButton(act,R.drawable.ui_left,params);
-
-    mPrevPhase.setOnClickListener( new View.OnClickListener()
-      {
-      @Override public void onClick(View v) { backPhase(); }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupNextPhase(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mNextPhase = new TransparentImageButton(act,R.drawable.ui_right,params);
-
-    mNextPhase.setOnClickListener( new View.OnClickListener()
-      {
-      @Override public void onClick(View v) { nextPhase(); }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupTextPhase(final SolverActivity act, final float width)
-    {
-    int padding = (int)(width*SolverActivity.PADDING);
-    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
-    params.topMargin    = margin;
-    params.bottomMargin = margin;
-    params.leftMargin   = margin;
-    params.rightMargin  = margin;
-
-    mMovesPhase = new TextView(act);
-    mMovesPhase.setTextSize(20);
-    mMovesPhase.setLayoutParams(params);
-    mMovesPhase.setPadding(padding,0,padding,0);
-    mMovesPhase.setGravity(Gravity.CENTER);
-    mMovesPhase.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
-    mMovesPhase.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor) { }
-  public void restorePreferences(SharedPreferences preferences) { }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void backPhase()
-    {
-    SolverActivity act = mAct.get();
-    ObjectControl control = act.getControl();
-
-    if( mCurrMove>0 )
-      {
-      int[][] moves = transformMoves(mMoves[mCurrPhase],0,mCurrMove, false);
-      control.applyScrambles(moves);
-      mCurrMove = 0;
-      }
-    else if( mCurrPhase>0 )
-      {
-      mCurrPhase--;
-      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-      mCurrMove = 0;
-      int[][] moves = transformMoves(mMoves[mCurrPhase],0,mNumMoves, false);
-      control.applyScrambles(moves);
-      }
-    else
-      {
-      mCurrPhase = mNumPhases-1;
-      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-      mCurrMove = mNumMoves;
-      int[][] moves = transformMoves(mMoves, true);
-      control.applyScrambles(moves);
-      }
-
-    setText(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void nextPhase()
-    {
-    SolverActivity act = mAct.get();
-    ObjectControl control = act.getControl();
-
-    if( mCurrPhase<mNumPhases-1 )
-      {
-      glowCubits(mCubitsNotInvolved[mCurrPhase]);
-      int[][] moves = transformMoves(mMoves[mCurrPhase],mCurrMove,mNumMoves, true);
-      control.applyScrambles(moves);
-      mCurrPhase++;
-      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-      mCurrMove = 0;
-      }
-    else if( mCurrMove<mNumMoves )
-      {
-      glowCubits(mCubitsNotInvolved[mCurrPhase]);
-      int[][] moves = transformMoves(mMoves[mCurrPhase],mCurrMove,mNumMoves, true);
-      control.applyScrambles(moves);
-      mCurrMove = mNumMoves;
-      }
-    else
-      {
-      mCurrPhase = 0;
-      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-      mCurrMove = 0;
-      int[][] moves = transformMoves(mMoves, false);
-      control.applyScrambles(moves);
-      }
-
-    setText(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setText(SolverActivity act)
-    {
-    int currMove = 0;
-    int totalMove = 0;
-
-    if( mMoves!=null )
-      {
-      currMove = mCurrMove;
-      for(int p=0; p<mCurrPhase; p++) currMove  += (mMoves[p]==null ? 0: mMoves[p].length);
-      for(int p=0; p<mNumPhases; p++) totalMove += (mMoves[p]==null ? 0: mMoves[p].length);
-      }
-
-    final int cMove = currMove;
-    final int tMove = totalMove;
-
-    act.runOnUiThread(new Runnable()
-      {
-      @Override
-      public void run()
-        {
-        mMovesPhase.setText(mPhaseNames[mCurrPhase]+" "+mCurrMove+"/"+mNumMoves);
-        mMovesText.setText(cMove+"/"+tMove);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int[][] transformMoves(int[][] moves, int start, int end, boolean front)
-    {
-    int mult = front ? 1:-1;
-    int len = end-start;
-    int[][] ret = new int[len][];
-
-    for(int m=0; m<len; m++)
-      {
-      int[] mv = moves[front ? start+m : end-1-m];
-      int[] rt = new int[3];
-      rt[0] = mv[0];
-      rt[1] = (1<<mv[1]);
-      rt[2] = mult*mv[2];
-      ret[m] = rt;
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int[][] transformMoves(int[][][] moves, boolean front)
-    {
-    int len = moves.length;
-    int totalLen = 0;
-    for (int[][] move : moves) totalLen += (move==null ? 0 : move.length);
-
-    int[][] ret = new int[totalLen][];
-    int mult = front ? 1:-1;
-    int index = 0;
-
-    for(int m=0; m<len; m++)
-      {
-      int[][] mv = moves[front ? m : len-1-m];
-      int l = (mv==null ? 0 : mv.length);
-
-      for(int p=0; p<l; p++)
-        {
-        int[] mve = mv[front ? p : l-1-p];
-        int[] rt = new int[3];
-        rt[0] = mve[0];
-        rt[1] = (1<<mve[1]);
-        rt[2] = mult*mve[2];
-        ret[index++] = rt;
-        }
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void glowCubits(int[] cubits)
-    {
-    if( !mEffectWorking )
-      {
-      mEffectWorking = true;
-      SolverActivity act=mAct.get();
-      ObjectControl control = act.getControl();
-      TwistyObject object=control.getObject();
-      DistortedEffects effects=object.getObjectEffects();
-      effects.apply(mGlow);
-
-      MeshBase mesh=object.getObjectMesh();
-      mesh.setComponentsNotAffectedByPostprocessing(cubits);
-
-      mHaloAndRadiusDyn.resetToBeginning();
-      mColorDyn.resetToBeginning();
-      mGlow.notifyWhenFinished(this);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int[] computeCubitsNotInvolved(int[][] subphases, int numCubits)
-    {
-    int numCubitsInvolved = 0;
-    boolean[] involved = new boolean[numCubits];
-
-    for(int[] sub : subphases)
-      if( sub!=null )
-        for(int s : sub)
-          {
-          numCubitsInvolved++;
-          involved[s] = true;
-          }
-
-    int[] ret = new int[numCubits-numCubitsInvolved];
-    int index = 0;
-
-    for(int c=0; c<numCubits; c++)
-      if( !involved[c] ) ret[index++] = c;
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void backMove()
-    {
-    if( mMoves!=null && mCanMove )
-      {
-      SolverActivity act=mAct.get();
-
-      if( mCurrMove>0 )
-        {
-        mCanMove = false;
-        int[] move = mMoves[mCurrPhase][--mCurrMove];
-        ObjectControl control = act.getControl();
-        control.blockTouch(MOVES_PLACE_0);
-        control.addRotation(this, move[0], (1<<move[1]), -move[2], MILLIS_PER_DEGREE);
-        }
-      else if( mCurrPhase>0 )
-        {
-        mCurrPhase--;
-        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-        mCurrMove = mNumMoves;
-        glowCubits(mCubitsNotInvolved[mCurrPhase]);
-        }
-      else
-        {
-        mCurrPhase = mNumPhases-1;
-        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-        mCurrMove = mNumMoves;
-        int[][] moves = transformMoves(mMoves, true);
-        ObjectControl control = act.getControl();
-        control.applyScrambles(moves);
-        glowCubits(mCubitsNotInvolved[mCurrPhase]);
-        }
-
-      setText(act);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void nextMove()
-    {
-    if( mMoves!=null && mCanMove )
-      {
-      SolverActivity act=mAct.get();
-
-      if( mCurrMove<mNumMoves )
-        {
-        mCanMove = false;
-        int[] move = mMoves[mCurrPhase][mCurrMove++];
-        ObjectControl control = act.getControl();
-        control.blockTouch(MOVES_PLACE_1);
-        control.addRotation(this, move[0], (1<<move[1]), move[2], MILLIS_PER_DEGREE);
-        if( mCurrMove==mNumMoves && mCurrPhase==mNumPhases-1 ) glowCubits(mCubitsNotInvolved[mCurrPhase]);
-        }
-      else if( mCurrPhase<mNumPhases-1 )
-        {
-        mCurrPhase++;
-        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-        mCurrMove = 0;
-        glowCubits(mCubitsNotInvolved[mCurrPhase-1]);
-        }
-      else
-        {
-        mCurrPhase = 0;
-        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-        mCurrMove = 0;
-        int[][] moves = transformMoves(mMoves, false);
-        ObjectControl control = act.getControl();
-        control.applyScrambles(moves);
-        }
-
-      setText(act);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void updateNames(String[] names)
-    {
-    mPhaseNames = names;
-    mNumPhases = names.length;
-    mMoves = new int[mNumPhases][][];
-    mCubitsNotInvolved = new int[mNumPhases][];
-    mCanMove = true;
-    if( mAct!=null ) setSolution(null,0,null);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setSolution(int[][] moves, int phase, int[][] subphases)
-    {
-    SolverActivity act=mAct.get();
-
-    if( subphases!=null )
-      {
-      ObjectControl control=act.getControl();
-      TwistyObject object=control.getObject();
-      int numCubits=object.getNumCubits();
-      mCubitsNotInvolved[phase]= computeCubitsNotInvolved(subphases, numCubits);
-      }
-
-    mMoves[phase] = moves;
-    if( phase==0 ) mNumMoves = (moves==null ? 0 : moves.length);
-    mCurrPhase = 0;
-    mCurrMove = 0;
-
-    setText(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onActionFinished(final long effectID)
-    {
-    mCanMove = true;
-    SolverActivity act=mAct.get();
-    ObjectControl control = act.getControl();
-    control.unblockRotation();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void effectFinished(long id)
-    {
-    SolverActivity act=mAct.get();
-    ObjectControl control = act.getControl();
-    TwistyObject object=control.getObject();
-    DistortedEffects effects=object.getObjectEffects();
-    effects.abortById(id);
-    mEffectWorking = false;
-    }
-  }
diff --git a/src/main/java/org/distorted/solverui/ScreenSetupPosition.java b/src/main/java/org/distorted/solverui/ScreenSetupPosition.java
new file mode 100644
index 00000000..eb148d49
--- /dev/null
+++ b/src/main/java/org/distorted/solverui/ScreenSetupPosition.java
@@ -0,0 +1,831 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solverui;
+
+import static org.distorted.objectlib.metadata.ListObjects.*;
+import static org.distorted.objectlib.solvers.verifiers.SolverTablebase.*;
+
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.core.content.ContextCompat;
+
+import org.distorted.dialogs.RubikDialogSolverError;
+import org.distorted.dialogs.RubikDialogSolverImpossible;
+import org.distorted.dialogs.RubikDialogSolvers;
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.MainActivity;
+import org.distorted.main.R;
+import org.distorted.objectlib.helpers.OperatingSystemInterface;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objectlib.metadata.ListObjects;
+import org.distorted.objectlib.shape.*;
+import org.distorted.objectlib.solvers.verifiers.ResultScreen;
+import org.distorted.objectlib.solvers.verifiers.SolverAbstract;
+import org.distorted.objectlib.solvers.verifiers.SolvingList;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenSetupPosition extends ScreenAbstract implements ResultScreen
+  {
+  private static final int RESET_DURATION = 1000;
+  private static final int MODE_NORMAL = 0;
+  private static final int MODE_DINO_4 = 1;
+
+  private static final int[][] colorsHex =
+    {
+      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
+      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
+      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
+      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
+      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
+      {R.string.color_orange1,R.string.color_orange2,R.string.color_orange3,R.string.color_orange4,R.string.color_orange5,R.string.color_orange6,R.string.color_orange7 },
+    };
+  private static final int[][] colorsTet =
+    {
+      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
+      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
+      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
+      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
+    };
+  private static final int[][] colorsOct =
+    {
+      {R.string.color_violet1,R.string.color_violet2,R.string.color_violet3,R.string.color_violet4,R.string.color_violet5,R.string.color_violet6,R.string.color_violet7 },
+      {R.string.color_grey1  ,R.string.color_grey2  ,R.string.color_grey3  ,R.string.color_grey4  ,R.string.color_grey5  ,R.string.color_grey6  ,R.string.color_grey7   },
+      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
+      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
+      {R.string.color_orange1,R.string.color_orange2,R.string.color_orange3,R.string.color_orange4,R.string.color_orange5,R.string.color_orange6,R.string.color_orange7 },
+      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
+      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
+      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
+    };
+  private static final int[][] colorsDi4 =
+    {
+      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
+      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
+      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
+      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
+    };
+
+  private static Bitmap[] mBitmap;
+  private ImageButton[] mColorButton;
+  private TransparentImageButton mResetButton,mBackButton, mSolveButton;
+  private boolean mSolving;
+  private int mCurrentColor, mCurrentButton;
+  private int[] mFaceColors;
+  private int mColorMode;
+  private int mNumColors;
+  private int mNumBitmapRows;
+  private float mBitmapSize;
+  private WeakReference<SolverActivity> mWeakAct;
+  private int mObjectOrdinal;
+  private String[] mPhaseNames;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(SolverActivity act)
+    {
+    ObjectControl control = act.getControl();
+    control.unsetLock();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final SolverActivity act)
+    {
+    ObjectControl control = act.getControl();
+    control.setLock(false);
+
+    float width = act.getScreenWidthInPixels();
+    float heigh = act.getScreenHeightInPixels();
+
+    mWeakAct = new WeakReference<>(act);
+    mSolving = false;
+    mPhaseNames = null;
+
+    mObjectOrdinal = act.getObjectOrdinal();
+    control.solveOnly();
+    generateFaceColors(mObjectOrdinal);
+
+    mNumBitmapRows = mNumColors>8 ? 2 : 1;
+    mBitmapSize = computeBitmapSize(width,heigh);
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+
+    if( mNumColors>0 )
+      {
+      setupBitmaps();
+      setupColorButtons(act);
+      markButton(act);
+      addButtonsToTopLayout(act,layoutTop);
+      }
+
+    // BOT ////////////////////////////
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
+
+    LinearLayout layoutL = new LinearLayout(act);
+    layoutL.setLayoutParams(params);
+    LinearLayout layoutM = new LinearLayout(act);
+    layoutM.setLayoutParams(params);
+    LinearLayout layoutR = new LinearLayout(act);
+    layoutR.setLayoutParams(params);
+
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    setupResetButton(act);
+    setupSolveButton(act);
+    setupBackButton(act);
+
+    layoutL.addView(mResetButton);
+    layoutM.addView(mSolveButton);
+    layoutR.addView(mBackButton);
+
+    layoutBot.addView(layoutL);
+    layoutBot.addView(layoutM);
+    layoutBot.addView(layoutR);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float computeBitmapSize(float width, float heigh)
+    {
+    final float BUTTON_RATIO = 0.75f;
+    float sizeV = (heigh/mNumBitmapRows)*MainActivity.RATIO_BAR;
+    float sizeH = (width*mNumBitmapRows)/mNumColors;
+
+    return BUTTON_RATIO*Math.min(sizeV,sizeH);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void addButtonsToTopLayout(SolverActivity act, LinearLayout layout)
+    {
+    if( mNumBitmapRows==1 )
+      {
+      for(ImageButton button: mColorButton) layout.addView(button);
+      }
+    else if( mNumBitmapRows==2 )
+      {
+      LinearLayout layoutV = new LinearLayout(act);
+      layoutV.setOrientation(LinearLayout.VERTICAL);
+      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
+      layoutV.setLayoutParams(params);
+      LinearLayout layoutT = new LinearLayout(act);
+      layoutT.setOrientation(LinearLayout.HORIZONTAL);
+      LinearLayout layoutB = new LinearLayout(act);
+      layoutB.setOrientation(LinearLayout.HORIZONTAL);
+
+      int numB = mColorButton.length;
+      for(int b=0     ; b<numB/2; b++) layoutT.addView(mColorButton[b]);
+      for(int b=numB/2; b<numB  ; b++) layoutB.addView(mColorButton[b]);
+
+      layoutV.addView(layoutT);
+      layoutV.addView(layoutB);
+      layout.addView(layoutV);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// This doesn't quite work in many cases, but in case of the solvers that will pop up in foreseeable
+// future it should be ok.
+
+  public void generateFaceColors(int object)
+    {
+    mColorMode = MODE_NORMAL;
+
+    if( object==PYRA_3.ordinal() ||
+        object==PYRA_4.ordinal() ||
+        object==PYRA_5.ordinal() ||
+        object==PDUO_2.ordinal() ||
+        object==JING_2.ordinal() ||
+        object==MORP_2.ordinal() ||
+        object==MORP_3.ordinal() ||
+        object==MORP_4.ordinal()  )
+      {
+      mNumColors  = ShapeTetrahedron.NUM_FACES;
+      mFaceColors = ShapeTetrahedron.FACE_COLORS;
+      }
+    else if( object==DIAM_2.ordinal() ||
+             object==DIAM_3.ordinal() ||
+             object==DIAM_4.ordinal() ||
+             object==TRAJ_3.ordinal() ||
+             object==TRAJ_4.ordinal() ||
+             object==PDIA_3.ordinal()  )
+      {
+      mNumColors  = ShapeOctahedron.NUM_FACES;
+      mFaceColors = ShapeOctahedron.FACE_COLORS;
+      }
+    else if( object==CRYS_3.ordinal() ||
+             object==STAR_3.ordinal() ||
+             object==PENT_2.ordinal() ||
+             object==KILO_3.ordinal() ||
+             object==KILO_5.ordinal() ||
+             object==MEGA_3.ordinal() ||
+             object==MEGA_5.ordinal()  )
+      {
+      mNumColors  = ShapeDodecahedron.NUM_FACES;
+      mFaceColors = ShapeDodecahedron.FACE_COLORS;
+      }
+    else if( object==BALL_4.ordinal() )
+      {
+      mNumColors  = ShapeDiamond.NUM_FACES;
+      mFaceColors = ShapeDiamond.FACE_COLORS;
+      }
+    else if( object==ICOS_2.ordinal() )
+      {
+      mNumColors  = ShapeIcosahedron.NUM_FACES;
+      mFaceColors = ShapeIcosahedron.FACE_COLORS;
+      }
+    else if( object==DIN4_3.ordinal() )
+      {
+      mNumColors  = 4;
+      mFaceColors = new int[] { ShapeColors.COLOR_YELLOW, ShapeColors.COLOR_RED, ShapeColors.COLOR_BLUE, ShapeColors.COLOR_WHITE};
+      mColorMode  = MODE_DINO_4;
+      }
+    else
+      {
+      mNumColors  = ShapeHexahedron.NUM_FACES;
+      mFaceColors = ShapeHexahedron.FACE_COLORS;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBitmaps()
+    {
+    final int SIZE = (int)mBitmapSize;
+    final float R = SIZE*0.15f;
+    final float M = SIZE*0.08f;
+
+    mBitmap = new Bitmap[mNumColors];
+
+    Paint paint = new Paint();
+    paint.setColor(0xff008800);
+    paint.setStyle(Paint.Style.FILL);
+
+    paint.setAntiAlias(true);
+    paint.setTextAlign(Paint.Align.CENTER);
+    paint.setStyle(Paint.Style.FILL);
+
+    for(int i=0; i<mNumColors; i++)
+      {
+      mBitmap[i] = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888);
+      Canvas canvas = new Canvas(mBitmap[i]);
+
+      paint.setColor(0xff000000);
+      canvas.drawRect(0, 0, SIZE, SIZE, paint);
+
+      paint.setColor(mFaceColors[i]);
+      canvas.drawRoundRect( M, M, SIZE-M, SIZE-M, R, R, paint);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int translateColor(int color)
+    {
+    if( mColorMode==MODE_DINO_4 )
+      {
+      int realColor = mFaceColors[color];
+      int[] hexColors = ShapeHexahedron.FACE_COLORS;
+
+      for(int i=0; i<6; i++)
+        if( hexColors[i]==realColor ) return i;
+      }
+
+    return color;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean pressSolve(SolverActivity act)
+    {
+    int[] solverOrdinals = SolvingList.getSolverOrdinals(mObjectOrdinal);
+
+    if( solverOrdinals!=null  )
+      {
+      SolvingList slvList = SolvingList.getSolver(solverOrdinals[0]);
+      OperatingSystemInterface os = act.getInterface();
+      TwistyObject object = act.getObject();
+      SolverAbstract solver = slvList.create(os,object);
+
+      if( solver!=null )
+        {
+        int[] result = solver.validatePosition(object);
+
+        if( result[0]>=0 ) // position is valid
+          {
+          if( solverOrdinals.length==1 ) // just one solver - simply launch it
+            {
+            solver.solve(this,result);
+            return true;
+            }
+          else // more than one solver - pop up a choosing dialog
+            {
+            ListObjects objList = ListObjects.getObject(mObjectOrdinal);
+            String upperName = objList.name();
+            Bundle bundle = new Bundle();
+            bundle.putString("argument", upperName );
+            RubikDialogSolvers solv = new RubikDialogSolvers();
+            solv.setArguments(bundle);
+            solv.show( act.getSupportFragmentManager(), RubikDialogSolvers.getDialogTag() );
+            return false;
+            }
+          }
+        else
+          {
+          displayImpossibleDialog(result,solver.getFaceColors());
+          return false;
+          }
+        }
+      else
+        {
+        displayErrorDialog(act.getString(R.string.solver_generic_not_implemented));
+        return false;
+        }
+      }
+    else  // no solvers? Impossible!
+      {
+      displayErrorDialog("No solvers found for object "+mObjectOrdinal);
+      return false;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void fail(String result) {}
+  public void setPhaseNames(String[] names) { mPhaseNames = names; }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupColorButtons(final SolverActivity act)
+    {
+    mColorButton = new ImageButton[mNumColors];
+
+    for(int i=0; i<mNumColors; i++)
+      {
+      final int ii = i;
+      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.MATCH_PARENT, 1.0f);
+
+      mColorButton[i] = new ImageButton(act);
+      mColorButton[i].setLayoutParams(params);
+      mColorButton[i].setImageBitmap(mBitmap[i]);
+
+      mColorButton[i].setOnClickListener( new View.OnClickListener()
+        {
+        @Override
+        public void onClick(View view)
+          {
+          mCurrentColor = translateColor(ii);
+          mCurrentButton= ii;
+          markButton(act);
+          }
+        });
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupResetButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
+    mResetButton = new TransparentImageButton(act, R.drawable.ui_reset, params);
+
+    mResetButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        control.resetTextureMapsEffect(RESET_DURATION);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupSolveButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mSolveButton = new TransparentImageButton(act,R.drawable.ui_solve,params);
+
+    mSolveButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        if( !mSolving && pressSolve(act) ) mSolving = true;
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        control.resetAllTextureMaps();
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void markButton(SolverActivity act)
+    {
+    if( mCurrentButton>=mNumColors )
+      {
+      mCurrentButton = 0;
+      mCurrentColor = translateColor(0);
+      }
+
+    for(int b=0; b<mNumColors; b++)
+      {
+      Drawable d = mColorButton[b].getBackground();
+      int s = b==mCurrentButton ? act.getSelectedColor() : act.getNormalColor();
+      d.setColorFilter(ContextCompat.getColor(act,s), PorterDuff.Mode.MULTIPLY);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+    editor.putInt("stateSolver_color" , mCurrentColor );
+    editor.putInt("stateSolver_button", mCurrentButton);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+    mCurrentColor = preferences.getInt("stateSolver_color" , 0);
+    mCurrentButton= preferences.getInt("stateSolver_button", 0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCurrentColor()
+    {
+    return mCurrentColor;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setSolved(final String moves)
+    {
+    mSolving = false;
+    final SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      act.runOnUiThread(new Runnable()
+        {
+        @Override
+        public void run()
+          {
+          ScreenList.switchScreen(act, ScreenList.SOLU);
+          ScreenSolutionSinglephased solution = (ScreenSolutionSinglephased) ScreenList.SOLU.getScreenClass();
+          solution.setSolution(moves);
+          if( !moves.isEmpty() ) act.doNotShowDialogAnymore();
+          }
+        });
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setSolved(final int[][] moves, final int phaseNumber, final int[][] subphases)
+    {
+    mSolving = false;
+    final SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      act.runOnUiThread(new Runnable()
+        {
+        @Override
+        public void run()
+          {
+          if( mPhaseNames!=null )
+            {
+            ScreenSolutionMultiphased screen = (ScreenSolutionMultiphased) ScreenList.PHAS.getScreenClass();
+            if( phaseNumber==0 )
+              {
+              ScreenList.switchScreen(act, ScreenList.PHAS);
+              screen.updateNames(mPhaseNames);
+              }
+            screen.setSolution(moves, phaseNumber,subphases);
+            }
+          else
+            {
+            ScreenList.switchScreen(act, ScreenList.SOLU);
+            ScreenSolutionSinglephased screen = (ScreenSolutionSinglephased) ScreenList.SOLU.getScreenClass();
+            screen.setSolution(moves);
+            }
+
+          if( moves!=null && moves.length>0 ) act.doNotShowDialogAnymore();
+          }
+        });
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void displayErrorDialog(String message)
+    {
+    mSolving = false;
+    SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      RubikDialogSolverError dialog = new RubikDialogSolverError();
+      Bundle bundle = new Bundle();
+      bundle.putString("argument", message );
+      dialog.setArguments(bundle);
+      dialog.show( act.getSupportFragmentManager(), null);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void displayImpossibleDialog(String message)
+    {
+    mSolving = false;
+    SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      RubikDialogSolverImpossible dialog = new RubikDialogSolverImpossible();
+      Bundle bundle = new Bundle();
+      bundle.putString("argument", message );
+      dialog.setArguments(bundle);
+      dialog.show( act.getSupportFragmentManager(), null);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void displayImpossibleDialog(int[] errorCode, int[] faceColors)
+    {
+    mSolving = false;
+    SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      String message = error(act.getResources(),errorCode,faceColors);
+      displayImpossibleDialog(message);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getHexColor(int color,int variant) { return colorsHex[color][variant]; }
+  int getTetColor(int color,int variant) { return colorsTet[color][variant]; }
+  int getOctColor(int color,int variant) { return colorsOct[color][variant]; }
+  int getDi4Color(int color,int variant) { return colorsDi4[color][variant]; }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexCornerMissingError(Resources res, int face0, int face1, int face2)
+    {
+    int j0 = getHexColor(face0,3);
+    int j1 = getHexColor(face1,3);
+    int j2 = getHexColor(face2,4);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+    String c2 = res.getString(j2);
+
+    return res.getString(R.string.solver_generic_missing_corner,c0,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexCenterMissingError(Resources res, int face)
+    {
+    int color = getHexColor(face,2);
+    String clr= res.getString(color);
+    return res.getString(R.string.solver_generic_missing_center,clr);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexEdgeMissingError(Resources res, int face0, int face1)
+    {
+    int j0 = getHexColor(face0,3);
+    int j1 = getHexColor(face1,6);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_missing_edge,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexEdgeTwistedError(Resources res, int color0, int color1)
+    {
+    int j0 = getHexColor(color0,3);
+    int j1 = getHexColor(color1,6);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_twisted_edge,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexCornerTwistedError(Resources res, int color0, int color1, int color2)
+    {
+    int j0 = getHexColor(color0,3);
+    int j1 = getHexColor(color1,3);
+    int j2 = getHexColor(color2,5);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+    String c2 = res.getString(j2);
+
+    return res.getString(R.string.solver_generic_twisted_corner,c0,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexEdgeMonoError(Resources res, int color)
+    {
+    int j0 = getHexColor(color,3);
+    int j1 = getHexColor(color,6);
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_edge_mono,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexEdgeTwiceError(Resources res, int color0, int color1)
+    {
+    int j0 = getHexColor(color0,3);
+    int j1 = getHexColor(color1,6);
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_edge_twice,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String octCenterMissingError(Resources res, int face)
+    {
+    int index = getOctColor(face,2);
+    String color = res.getString(index);
+    return res.getString(R.string.solver_generic_missing_center,color);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String octCornerMissingError(Resources res, int f1, int f2)
+    {
+    int i1 = getOctColor(f1,3);
+    int i2 = getOctColor(f2,4);
+    String c1 = res.getString(i1);
+    String c2 = res.getString(i2);
+    return res.getString(R.string.solver_generic_missing_corner2,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String tetCornerMissingError(Resources res, int color0, int color1, int color2)
+    {
+    int j0 = getTetColor(color0,3);
+    int j1 = getTetColor(color1,3);
+    int j2 = getTetColor(color2,4);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+    String c2 = res.getString(j2);
+
+    return res.getString(R.string.solver_generic_missing_corner,c0,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String tetEdgeMissingError(Resources res, int face0, int face1)
+    {
+    int j0 = getTetColor(face0,3);
+    int j1 = getTetColor(face1,6);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_missing_edge,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String tetCenterMissingError(Resources res, int face)
+    {
+    int j = getTetColor(face,2);
+    String c = res.getString(j);
+    return res.getString(R.string.solver_generic_missing_center,c);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String tetVertexMissingError(Resources res, int color0, int color1, int color2)
+    {
+    int j0 = getTetColor(color0,3);
+    int j1 = getTetColor(color1,3);
+    int j2 = getTetColor(color2,4);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+    String c2 = res.getString(j2);
+
+    return res.getString(R.string.solver_generic_missing_vertex,c0,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String di4EdgeThreeError(Resources res, int color)
+    {
+    int j0 = getDi4Color(color,7);
+    String c0 = res.getString(j0);
+    return res.getString(R.string.solver_generic_edge_three,c0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String error(Resources res, int[] err, int[] faceColors)
+    {
+    switch(err[0])
+      {
+      case ERROR_HEX_CORNER_MISSING    : return hexCornerMissingError(res,err[1],err[2],err[3]);
+      case ERROR_HEX_CENTER_MISSING    : return hexCenterMissingError(res,err[1]);
+      case ERROR_HEX_EDGE_MISSING      : return hexEdgeMissingError(res,err[1],err[2]);
+      case ERROR_HEX_EDGE_TWISTED      : return hexEdgeTwistedError(res,err[1],err[2]);
+      case ERROR_HEX_EDGE_MONOCHROMATIC: return hexEdgeMonoError(res,err[1]);
+      case ERROR_HEX_EDGE_TWICE        : return hexEdgeTwiceError(res,err[1],err[2]);
+      case ERROR_HEX_CORNER_TWISTED    : return hexCornerTwistedError(res,err[1],err[2],err[3]);
+
+      case ERROR_TET_CORNER_MISSING    : return tetCornerMissingError(res,err[1],err[2],err[3]);
+      case ERROR_TET_VERTEX_MISSING    : return tetVertexMissingError(res,err[1],err[2],err[3]);
+      case ERROR_TET_EDGE_MISSING      : return tetEdgeMissingError(res,faceColors[err[1]],faceColors[err[2]]);
+      case ERROR_TET_CENTER_MISSING    : return tetCenterMissingError(res,err[1]);
+
+      case ERROR_OCT_CENTER_MISSING    : return octCenterMissingError(res,err[1]);
+      case ERROR_OCT_CORNER_MISSING    : return octCornerMissingError(res,err[1],err[2]);
+
+      case ERROR_DI4_EDGE_THREE        : return di4EdgeThreeError(res,err[1]);
+
+      case ERROR_CORNERS_CANNOT        : return res.getString(R.string.solver_generic_corners_cannot);
+      case ERROR_EDGE_CANNOT           : return res.getString(R.string.solver_generic_edges_cannot);
+      case ERROR_CORNER_TWISTED        : return res.getString(R.string.solver_generic_corner_twist);
+      case ERROR_CORNER_TWIST_90       : return res.getString(R.string.solver_generic_corner_twist) + " (90)";
+      case ERROR_CORNER_TWIST_180      : return res.getString(R.string.solver_generic_corner_twist) + " (180)";
+      case ERROR_EDGE_TWISTED          : return res.getString(R.string.solver_generic_edge_twist);
+      case ERROR_TWO_CENTERS           : return res.getString(R.string.solver_generic_two_centers);
+      case ERROR_TWO_CORNERS           : return res.getString(R.string.solver_generic_two_corners);
+      case ERROR_TWO_EDGES             : return res.getString(R.string.solver_generic_two_edges);
+      case ERROR_FREE_CORNERS_NOT_EVEN : return res.getString(R.string.solver_generic_free_corners_odd);
+      case ERROR_FREE_CORNERS_ROTATED  : return res.getString(R.string.solver_generic_free_corners_rotated);
+      case ERROR_VERTICES_CANNOT       : return res.getString(R.string.solver_generic_vertices_cannot);
+      case ERROR_C_V_DONT_MATCH        : return res.getString(R.string.solver_generic_c_v_dont_match);
+      case ERROR_TWO_CORNERS_TWO_EDGES : return res.getString(R.string.solver_two_corners_two_edges);
+      }
+
+    return null;
+    }
+  }
diff --git a/src/main/java/org/distorted/solverui/ScreenSolution.java b/src/main/java/org/distorted/solverui/ScreenSolution.java
deleted file mode 100644
index 9d8f83b5..00000000
--- a/src/main/java/org/distorted/solverui/ScreenSolution.java
+++ /dev/null
@@ -1,284 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import android.content.SharedPreferences;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-import org.distorted.objectlib.helpers.MovesFinished;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.patterns.RubikPattern;
-
-import java.lang.ref.WeakReference;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenSolution extends ScreenAbstract implements MovesFinished
-  {
-  private static final int MILLIS_PER_DEGREE = 6;
-
-  private TransparentImageButton mPrevButton, mNextButton, mBackButton;
-  private TextView mMovesText;
-  private int[][] mMoves;
-  private int mCurrMove, mNumMoves;
-  private boolean mCanRotate;
-  private float mButtonSize;
-  private WeakReference<SolverActivity> mAct;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(SolverActivity act)
-    {
-    ObjectControl control = act.getControl();
-    control.solveOnly();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final SolverActivity act)
-    {
-    mAct = new WeakReference<>(act);
-
-    float width = act.getScreenWidthInPixels();
-    mButtonSize = width*SolverActivity.BUTTON_TEXT_SIZE;
-    float titleSize  = width*SolverActivity.TITLE_TEXT_SIZE;
-
-    LayoutInflater inflater = act.getLayoutInflater();
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-
-    final TextView text = (TextView)inflater.inflate(R.layout.upper_text, null);
-    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
-    text.setText(R.string.solution);
-    layoutTop.addView(text);
-
-    // BOT ////////////////////////////
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
-
-    LinearLayout layoutLeft = new LinearLayout(act);
-    layoutLeft.setLayoutParams(paramsL);
-    LinearLayout layoutMid = new LinearLayout(act);
-    layoutMid.setLayoutParams(paramsM);
-    LinearLayout layoutRight = new LinearLayout(act);
-    layoutRight.setLayoutParams(paramsR);
-
-    setupPrevButton(act);
-    setupNextButton(act);
-    setupTextView(act,width);
-
-    layoutLeft.addView(mPrevButton);
-    layoutLeft.addView(mMovesText);
-    layoutLeft.addView(mNextButton);
-
-    setupBackButton(act);
-
-    layoutRight.addView(mBackButton);
-
-    layoutBot.addView(layoutLeft);
-    layoutBot.addView(layoutMid);
-    layoutBot.addView(layoutRight);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        backMove(control);
-        mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupNextButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
-
-    mNextButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        makeMove(control);
-        mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupTextView(final SolverActivity act, final float width)
-    {
-    int padding = (int)(width*SolverActivity.PADDING);
-    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
-    params.topMargin    = margin;
-    params.bottomMargin = margin;
-    params.leftMargin   = margin;
-    params.rightMargin  = margin;
-
-    mMovesText = new TextView(act);
-    mMovesText.setTextSize(20);
-    mMovesText.setLayoutParams(params);
-    mMovesText.setPadding(padding,0,padding,0);
-    mMovesText.setGravity(Gravity.CENTER);
-    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void makeMove(ObjectControl control)
-    {
-    if( mCanRotate )
-      {
-      mCurrMove++;
-
-      if( mCurrMove>mNumMoves )
-        {
-        mCurrMove= 0;
-        control.initializeObject(null);
-        }
-      else
-        {
-        int axis      = mMoves[mCurrMove-1][0];
-		int rowBitmap = mMoves[mCurrMove-1][1];
-		int bareAngle = mMoves[mCurrMove-1][2];
-
-        if( bareAngle!=0 )
-          {
-          mCanRotate = false;
-          control.addRotation(this, axis, rowBitmap, bareAngle, MILLIS_PER_DEGREE);
-          }
-        else
-          {
-          android.util.Log.e("solution", "error: solution contains angle 0");
-          }
-        }
-      }
-    else
-      {
-      android.util.Log.e("solution", "failed to make move!");
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void backMove(ObjectControl control)
-    {
-    if( mCanRotate )
-      {
-      mCurrMove--;
-
-      if( mCurrMove<0 )
-        {
-        mCurrMove=mNumMoves;
-        control.initializeObject(mMoves);
-        }
-      else
-        {
-        int axis      = mMoves[mCurrMove][0];
-		int rowBitmap = mMoves[mCurrMove][1];
-		int bareAngle = mMoves[mCurrMove][2];
-
-        if( bareAngle!=0 )
-          {
-          mCanRotate = false;
-          control.addRotation(this, axis, rowBitmap, -bareAngle, MILLIS_PER_DEGREE);
-          }
-        else
-          {
-          android.util.Log.e("solution", "error: solution contains angle 0");
-          }
-        }
-      }
-    else
-      {
-      android.util.Log.e("solution", "failed to back move!");
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setSolution(String moves)
-    {
-    mCanRotate= true;
-    mCurrMove = 0;
-    mNumMoves = moves.length()/4;
-    mMoves    = new int[mNumMoves][3];
-
-    RubikPattern.parseMoves(mMoves,mNumMoves,moves);
-
-    SolverActivity act = mAct.get();
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setSolution(int[][] moves)
-    {
-    mCanRotate= true;
-    mCurrMove = 0;
-    mNumMoves = moves==null ? 0 : moves.length;
-    mMoves    = moves;
-
-    SolverActivity act = mAct.get();
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor) { }
-  public void restorePreferences(SharedPreferences preferences) { }
-  public void onActionFinished(final long effectID) { mCanRotate = true; }
-  }
diff --git a/src/main/java/org/distorted/solverui/ScreenSolutionMultiphased.java b/src/main/java/org/distorted/solverui/ScreenSolutionMultiphased.java
new file mode 100644
index 00000000..62a3c48e
--- /dev/null
+++ b/src/main/java/org/distorted/solverui/ScreenSolutionMultiphased.java
@@ -0,0 +1,578 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2024 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solverui;
+
+import android.content.SharedPreferences;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.library.effect.PostprocessEffectGlow;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.type.Dynamic2D;
+import org.distorted.library.type.Dynamic4D;
+import org.distorted.library.type.Static2D;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.R;
+import org.distorted.objectlib.helpers.MovesFinished;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenSolutionMultiphased extends ScreenAbstract implements MovesFinished, EffectListener
+  {
+  private static final int MOVES_PLACE_0 = 100;
+  private static final int MOVES_PLACE_1 = 101;
+  private static final int FLASH_TIME = 1200;
+  private static final int MILLIS_PER_DEGREE = 6;
+
+  private WeakReference<SolverActivity> mAct;
+  private TransparentImageButton mPrevButton, mNextButton, mBackButton, mPrevPhase, mNextPhase;
+  private float mButtonSize;
+
+  private TextView mMovesText, mMovesPhase;
+  private String[] mPhaseNames;
+  private int mNumPhases;
+  private int[][][] mMoves;
+  private int[][] mCubitsNotInvolved;
+  private int mNumMoves,mCurrMove,mCurrPhase;
+  private boolean mCanMove;
+
+  private Dynamic2D mHaloAndRadiusDyn;
+  private Dynamic4D mColorDyn;
+  private PostprocessEffectGlow mGlow;
+  private boolean mEffectWorking;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(SolverActivity act)
+    {
+    ObjectControl control = act.getControl();
+    control.solveOnly();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final SolverActivity act)
+    {
+    mAct = new WeakReference<>(act);
+
+    mHaloAndRadiusDyn = new Dynamic2D(FLASH_TIME,1.0f);
+    mHaloAndRadiusDyn.add(new Static2D( 0,0));
+    mHaloAndRadiusDyn.add(new Static2D(10,5));
+
+    mColorDyn = new Dynamic4D(FLASH_TIME,1.0f);
+
+    final int[] colors  = new int[] {1,1,1}; // white
+
+    Static4D P1 = new Static4D(colors[0],colors[1],colors[2], 0.0f);
+    Static4D P2 = new Static4D(colors[0],colors[1],colors[2], 1.0f);
+    mColorDyn.add(P1);
+    mColorDyn.add(P2);
+
+    mGlow = new PostprocessEffectGlow(mHaloAndRadiusDyn,mColorDyn);
+
+    float width = act.getScreenWidthInPixels();
+    mButtonSize = width*SolverActivity.BUTTON_TEXT_SIZE;
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+
+    setupPrevPhase(act);
+    setupNextPhase(act);
+    setupTextPhase(act,width);
+
+    layoutTop.addView(mPrevPhase);
+    layoutTop.addView(mMovesPhase);
+    layoutTop.addView(mNextPhase);
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(paramsL);
+    LinearLayout layoutMid = new LinearLayout(act);
+    layoutMid.setLayoutParams(paramsM);
+    LinearLayout layoutRight = new LinearLayout(act);
+    layoutRight.setLayoutParams(paramsR);
+
+    setupPrevButton(act);
+    setupNextButton(act);
+    setupTextView(act,width);
+
+    layoutLeft.addView(mPrevButton);
+    layoutLeft.addView(mMovesText);
+    layoutLeft.addView(mNextButton);
+
+    setupBackButton(act);
+
+    layoutRight.addView(mBackButton);
+
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutMid);
+    layoutBot.addView(layoutRight);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override public void onClick(View v) { backMove(); }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
+
+    mNextButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override public void onClick(View v) { nextMove(); }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextView(final SolverActivity act, final float width)
+    {
+    int padding = (int)(width*SolverActivity.PADDING);
+    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
+    params.topMargin    = margin;
+    params.bottomMargin = margin;
+    params.leftMargin   = margin;
+    params.rightMargin  = margin;
+
+    mMovesText = new TextView(act);
+    mMovesText.setTextSize(20);
+    mMovesText.setLayoutParams(params);
+    mMovesText.setPadding(padding,0,padding,0);
+    mMovesText.setGravity(Gravity.CENTER);
+    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevPhase(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevPhase = new TransparentImageButton(act,R.drawable.ui_left,params);
+
+    mPrevPhase.setOnClickListener( new View.OnClickListener()
+      {
+      @Override public void onClick(View v) { backPhase(); }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextPhase(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextPhase = new TransparentImageButton(act,R.drawable.ui_right,params);
+
+    mNextPhase.setOnClickListener( new View.OnClickListener()
+      {
+      @Override public void onClick(View v) { nextPhase(); }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextPhase(final SolverActivity act, final float width)
+    {
+    int padding = (int)(width*SolverActivity.PADDING);
+    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
+    params.topMargin    = margin;
+    params.bottomMargin = margin;
+    params.leftMargin   = margin;
+    params.rightMargin  = margin;
+
+    mMovesPhase = new TextView(act);
+    mMovesPhase.setTextSize(20);
+    mMovesPhase.setLayoutParams(params);
+    mMovesPhase.setPadding(padding,0,padding,0);
+    mMovesPhase.setGravity(Gravity.CENTER);
+    mMovesPhase.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
+    mMovesPhase.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor) { }
+  public void restorePreferences(SharedPreferences preferences) { }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void backPhase()
+    {
+    SolverActivity act = mAct.get();
+    ObjectControl control = act.getControl();
+
+    if( mCurrMove>0 )
+      {
+      int[][] moves = transformMoves(mMoves[mCurrPhase],0,mCurrMove, false);
+      control.applyScrambles(moves);
+      mCurrMove = 0;
+      }
+    else if( mCurrPhase>0 )
+      {
+      mCurrPhase--;
+      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+      mCurrMove = 0;
+      int[][] moves = transformMoves(mMoves[mCurrPhase],0,mNumMoves, false);
+      control.applyScrambles(moves);
+      }
+    else
+      {
+      mCurrPhase = mNumPhases-1;
+      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+      mCurrMove = mNumMoves;
+      int[][] moves = transformMoves(mMoves, true);
+      control.applyScrambles(moves);
+      }
+
+    setText(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void nextPhase()
+    {
+    SolverActivity act = mAct.get();
+    ObjectControl control = act.getControl();
+
+    if( mCurrPhase<mNumPhases-1 )
+      {
+      glowCubits(mCubitsNotInvolved[mCurrPhase]);
+      int[][] moves = transformMoves(mMoves[mCurrPhase],mCurrMove,mNumMoves, true);
+      control.applyScrambles(moves);
+      mCurrPhase++;
+      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+      mCurrMove = 0;
+      }
+    else if( mCurrMove<mNumMoves )
+      {
+      glowCubits(mCubitsNotInvolved[mCurrPhase]);
+      int[][] moves = transformMoves(mMoves[mCurrPhase],mCurrMove,mNumMoves, true);
+      control.applyScrambles(moves);
+      mCurrMove = mNumMoves;
+      }
+    else
+      {
+      mCurrPhase = 0;
+      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+      mCurrMove = 0;
+      int[][] moves = transformMoves(mMoves, false);
+      control.applyScrambles(moves);
+      }
+
+    setText(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setText(SolverActivity act)
+    {
+    int currMove = 0;
+    int totalMove = 0;
+
+    if( mMoves!=null )
+      {
+      currMove = mCurrMove;
+      for(int p=0; p<mCurrPhase; p++) currMove  += (mMoves[p]==null ? 0: mMoves[p].length);
+      for(int p=0; p<mNumPhases; p++) totalMove += (mMoves[p]==null ? 0: mMoves[p].length);
+      }
+
+    final int cMove = currMove;
+    final int tMove = totalMove;
+
+    act.runOnUiThread(new Runnable()
+      {
+      @Override
+      public void run()
+        {
+        mMovesPhase.setText(mPhaseNames[mCurrPhase]+" "+mCurrMove+"/"+mNumMoves);
+        mMovesText.setText(cMove+"/"+tMove);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int[][] transformMoves(int[][] moves, int start, int end, boolean front)
+    {
+    int mult = front ? 1:-1;
+    int len = end-start;
+    int[][] ret = new int[len][];
+
+    for(int m=0; m<len; m++)
+      {
+      int[] mv = moves[front ? start+m : end-1-m];
+      int[] rt = new int[3];
+      rt[0] = mv[0];
+      rt[1] = (1<<mv[1]);
+      rt[2] = mult*mv[2];
+      ret[m] = rt;
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int[][] transformMoves(int[][][] moves, boolean front)
+    {
+    int len = moves.length;
+    int totalLen = 0;
+    for (int[][] move : moves) totalLen += (move==null ? 0 : move.length);
+
+    int[][] ret = new int[totalLen][];
+    int mult = front ? 1:-1;
+    int index = 0;
+
+    for(int m=0; m<len; m++)
+      {
+      int[][] mv = moves[front ? m : len-1-m];
+      int l = (mv==null ? 0 : mv.length);
+
+      for(int p=0; p<l; p++)
+        {
+        int[] mve = mv[front ? p : l-1-p];
+        int[] rt = new int[3];
+        rt[0] = mve[0];
+        rt[1] = (1<<mve[1]);
+        rt[2] = mult*mve[2];
+        ret[index++] = rt;
+        }
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void glowCubits(int[] cubits)
+    {
+    if( !mEffectWorking )
+      {
+      mEffectWorking = true;
+      SolverActivity act=mAct.get();
+      ObjectControl control = act.getControl();
+      TwistyObject object=control.getObject();
+      DistortedEffects effects=object.getObjectEffects();
+      effects.apply(mGlow);
+
+      MeshBase mesh=object.getObjectMesh();
+      mesh.setComponentsNotAffectedByPostprocessing(cubits);
+
+      mHaloAndRadiusDyn.resetToBeginning();
+      mColorDyn.resetToBeginning();
+      mGlow.notifyWhenFinished(this);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int[] computeCubitsNotInvolved(int[][] subphases, int numCubits)
+    {
+    int numCubitsInvolved = 0;
+    boolean[] involved = new boolean[numCubits];
+
+    for(int[] sub : subphases)
+      if( sub!=null )
+        for(int s : sub)
+          {
+          numCubitsInvolved++;
+          involved[s] = true;
+          }
+
+    int[] ret = new int[numCubits-numCubitsInvolved];
+    int index = 0;
+
+    for(int c=0; c<numCubits; c++)
+      if( !involved[c] ) ret[index++] = c;
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void backMove()
+    {
+    if( mMoves!=null && mCanMove )
+      {
+      SolverActivity act=mAct.get();
+
+      if( mCurrMove>0 )
+        {
+        mCanMove = false;
+        int[] move = mMoves[mCurrPhase][--mCurrMove];
+        ObjectControl control = act.getControl();
+        control.blockTouch(MOVES_PLACE_0);
+        control.addRotation(this, move[0], (1<<move[1]), -move[2], MILLIS_PER_DEGREE);
+        }
+      else if( mCurrPhase>0 )
+        {
+        mCurrPhase--;
+        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+        mCurrMove = mNumMoves;
+        glowCubits(mCubitsNotInvolved[mCurrPhase]);
+        }
+      else
+        {
+        mCurrPhase = mNumPhases-1;
+        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+        mCurrMove = mNumMoves;
+        int[][] moves = transformMoves(mMoves, true);
+        ObjectControl control = act.getControl();
+        control.applyScrambles(moves);
+        glowCubits(mCubitsNotInvolved[mCurrPhase]);
+        }
+
+      setText(act);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void nextMove()
+    {
+    if( mMoves!=null && mCanMove )
+      {
+      SolverActivity act=mAct.get();
+
+      if( mCurrMove<mNumMoves )
+        {
+        mCanMove = false;
+        int[] move = mMoves[mCurrPhase][mCurrMove++];
+        ObjectControl control = act.getControl();
+        control.blockTouch(MOVES_PLACE_1);
+        control.addRotation(this, move[0], (1<<move[1]), move[2], MILLIS_PER_DEGREE);
+        if( mCurrMove==mNumMoves && mCurrPhase==mNumPhases-1 ) glowCubits(mCubitsNotInvolved[mCurrPhase]);
+        }
+      else if( mCurrPhase<mNumPhases-1 )
+        {
+        mCurrPhase++;
+        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+        mCurrMove = 0;
+        glowCubits(mCubitsNotInvolved[mCurrPhase-1]);
+        }
+      else
+        {
+        mCurrPhase = 0;
+        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+        mCurrMove = 0;
+        int[][] moves = transformMoves(mMoves, false);
+        ObjectControl control = act.getControl();
+        control.applyScrambles(moves);
+        }
+
+      setText(act);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void updateNames(String[] names)
+    {
+    mPhaseNames = names;
+    mNumPhases = names.length;
+    mMoves = new int[mNumPhases][][];
+    mCubitsNotInvolved = new int[mNumPhases][];
+    mCanMove = true;
+    if( mAct!=null ) setSolution(null,0,null);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setSolution(int[][] moves, int phase, int[][] subphases)
+    {
+    SolverActivity act=mAct.get();
+
+    if( subphases!=null )
+      {
+      ObjectControl control=act.getControl();
+      TwistyObject object=control.getObject();
+      int numCubits=object.getNumCubits();
+      mCubitsNotInvolved[phase]= computeCubitsNotInvolved(subphases, numCubits);
+      }
+
+    mMoves[phase] = moves;
+    if( phase==0 ) mNumMoves = (moves==null ? 0 : moves.length);
+    mCurrPhase = 0;
+    mCurrMove = 0;
+
+    setText(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onActionFinished(final long effectID)
+    {
+    mCanMove = true;
+    SolverActivity act=mAct.get();
+    ObjectControl control = act.getControl();
+    control.unblockRotation();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void effectFinished(long id)
+    {
+    SolverActivity act=mAct.get();
+    ObjectControl control = act.getControl();
+    TwistyObject object=control.getObject();
+    DistortedEffects effects=object.getObjectEffects();
+    effects.abortById(id);
+    mEffectWorking = false;
+    }
+  }
diff --git a/src/main/java/org/distorted/solverui/ScreenSolutionSinglephased.java b/src/main/java/org/distorted/solverui/ScreenSolutionSinglephased.java
new file mode 100644
index 00000000..c15dd5eb
--- /dev/null
+++ b/src/main/java/org/distorted/solverui/ScreenSolutionSinglephased.java
@@ -0,0 +1,284 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solverui;
+
+import android.content.SharedPreferences;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+import org.distorted.objectlib.helpers.MovesFinished;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.patterns.RubikPattern;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenSolutionSinglephased extends ScreenAbstract implements MovesFinished
+  {
+  private static final int MILLIS_PER_DEGREE = 6;
+
+  private TransparentImageButton mPrevButton, mNextButton, mBackButton;
+  private TextView mMovesText;
+  private int[][] mMoves;
+  private int mCurrMove, mNumMoves;
+  private boolean mCanRotate;
+  private float mButtonSize;
+  private WeakReference<SolverActivity> mAct;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(SolverActivity act)
+    {
+    ObjectControl control = act.getControl();
+    control.solveOnly();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final SolverActivity act)
+    {
+    mAct = new WeakReference<>(act);
+
+    float width = act.getScreenWidthInPixels();
+    mButtonSize = width*SolverActivity.BUTTON_TEXT_SIZE;
+    float titleSize  = width*SolverActivity.TITLE_TEXT_SIZE;
+
+    LayoutInflater inflater = act.getLayoutInflater();
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+
+    final TextView text = (TextView)inflater.inflate(R.layout.upper_text, null);
+    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
+    text.setText(R.string.solution);
+    layoutTop.addView(text);
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(paramsL);
+    LinearLayout layoutMid = new LinearLayout(act);
+    layoutMid.setLayoutParams(paramsM);
+    LinearLayout layoutRight = new LinearLayout(act);
+    layoutRight.setLayoutParams(paramsR);
+
+    setupPrevButton(act);
+    setupNextButton(act);
+    setupTextView(act,width);
+
+    layoutLeft.addView(mPrevButton);
+    layoutLeft.addView(mMovesText);
+    layoutLeft.addView(mNextButton);
+
+    setupBackButton(act);
+
+    layoutRight.addView(mBackButton);
+
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutMid);
+    layoutBot.addView(layoutRight);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        backMove(control);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
+
+    mNextButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        makeMove(control);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextView(final SolverActivity act, final float width)
+    {
+    int padding = (int)(width*SolverActivity.PADDING);
+    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
+    params.topMargin    = margin;
+    params.bottomMargin = margin;
+    params.leftMargin   = margin;
+    params.rightMargin  = margin;
+
+    mMovesText = new TextView(act);
+    mMovesText.setTextSize(20);
+    mMovesText.setLayoutParams(params);
+    mMovesText.setPadding(padding,0,padding,0);
+    mMovesText.setGravity(Gravity.CENTER);
+    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void makeMove(ObjectControl control)
+    {
+    if( mCanRotate )
+      {
+      mCurrMove++;
+
+      if( mCurrMove>mNumMoves )
+        {
+        mCurrMove= 0;
+        control.initializeObject(null);
+        }
+      else
+        {
+        int axis      = mMoves[mCurrMove-1][0];
+		int rowBitmap = mMoves[mCurrMove-1][1];
+		int bareAngle = mMoves[mCurrMove-1][2];
+
+        if( bareAngle!=0 )
+          {
+          mCanRotate = false;
+          control.addRotation(this, axis, rowBitmap, bareAngle, MILLIS_PER_DEGREE);
+          }
+        else
+          {
+          android.util.Log.e("solution", "error: solution contains angle 0");
+          }
+        }
+      }
+    else
+      {
+      android.util.Log.e("solution", "failed to make move!");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void backMove(ObjectControl control)
+    {
+    if( mCanRotate )
+      {
+      mCurrMove--;
+
+      if( mCurrMove<0 )
+        {
+        mCurrMove=mNumMoves;
+        control.initializeObject(mMoves);
+        }
+      else
+        {
+        int axis      = mMoves[mCurrMove][0];
+		int rowBitmap = mMoves[mCurrMove][1];
+		int bareAngle = mMoves[mCurrMove][2];
+
+        if( bareAngle!=0 )
+          {
+          mCanRotate = false;
+          control.addRotation(this, axis, rowBitmap, -bareAngle, MILLIS_PER_DEGREE);
+          }
+        else
+          {
+          android.util.Log.e("solution", "error: solution contains angle 0");
+          }
+        }
+      }
+    else
+      {
+      android.util.Log.e("solution", "failed to back move!");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setSolution(String moves)
+    {
+    mCanRotate= true;
+    mCurrMove = 0;
+    mNumMoves = moves.length()/4;
+    mMoves    = new int[mNumMoves][3];
+
+    RubikPattern.parseMoves(mMoves,mNumMoves,moves);
+
+    SolverActivity act = mAct.get();
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setSolution(int[][] moves)
+    {
+    mCanRotate= true;
+    mCurrMove = 0;
+    mNumMoves = moves==null ? 0 : moves.length;
+    mMoves    = moves;
+
+    SolverActivity act = mAct.get();
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor) { }
+  public void restorePreferences(SharedPreferences preferences) { }
+  public void onActionFinished(final long effectID) { mCanRotate = true; }
+  }
diff --git a/src/main/java/org/distorted/solverui/ScreenSolver.java b/src/main/java/org/distorted/solverui/ScreenSolver.java
deleted file mode 100644
index c407f032..00000000
--- a/src/main/java/org/distorted/solverui/ScreenSolver.java
+++ /dev/null
@@ -1,800 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import static org.distorted.objectlib.metadata.ListObjects.*;
-import static org.distorted.objectlib.solvers.verifiers.SolverTablebase.*;
-
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-import androidx.core.content.ContextCompat;
-
-import org.distorted.dialogs.RubikDialogSolverError;
-import org.distorted.dialogs.RubikDialogSolverImpossible;
-import org.distorted.dialogs.RubikDialogSolvers;
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.MainActivity;
-import org.distorted.main.R;
-import org.distorted.objectlib.helpers.OperatingSystemInterface;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObject;
-import org.distorted.objectlib.metadata.ListObjects;
-import org.distorted.objectlib.shape.*;
-import org.distorted.objectlib.solvers.verifiers.ResultScreen;
-import org.distorted.objectlib.solvers.verifiers.SolverAbstract;
-import org.distorted.objectlib.solvers.verifiers.SolvingList;
-
-import java.lang.ref.WeakReference;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenSolver extends ScreenAbstract implements ResultScreen
-  {
-  private static final int RESET_DURATION = 1000;
-  private static final int MODE_NORMAL = 0;
-  private static final int MODE_DINO_4 = 1;
-
-  private static final int[][] colorsHex =
-    {
-      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
-      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
-      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
-      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
-      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
-      {R.string.color_orange1,R.string.color_orange2,R.string.color_orange3,R.string.color_orange4,R.string.color_orange5,R.string.color_orange6,R.string.color_orange7 },
-    };
-  private static final int[][] colorsTet =
-    {
-      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
-      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
-      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
-      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
-    };
-  private static final int[][] colorsOct =
-    {
-      {R.string.color_violet1,R.string.color_violet2,R.string.color_violet3,R.string.color_violet4,R.string.color_violet5,R.string.color_violet6,R.string.color_violet7 },
-      {R.string.color_grey1  ,R.string.color_grey2  ,R.string.color_grey3  ,R.string.color_grey4  ,R.string.color_grey5  ,R.string.color_grey6  ,R.string.color_grey7   },
-      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
-      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
-      {R.string.color_orange1,R.string.color_orange2,R.string.color_orange3,R.string.color_orange4,R.string.color_orange5,R.string.color_orange6,R.string.color_orange7 },
-      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
-      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
-      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
-    };
-  private static final int[][] colorsDi4 =
-    {
-      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
-      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
-      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
-      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
-    };
-
-  private static Bitmap[] mBitmap;
-  private ImageButton[] mColorButton;
-  private TransparentImageButton mResetButton,mBackButton, mSolveButton;
-  private boolean mSolving;
-  private int mCurrentColor, mCurrentButton;
-  private int[] mFaceColors;
-  private int mColorMode;
-  private int mNumColors;
-  private float mBitmapSize;
-  private WeakReference<SolverActivity> mWeakAct;
-  private int mObjectOrdinal;
-  private String[] mPhaseNames;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(SolverActivity act)
-    {
-    ObjectControl control = act.getControl();
-    control.unsetLock();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final SolverActivity act)
-    {
-    ObjectControl control = act.getControl();
-    control.setLock(false);
-
-    float width = act.getScreenWidthInPixels();
-    float heigh = act.getScreenHeightInPixels();
-
-    mWeakAct = new WeakReference<>(act);
-    mSolving = false;
-    mPhaseNames = null;
-
-    mObjectOrdinal = act.getObjectOrdinal();
-    control.solveOnly();
-    generateFaceColors(mObjectOrdinal);
-
-    final float BUTTON_RATIO = 0.75f;
-    int sizeV = (int)(heigh*MainActivity.RATIO_BAR*BUTTON_RATIO);
-    int sizeH = (int)((width/mNumColors)*BUTTON_RATIO);
-    mBitmapSize = Math.min(sizeV,sizeH);
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-
-    if( mNumColors>0 )
-      {
-      setupBitmaps();
-      setupColorButtons(act,width);
-      markButton(act);
-      }
-
-    for(ImageButton button: mColorButton) layoutTop.addView(button);
-
-    // BOT ////////////////////////////
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
-
-    LinearLayout layoutL = new LinearLayout(act);
-    layoutL.setLayoutParams(params);
-    LinearLayout layoutM = new LinearLayout(act);
-    layoutM.setLayoutParams(params);
-    LinearLayout layoutR = new LinearLayout(act);
-    layoutR.setLayoutParams(params);
-
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    setupResetButton(act);
-    setupSolveButton(act);
-    setupBackButton(act);
-
-    layoutL.addView(mResetButton);
-    layoutM.addView(mSolveButton);
-    layoutR.addView(mBackButton);
-
-    layoutBot.addView(layoutL);
-    layoutBot.addView(layoutM);
-    layoutBot.addView(layoutR);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// This doesn't quite work in many cases, but in case of the solvers that will pop up in foreseeable
-// future it should be ok.
-
-  public void generateFaceColors(int object)
-    {
-    mColorMode = MODE_NORMAL;
-
-    if( object==PYRA_3.ordinal() ||
-        object==PYRA_4.ordinal() ||
-        object==PYRA_5.ordinal() ||
-        object==PDUO_2.ordinal() ||
-        object==JING_2.ordinal() ||
-        object==MORP_2.ordinal() ||
-        object==MORP_3.ordinal() ||
-        object==MORP_4.ordinal()  )
-      {
-      mNumColors  = ShapeTetrahedron.NUM_FACES;
-      mFaceColors = ShapeTetrahedron.FACE_COLORS;
-      }
-    else if( object==DIAM_2.ordinal() ||
-             object==DIAM_3.ordinal() ||
-             object==DIAM_4.ordinal() ||
-             object==TRAJ_3.ordinal() ||
-             object==TRAJ_4.ordinal() ||
-             object==PDIA_3.ordinal()  )
-      {
-      mNumColors  = ShapeOctahedron.NUM_FACES;
-      mFaceColors = ShapeOctahedron.FACE_COLORS;
-      }
-    else if( object==CRYS_3.ordinal() ||
-             object==STAR_3.ordinal() ||
-             object==PENT_2.ordinal() ||
-             object==KILO_3.ordinal() ||
-             object==KILO_5.ordinal() ||
-             object==MEGA_3.ordinal() ||
-             object==MEGA_5.ordinal()  )
-      {
-      mNumColors  = ShapeDodecahedron.NUM_FACES;
-      mFaceColors = ShapeDodecahedron.FACE_COLORS;
-      }
-    else if( object==BALL_4.ordinal() )
-      {
-      mNumColors  = ShapeDiamond.NUM_FACES;
-      mFaceColors = ShapeDiamond.FACE_COLORS;
-      }
-    else if( object==ICOS_2.ordinal() )
-      {
-      mNumColors  = ShapeIcosahedron.NUM_FACES;
-      mFaceColors = ShapeIcosahedron.FACE_COLORS;
-      }
-    else if( object==DIN4_3.ordinal() )
-      {
-      mNumColors  = 4;
-      mFaceColors = new int[] { ShapeColors.COLOR_YELLOW, ShapeColors.COLOR_RED, ShapeColors.COLOR_BLUE, ShapeColors.COLOR_WHITE};
-      mColorMode  = MODE_DINO_4;
-      }
-    else
-      {
-      mNumColors  = ShapeHexahedron.NUM_FACES;
-      mFaceColors = ShapeHexahedron.FACE_COLORS;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBitmaps()
-    {
-    final int SIZE = (int)mBitmapSize;
-    final float R = SIZE*0.15f;
-    final float M = SIZE*0.08f;
-
-    mBitmap = new Bitmap[mNumColors];
-
-    Paint paint = new Paint();
-    paint.setColor(0xff008800);
-    paint.setStyle(Paint.Style.FILL);
-
-    paint.setAntiAlias(true);
-    paint.setTextAlign(Paint.Align.CENTER);
-    paint.setStyle(Paint.Style.FILL);
-
-    for(int i=0; i<mNumColors; i++)
-      {
-      mBitmap[i] = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888);
-      Canvas canvas = new Canvas(mBitmap[i]);
-
-      paint.setColor(0xff000000);
-      canvas.drawRect(0, 0, SIZE, SIZE, paint);
-
-      paint.setColor(mFaceColors[i]);
-      canvas.drawRoundRect( M, M, SIZE-M, SIZE-M, R, R, paint);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int translateColor(int color)
-    {
-    if( mColorMode==MODE_DINO_4 )
-      {
-      int realColor = mFaceColors[color];
-      int[] hexColors = ShapeHexahedron.FACE_COLORS;
-
-      for(int i=0; i<6; i++)
-        if( hexColors[i]==realColor ) return i;
-      }
-
-    return color;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean pressSolve(SolverActivity act)
-    {
-    int[] solverOrdinals = SolvingList.getSolverOrdinals(mObjectOrdinal);
-
-    if( solverOrdinals!=null  )
-      {
-      SolvingList slvList = SolvingList.getSolver(solverOrdinals[0]);
-      OperatingSystemInterface os = act.getInterface();
-      TwistyObject object = act.getObject();
-      SolverAbstract solver = slvList.create(os,object);
-
-      if( solver!=null )
-        {
-        int[] result = solver.validatePosition(object);
-
-        if( result[0]>=0 ) // position is valid
-          {
-          if( solverOrdinals.length==1 ) // just one solver - simply launch it
-            {
-            solver.solve(this,result);
-            return true;
-            }
-          else // more than one solver - pop up a choosing dialog
-            {
-            ListObjects objList = ListObjects.getObject(mObjectOrdinal);
-            String upperName = objList.name();
-            Bundle bundle = new Bundle();
-            bundle.putString("argument", upperName );
-            RubikDialogSolvers solv = new RubikDialogSolvers();
-            solv.setArguments(bundle);
-            solv.show( act.getSupportFragmentManager(), RubikDialogSolvers.getDialogTag() );
-            return false;
-            }
-          }
-        else
-          {
-          displayImpossibleDialog(result,solver.getFaceColors());
-          return false;
-          }
-        }
-      else
-        {
-        displayErrorDialog(act.getString(R.string.solver_generic_not_implemented));
-        return false;
-        }
-      }
-    else  // no solvers? Impossible!
-      {
-      displayErrorDialog("No solvers found for object "+mObjectOrdinal);
-      return false;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void fail(String result) {}
-  public void setPhaseNames(String[] names) { mPhaseNames = names; }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupColorButtons(final SolverActivity act, final float width)
-    {
-    mColorButton = new ImageButton[mNumColors];
-    int padding = (int)(width*SolverActivity.PADDING);
-    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
-
-    for(int i=0; i<mNumColors; i++)
-      {
-      final int ii = i;
-      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.MATCH_PARENT, 1.0f);
-      params.topMargin    = margin;
-      params.bottomMargin = margin;
-      params.leftMargin   = margin;
-      params.rightMargin  = margin;
-
-      mColorButton[i] = new ImageButton(act);
-      mColorButton[i].setLayoutParams(params);
-      mColorButton[i].setPadding(padding,0,padding,0);
-      mColorButton[i].setImageBitmap(mBitmap[i]);
-
-      mColorButton[i].setOnClickListener( new View.OnClickListener()
-        {
-        @Override
-        public void onClick(View view)
-          {
-          mCurrentColor = translateColor(ii);
-          mCurrentButton= ii;
-          markButton(act);
-          }
-        });
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupResetButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
-    mResetButton = new TransparentImageButton(act, R.drawable.ui_reset, params);
-
-    mResetButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        control.resetTextureMapsEffect(RESET_DURATION);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupSolveButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mSolveButton = new TransparentImageButton(act,R.drawable.ui_solve,params);
-
-    mSolveButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        if( !mSolving && pressSolve(act) ) mSolving = true;
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        control.resetAllTextureMaps();
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void markButton(SolverActivity act)
-    {
-    if( mCurrentButton>=mNumColors )
-      {
-      mCurrentButton = 0;
-      mCurrentColor = translateColor(0);
-      }
-
-    for(int b=0; b<mNumColors; b++)
-      {
-      Drawable d = mColorButton[b].getBackground();
-      int s = b==mCurrentButton ? act.getSelectedColor() : act.getNormalColor();
-      d.setColorFilter(ContextCompat.getColor(act,s), PorterDuff.Mode.MULTIPLY);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-    editor.putInt("stateSolver_color" , mCurrentColor );
-    editor.putInt("stateSolver_button", mCurrentButton);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restorePreferences(SharedPreferences preferences)
-    {
-    mCurrentColor = preferences.getInt("stateSolver_color" , 0);
-    mCurrentButton= preferences.getInt("stateSolver_button", 0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getCurrentColor()
-    {
-    return mCurrentColor;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setSolved(final String moves)
-    {
-    mSolving = false;
-    final SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      act.runOnUiThread(new Runnable()
-        {
-        @Override
-        public void run()
-          {
-          ScreenList.switchScreen(act, ScreenList.SOLU);
-          ScreenSolution solution = (ScreenSolution) ScreenList.SOLU.getScreenClass();
-          solution.setSolution(moves);
-          if( !moves.isEmpty() ) act.doNotShowDialogAnymore();
-          }
-        });
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setSolved(final int[][] moves, final int phaseNumber, final int[][] subphases)
-    {
-    mSolving = false;
-    final SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      act.runOnUiThread(new Runnable()
-        {
-        @Override
-        public void run()
-          {
-          if( mPhaseNames!=null )
-            {
-            ScreenPhasedSolution screen = (ScreenPhasedSolution) ScreenList.PHAS.getScreenClass();
-            if( phaseNumber==0 )
-              {
-              ScreenList.switchScreen(act, ScreenList.PHAS);
-              screen.updateNames(mPhaseNames);
-              }
-            screen.setSolution(moves, phaseNumber,subphases);
-            }
-          else
-            {
-            ScreenList.switchScreen(act, ScreenList.SOLU);
-            ScreenSolution screen = (ScreenSolution) ScreenList.SOLU.getScreenClass();
-            screen.setSolution(moves);
-            }
-
-          if( moves!=null && moves.length>0 ) act.doNotShowDialogAnymore();
-          }
-        });
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void displayErrorDialog(String message)
-    {
-    mSolving = false;
-    SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      RubikDialogSolverError dialog = new RubikDialogSolverError();
-      Bundle bundle = new Bundle();
-      bundle.putString("argument", message );
-      dialog.setArguments(bundle);
-      dialog.show( act.getSupportFragmentManager(), null);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void displayImpossibleDialog(String message)
-    {
-    mSolving = false;
-    SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      RubikDialogSolverImpossible dialog = new RubikDialogSolverImpossible();
-      Bundle bundle = new Bundle();
-      bundle.putString("argument", message );
-      dialog.setArguments(bundle);
-      dialog.show( act.getSupportFragmentManager(), null);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void displayImpossibleDialog(int[] errorCode, int[] faceColors)
-    {
-    mSolving = false;
-    SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      String message = error(act.getResources(),errorCode,faceColors);
-      displayImpossibleDialog(message);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getHexColor(int color,int variant) { return colorsHex[color][variant]; }
-  int getTetColor(int color,int variant) { return colorsTet[color][variant]; }
-  int getOctColor(int color,int variant) { return colorsOct[color][variant]; }
-  int getDi4Color(int color,int variant) { return colorsDi4[color][variant]; }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexCornerMissingError(Resources res, int face0, int face1, int face2)
-    {
-    int j0 = getHexColor(face0,3);
-    int j1 = getHexColor(face1,3);
-    int j2 = getHexColor(face2,4);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-    String c2 = res.getString(j2);
-
-    return res.getString(R.string.solver_generic_missing_corner,c0,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexCenterMissingError(Resources res, int face)
-    {
-    int color = getHexColor(face,2);
-    String clr= res.getString(color);
-    return res.getString(R.string.solver_generic_missing_center,clr);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexEdgeMissingError(Resources res, int face0, int face1)
-    {
-    int j0 = getHexColor(face0,3);
-    int j1 = getHexColor(face1,6);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_missing_edge,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexEdgeTwistedError(Resources res, int color0, int color1)
-    {
-    int j0 = getHexColor(color0,3);
-    int j1 = getHexColor(color1,6);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_twisted_edge,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexCornerTwistedError(Resources res, int color0, int color1, int color2)
-    {
-    int j0 = getHexColor(color0,3);
-    int j1 = getHexColor(color1,3);
-    int j2 = getHexColor(color2,5);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-    String c2 = res.getString(j2);
-
-    return res.getString(R.string.solver_generic_twisted_corner,c0,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexEdgeMonoError(Resources res, int color)
-    {
-    int j0 = getHexColor(color,3);
-    int j1 = getHexColor(color,6);
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_edge_mono,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexEdgeTwiceError(Resources res, int color0, int color1)
-    {
-    int j0 = getHexColor(color0,3);
-    int j1 = getHexColor(color1,6);
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_edge_twice,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String octCenterMissingError(Resources res, int face)
-    {
-    int index = getOctColor(face,2);
-    String color = res.getString(index);
-    return res.getString(R.string.solver_generic_missing_center,color);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String octCornerMissingError(Resources res, int f1, int f2)
-    {
-    int i1 = getOctColor(f1,3);
-    int i2 = getOctColor(f2,4);
-    String c1 = res.getString(i1);
-    String c2 = res.getString(i2);
-    return res.getString(R.string.solver_generic_missing_corner2,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String tetCornerMissingError(Resources res, int color0, int color1, int color2)
-    {
-    int j0 = getTetColor(color0,3);
-    int j1 = getTetColor(color1,3);
-    int j2 = getTetColor(color2,4);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-    String c2 = res.getString(j2);
-
-    return res.getString(R.string.solver_generic_missing_corner,c0,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String tetEdgeMissingError(Resources res, int face0, int face1)
-    {
-    int j0 = getTetColor(face0,3);
-    int j1 = getTetColor(face1,6);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_missing_edge,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String tetCenterMissingError(Resources res, int face)
-    {
-    int j = getTetColor(face,2);
-    String c = res.getString(j);
-    return res.getString(R.string.solver_generic_missing_center,c);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String tetVertexMissingError(Resources res, int color0, int color1, int color2)
-    {
-    int j0 = getTetColor(color0,3);
-    int j1 = getTetColor(color1,3);
-    int j2 = getTetColor(color2,4);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-    String c2 = res.getString(j2);
-
-    return res.getString(R.string.solver_generic_missing_vertex,c0,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String di4EdgeThreeError(Resources res, int color)
-    {
-    int j0 = getDi4Color(color,7);
-    String c0 = res.getString(j0);
-    return res.getString(R.string.solver_generic_edge_three,c0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String error(Resources res, int[] err, int[] faceColors)
-    {
-    switch(err[0])
-      {
-      case ERROR_HEX_CORNER_MISSING    : return hexCornerMissingError(res,err[1],err[2],err[3]);
-      case ERROR_HEX_CENTER_MISSING    : return hexCenterMissingError(res,err[1]);
-      case ERROR_HEX_EDGE_MISSING      : return hexEdgeMissingError(res,err[1],err[2]);
-      case ERROR_HEX_EDGE_TWISTED      : return hexEdgeTwistedError(res,err[1],err[2]);
-      case ERROR_HEX_EDGE_MONOCHROMATIC: return hexEdgeMonoError(res,err[1]);
-      case ERROR_HEX_EDGE_TWICE        : return hexEdgeTwiceError(res,err[1],err[2]);
-      case ERROR_HEX_CORNER_TWISTED    : return hexCornerTwistedError(res,err[1],err[2],err[3]);
-
-      case ERROR_TET_CORNER_MISSING    : return tetCornerMissingError(res,err[1],err[2],err[3]);
-      case ERROR_TET_VERTEX_MISSING    : return tetVertexMissingError(res,err[1],err[2],err[3]);
-      case ERROR_TET_EDGE_MISSING      : return tetEdgeMissingError(res,faceColors[err[1]],faceColors[err[2]]);
-      case ERROR_TET_CENTER_MISSING    : return tetCenterMissingError(res,err[1]);
-
-      case ERROR_OCT_CENTER_MISSING    : return octCenterMissingError(res,err[1]);
-      case ERROR_OCT_CORNER_MISSING    : return octCornerMissingError(res,err[1],err[2]);
-
-      case ERROR_DI4_EDGE_THREE        : return di4EdgeThreeError(res,err[1]);
-
-      case ERROR_CORNERS_CANNOT        : return res.getString(R.string.solver_generic_corners_cannot);
-      case ERROR_EDGE_CANNOT           : return res.getString(R.string.solver_generic_edges_cannot);
-      case ERROR_CORNER_TWISTED        : return res.getString(R.string.solver_generic_corner_twist);
-      case ERROR_CORNER_TWIST_90       : return res.getString(R.string.solver_generic_corner_twist) + " (90)";
-      case ERROR_CORNER_TWIST_180      : return res.getString(R.string.solver_generic_corner_twist) + " (180)";
-      case ERROR_EDGE_TWISTED          : return res.getString(R.string.solver_generic_edge_twist);
-      case ERROR_TWO_CENTERS           : return res.getString(R.string.solver_generic_two_centers);
-      case ERROR_TWO_CORNERS           : return res.getString(R.string.solver_generic_two_corners);
-      case ERROR_TWO_EDGES             : return res.getString(R.string.solver_generic_two_edges);
-      case ERROR_FREE_CORNERS_NOT_EVEN : return res.getString(R.string.solver_generic_free_corners_odd);
-      case ERROR_FREE_CORNERS_ROTATED  : return res.getString(R.string.solver_generic_free_corners_rotated);
-      case ERROR_VERTICES_CANNOT       : return res.getString(R.string.solver_generic_vertices_cannot);
-      case ERROR_C_V_DONT_MATCH        : return res.getString(R.string.solver_generic_c_v_dont_match);
-      case ERROR_TWO_CORNERS_TWO_EDGES : return res.getString(R.string.solver_two_corners_two_edges);
-      }
-
-    return null;
-    }
-  }
diff --git a/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java b/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java
index 676e1bc7..2da64cbe 100644
--- a/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java
+++ b/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java
@@ -170,7 +170,7 @@ public class SolverObjectLibInterface implements ObjectLibInterface
   public void onReplaceModeDown(int cubit, int face)
     {
     SolverActivity act = mAct.get();
-    ScreenSolver solver = (ScreenSolver) ScreenList.SVER.getScreenClass();
+    ScreenSetupPosition solver = (ScreenSetupPosition) ScreenList.SVER.getScreenClass();
     int color = solver.getCurrentColor();
     int currObject = act.getObjectOrdinal();
     mLastCubitColor = SolvingList.cubitIsLocked(currObject,cubit);
