commit e9245b7b08eef65b4b446a8518f0330dc5e8a326
Author: leszek <leszek@koltunski.pl>
Date:   Wed Nov 8 15:20:21 2023 +0100

    Initial support for the new Pattern Dialog.

diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 76f5c79b..e0604685 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -36,6 +36,7 @@
         <activity android:name="org.distorted.bandaged.BandagedPlayActivity" android:exported="false" android:screenOrientation="portrait"/>
         <activity android:name="org.distorted.purchase.PurchaseActivity" android:exported="false" android:screenOrientation="portrait"/>
         <activity android:name="org.distorted.solverui.SolverActivity" android:exported="false" android:screenOrientation="portrait"/>
+        <activity android:name="org.distorted.patternui.PatternActivity" android:exported="false" android:screenOrientation="portrait"/>
 
         <service
             android:name="org.distorted.messaging.RubikMessagingService"
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogPattern.java b/src/main/java/org/distorted/dialogs/RubikDialogPattern.java
index 057c5720..883b8782 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogPattern.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogPattern.java
@@ -57,20 +57,8 @@ public class RubikDialogPattern extends RubikDialogAbstract
   public boolean hasArgument()  { return false; }
   public int getPositive()      { return R.string.ok; }
   public int getNegative()      { return -1; }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void positiveAction()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void negativeAction()
-    {
-
-    }
+  public void positiveAction()  { }
+  public void negativeAction()  { }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogPatternSingle.java b/src/main/java/org/distorted/dialogs/RubikDialogPatternSingle.java
new file mode 100644
index 00000000..9e996ab0
--- /dev/null
+++ b/src/main/java/org/distorted/dialogs/RubikDialogPatternSingle.java
@@ -0,0 +1,153 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.dialogs;
+
+import android.app.Dialog;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ExpandableListView;
+
+import androidx.fragment.app.FragmentActivity;
+
+import org.distorted.main.R;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.patterns.RubikPattern;
+import org.distorted.objectlib.patterns.RubikPatternList;
+import org.distorted.objects.RubikObjectList;
+import org.distorted.patternui.PatternActivity;
+import org.distorted.patternui.ScreenList;
+import org.distorted.patternui.ScreenPattern;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikDialogPatternSingle extends RubikDialogAbstract
+  {
+  private ExpandableListView mListView;
+  private int mPatternOrdinal, mPos;
+  private int mExpandedGroup;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Override
+  public void onResume()
+    {
+    super.onResume();
+
+    Window window = getDialog().getWindow();
+
+    if( window!=null )
+      {
+      WindowManager.LayoutParams params = window.getAttributes();
+      params.width  = (int)Math.min( mHeight*0.65f,mWidth*0.98f );
+      params.height = (int)Math.min( mHeight*0.80f,mWidth*1.30f );
+      window.setAttributes(params);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getResource()      { return R.layout.dialog_pattern_tab; }
+  public int getTitleResource() { return R.string.choose_pattern; }
+  public boolean hasArgument()  { return true; }
+  public int getPositive()      { return -1; }
+  public int getNegative()      { return -1; }
+  public void positiveAction()  { }
+  public void negativeAction()  { }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void prepareBody(Dialog dialog, View view, FragmentActivity act, float size)
+    {
+    int objectOrdinal = RubikObjectList.getOrdinal(mArgument);
+
+    if( objectOrdinal<0 )
+      {
+      android.util.Log.e("D", "object "+mArgument+" not found");
+      return;
+      }
+
+    mPatternOrdinal = RubikPatternList.getOrdinal(objectOrdinal);
+
+    if( mPatternOrdinal<0 )
+      {
+      android.util.Log.e("D", "patterns for object "+mArgument+" not found");
+      return;
+      }
+
+    final PatternActivity pact = (PatternActivity)getContext();
+    int height = pact!=null ? pact.getScreenHeightInPixels() : 100;
+    final ObjectControl control = pact!=null ? pact.getControl() : null;
+
+    RubikPattern pattern = RubikPattern.getInstance();
+    mExpandedGroup = pattern.recallExpanded(mPatternOrdinal);
+
+    mListView = view.findViewById(R.id.patternListView);
+    RubikDialogPatternListAdapter listAdapter = new RubikDialogPatternListAdapter(act,mPatternOrdinal,height);
+    mListView.setAdapter(listAdapter);
+
+    if( mExpandedGroup>=0 )
+      {
+      mListView.expandGroup(mExpandedGroup);
+      }
+
+    int visible = pattern.recallVisiblePos(mPatternOrdinal);
+    mListView.setSelectionFromTop(visible,0);
+
+
+    mListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener()
+      {
+      @Override
+      public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id)
+        {
+        RubikPattern pattern = RubikPattern.getInstance();
+        int[][] moves = pattern.reInitialize(mPatternOrdinal, groupPosition, childPosition);
+        control.initializeObject(moves);
+
+        ScreenPattern state = (ScreenPattern) ScreenList.PATT.getScreenClass();
+        state.setPattern(pact, mPatternOrdinal, groupPosition, childPosition);
+
+        rememberState();
+        dismiss();
+
+        return false;
+        }
+      });
+
+    mListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener()
+      {
+      @Override
+      public void onGroupExpand(int groupPosition)
+        {
+        if(mExpandedGroup!=-1 && groupPosition!=mExpandedGroup)
+          {
+          mListView.collapseGroup(mExpandedGroup);
+          }
+
+        mExpandedGroup = groupPosition;
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void rememberState()
+    {
+    RubikPattern pattern = RubikPattern.getInstance();
+    pattern.rememberState(mPatternOrdinal,mPos,mListView.getFirstVisiblePosition(),mExpandedGroup);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static String getDialogTag()
+    {
+    return "DialogPatternSingle";
+    }
+  }
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java b/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java
index 040874f3..c0a1a23d 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogPatternView.java
@@ -30,7 +30,6 @@ import org.distorted.screens.RubikScreenPattern;
 public class RubikDialogPatternView extends FrameLayout
   {
   private ExpandableListView mListView;
-  private RubikDialogPatternListAdapter mListAdapter;
   private RubikDialogPattern mDialog;
   private int mTab, mPos;
   private int mExpandedGroup;
@@ -67,8 +66,8 @@ public class RubikDialogPatternView extends FrameLayout
     View tab = inflate( act, R.layout.dialog_pattern_tab, null);
 
     mListView = tab.findViewById(R.id.patternListView);
-    mListAdapter = new RubikDialogPatternListAdapter(act,mTab, ract.getScreenHeightInPixels());
-    mListView.setAdapter(mListAdapter);
+    RubikDialogPatternListAdapter listAdapter = new RubikDialogPatternListAdapter(act,mTab, ract.getScreenHeightInPixels());
+    mListView.setAdapter(listAdapter);
 
     if( mExpandedGroup>=0 )
       {
diff --git a/src/main/java/org/distorted/main/MainActivity.java b/src/main/java/org/distorted/main/MainActivity.java
index 9c54015e..577c2c91 100644
--- a/src/main/java/org/distorted/main/MainActivity.java
+++ b/src/main/java/org/distorted/main/MainActivity.java
@@ -43,9 +43,11 @@ import org.distorted.dialogs.RubikDialogUpdates;
 import org.distorted.external.RubikNetwork;
 import org.distorted.external.RubikScores;
 import org.distorted.external.RubikUpdates;
+import org.distorted.main_old.RubikSurfaceView;
 import org.distorted.messaging.RubikInAppMessanging;
+import org.distorted.objectlib.main.ObjectControl;
 import org.distorted.objects.RubikObjectList;
-import org.distorted.purchase.PurchaseActivity;
+import org.distorted.patternui.PatternActivity;
 import org.distorted.solverui.SolverActivity;
 import org.distorted.tutorials.TutorialActivity;
 
@@ -396,6 +398,15 @@ public class MainActivity extends AppCompatActivity implements RubikNetwork.Upda
       startActivity(intent);
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void switchToPattern(int objectOrdinal)
+      {
+      Intent intent = new Intent(this, PatternActivity.class);
+      intent.putExtra("obj", objectOrdinal);
+      startActivity(intent);
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void onScores(View v)
diff --git a/src/main/java/org/distorted/main/MainObjectPopup.java b/src/main/java/org/distorted/main/MainObjectPopup.java
index 89c4aa0e..ffd3ce4d 100644
--- a/src/main/java/org/distorted/main/MainObjectPopup.java
+++ b/src/main/java/org/distorted/main/MainObjectPopup.java
@@ -91,7 +91,8 @@ public class MainObjectPopup
         @Override
         public void onClick(View v)
           {
-
+          mPopup.dismiss();
+          act.switchToPattern(ordinal);
           }
         });
       }
diff --git a/src/main/java/org/distorted/patternui/PatternActivity.java b/src/main/java/org/distorted/patternui/PatternActivity.java
new file mode 100644
index 00000000..d13e3f92
--- /dev/null
+++ b/src/main/java/org/distorted/patternui/PatternActivity.java
@@ -0,0 +1,325 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.patternui;
+
+import static org.distorted.objectlib.main.TwistyObject.MESH_NICE;
+
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.DisplayCutout;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.preference.PreferenceManager;
+
+import org.distorted.dialogs.RubikDialogError;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.main.MainActivity;
+import org.distorted.main.R;
+import org.distorted.objectlib.main.InitAssets;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+import org.distorted.os.OSInterface;
+
+import java.io.InputStream;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PatternActivity extends AppCompatActivity
+{
+    public static final float RATIO_BAR       = 0.100f;
+    public static final float PADDING         = 0.010f;
+    public static final float SMALL_MARGIN    = 0.004f;
+    public static final float BUTTON_TEXT_SIZE= 0.050f;
+    public static final float TITLE_TEXT_SIZE = 0.060f;
+
+    private static final int ACTIVITY_NUMBER = 1;
+    private static final float RATIO_INSET= 0.09f;
+
+    private boolean mJustStarted;
+    private static int mScreenWidth, mScreenHeight;
+    private int mCurrentApiVersion;
+    private int mHeightUpperBar;
+    private int mObjectOrdinal;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void onCreate(Bundle savedState)
+      {
+      super.onCreate(savedState);
+      DistortedLibrary.onCreate(ACTIVITY_NUMBER);
+      setTheme(R.style.MaterialThemeNoActionBar);
+      setContentView(R.layout.pattern);
+      hideNavigationBar();
+
+      mJustStarted = true;
+      Bundle b = getIntent().getExtras();
+      mObjectOrdinal = b!=null ? b.getInt("obj") : 0;
+
+      DisplayMetrics displaymetrics = new DisplayMetrics();
+      getWindowManager().getDefaultDisplay().getRealMetrics(displaymetrics);
+      mScreenWidth =displaymetrics.widthPixels;
+      mScreenHeight=displaymetrics.heightPixels;
+
+      cutoutHack();
+      computeBarHeights();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this does not include possible insets
+
+    private void computeBarHeights()
+      {
+      int barHeight = (int)(mScreenHeight*RATIO_BAR);
+      mHeightUpperBar = barHeight;
+
+      LinearLayout layoutTop = findViewById(R.id.upperBar);
+      LinearLayout layoutBot = findViewById(R.id.lowerBar);
+
+      ViewGroup.LayoutParams paramsTop = layoutTop.getLayoutParams();
+      paramsTop.height = mHeightUpperBar;
+      layoutTop.setLayoutParams(paramsTop);
+      ViewGroup.LayoutParams paramsBot = layoutBot.getLayoutParams();
+      paramsBot.height = barHeight;
+      layoutBot.setLayoutParams(paramsBot);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void hideNavigationBar()
+      {
+      mCurrentApiVersion = Build.VERSION.SDK_INT;
+
+      if(mCurrentApiVersion >= Build.VERSION_CODES.KITKAT)
+        {
+        final View decorView = getWindow().getDecorView();
+
+        decorView.setSystemUiVisibility(MainActivity.FLAGS);
+
+        decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener()
+          {
+          @Override
+          public void onSystemUiVisibilityChange(int visibility)
+            {
+            if((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0)
+              {
+              decorView.setSystemUiVisibility(MainActivity.FLAGS);
+              }
+            }
+          });
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onAttachedToWindow()
+      {
+      super.onAttachedToWindow();
+
+      if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.P )
+        {
+        DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
+        int insetHeight = cutout!=null ? cutout.getSafeInsetTop() : 0;
+
+        LinearLayout layoutHid = findViewById(R.id.hiddenBar);
+        ViewGroup.LayoutParams paramsHid = layoutHid.getLayoutParams();
+        paramsHid.height = (int)(insetHeight*RATIO_INSET);
+        layoutHid.setLayoutParams(paramsHid);
+        mHeightUpperBar += paramsHid.height;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// do not avoid cutouts
+
+    private void cutoutHack()
+      {
+      if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.P )
+        {
+        getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus)
+      {
+      super.onWindowFocusChanged(hasFocus);
+
+      if( mCurrentApiVersion >= Build.VERSION_CODES.KITKAT && hasFocus )
+        {
+        getWindow().getDecorView().setSystemUiVisibility(MainActivity.FLAGS);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onPause() 
+      {
+      super.onPause();
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      view.onPause();
+      DistortedLibrary.onPause(ACTIVITY_NUMBER);
+      savePreferences();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onResume() 
+      {
+      super.onResume();
+      DistortedLibrary.onResume(ACTIVITY_NUMBER);
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      view.onResume();
+
+      createObject();
+      RubikObjectList.setCurrObject(mObjectOrdinal);
+
+      SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+      restorePreferences(preferences,mJustStarted);
+      ScreenList.setScreen(this);
+
+      if( mJustStarted )
+        {
+        mJustStarted = false;
+        RubikObjectList.restoreMeshState(preferences);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onDestroy() 
+      {
+      DistortedLibrary.onDestroy(ACTIVITY_NUMBER);
+      super.onDestroy();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void savePreferences()
+      {
+      SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
+      SharedPreferences.Editor editor = preferences.edit();
+
+      for(int i = 0; i< ScreenList.LENGTH; i++ )
+        {
+        ScreenList.getScreen(i).getScreenClass().savePreferences(editor);
+        }
+
+      RubikObjectList.savePreferences(editor);
+      RubikObjectList.saveMeshState(editor);
+      ScreenList.savePreferences(editor);
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      OSInterface os = view.getInterface();
+      os.setEditor(editor);
+      view.getObjectControl().savePreferences();
+
+      boolean success = editor.commit();
+      if( !success ) android.util.Log.e("D", "Failed to save preferences");
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void restorePreferences(SharedPreferences preferences, boolean justStarted)
+      {
+      RubikObjectList.restorePreferences(this,preferences,justStarted);
+
+      for (int i = 0; i<ScreenList.LENGTH; i++)
+        {
+        ScreenList.getScreen(i).getScreenClass().restorePreferences(preferences);
+        }
+
+      ScreenList.restorePreferences(preferences);
+
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      OSInterface os = view.getInterface();
+      os.setPreferences(preferences);
+      view.getObjectControl().restorePreferences();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void OpenGLError()
+      {
+      RubikDialogError errDiag = new RubikDialogError();
+      errDiag.show(getSupportFragmentManager(), null);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TwistyObject getObject()
+      {
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      return view.getObjectControl().getObject();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public DistortedScreen getScreen()
+      {
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      return view.getRenderer().getScreen();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public ObjectControl getControl()
+      {
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      return view.getObjectControl();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getScreenWidthInPixels()
+      {
+      return mScreenWidth;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getScreenHeightInPixels()
+      {
+      return mScreenHeight;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void createObject()
+      {
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      ObjectControl control = view.getObjectControl();
+      RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
+      int iconMode  = TwistyObject.MODE_NORM;
+      InputStream jsonStream = object==null ? null : object.getObjectStream(this);
+      InputStream meshStream = object==null ? null : object.getMeshStream(this);
+      String name = object==null ? "NULL" : object.getUpperName();
+      OSInterface os = view.getInterface();
+      InitAssets asset = new InitAssets(jsonStream,meshStream,os);
+
+      control.changeIfDifferent(mObjectOrdinal,name,MESH_NICE,iconMode,asset);
+      }
+}
diff --git a/src/main/java/org/distorted/patternui/PatternObjectLibInterface.java b/src/main/java/org/distorted/patternui/PatternObjectLibInterface.java
new file mode 100644
index 00000000..c8319058
--- /dev/null
+++ b/src/main/java/org/distorted/patternui/PatternObjectLibInterface.java
@@ -0,0 +1,153 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.patternui;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.message.EffectMessageSender;
+import org.distorted.main.BuildConfig;
+import org.distorted.objectlib.helpers.BlockController;
+import org.distorted.objectlib.helpers.ObjectLibInterface;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PatternObjectLibInterface implements ObjectLibInterface
+{
+  PatternObjectLibInterface(PatternActivity act) { }
+  public void onScrambleEffectFinished() { }
+  public void onFinishRotation(int axis, int row, int angle) { }
+  public void onBeginRotation() { }
+  public void failedToDrag() { }
+  public void onSolved() { }
+  public void onObjectCreated(long time) { }
+  public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum) { }
+  public void onReplaceModeDown(int cubit, int face) { }
+  public void onReplaceModeUp() { }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportProblem(String problem, boolean recordException)
+    {
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.e("libInterface", problem);
+      }
+    else
+      {
+      if( recordException )
+        {
+        Exception ex = new Exception(problem);
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.setCustomKey("problem" , problem);
+        crashlytics.recordException(ex);
+        }
+      else
+        {
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.log(problem);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportScramblingProblem(int place, long pause, long resume, long time)
+    {
+    String error = "SCRAMBLING BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportRotationProblem(int place, long pause, long resume, long time)
+    {
+    String error = "ROTATION BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume);
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportThreadProblem(int place, long pause, long resume, long time)
+    {
+    String error = EffectMessageSender.reportState();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause  );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportBlockProblem(int type, int place, long pause, long resume, long time)
+    {
+    switch(type)
+      {
+      case BlockController.TYPE_SCRAMBLING: reportScramblingProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_ROTATION  : reportRotationProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_THREAD    : reportThreadProblem(place,pause,resume,time); break;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportJSONError(String error, int ordinal)
+    {
+    RubikObject object = RubikObjectList.getObject(ordinal);
+    String name = object==null ? "NULL" : object.getUpperName();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", "name="+name+" JSON error: "+error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("name" , name );
+      crashlytics.setCustomKey("JSONerror", error );
+      crashlytics.recordException(ex);
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/patternui/PatternRenderer.java b/src/main/java/org/distorted/patternui/PatternRenderer.java
new file mode 100644
index 00000000..1685fef0
--- /dev/null
+++ b/src/main/java/org/distorted/patternui/PatternRenderer.java
@@ -0,0 +1,158 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.patternui;
+
+import android.content.res.Resources;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.VertexEffectQuaternion;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.library.main.InternalOutputSurface;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.main.BuildConfig;
+import org.distorted.objectlib.effects.BaseEffect;
+import org.distorted.objectlib.main.ObjectControl;
+
+import java.io.InputStream;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PatternRenderer implements GLSurfaceView.Renderer, DistortedLibrary.LibraryUser
+{
+   public static final float BRIGHTNESS = 0.30f;
+
+   private final PatternSurfaceView mView;
+   private final Resources mResources;
+   private final DistortedScreen mScreen;
+   private final ObjectControl mControl;
+   private boolean mErrorShown;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   PatternRenderer(PatternSurfaceView v)
+     {
+     mView = v;
+     mResources = v.getResources();
+
+     mErrorShown = false;
+     mControl = v.getObjectControl();
+     mScreen = new DistortedScreen();
+     mScreen.glClearColor(BRIGHTNESS, BRIGHTNESS, BRIGHTNESS, 1.0f);
+     mScreen.enableDepthStencil(InternalOutputSurface.DEPTH_NO_STENCIL);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   DistortedScreen getScreen()
+     {
+     return mScreen;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// various things are done here delayed, 'after the next render' as not to be done mid-render and
+// cause artifacts.
+
+   @Override
+   public void onDrawFrame(GL10 glUnused)
+     {
+     long time = System.currentTimeMillis();
+     mControl.preRender();
+     mScreen.render(time);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceChanged(GL10 glUnused, int width, int height)
+      {
+      mScreen.resize(width,height);
+      mView.setScreenSize(width,height);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
+      {
+      DistortedLibrary.setMax(EffectType.VERTEX,ObjectControl.MAX_QUATS+1);
+      MeshBase.setMaxEffComponents(ObjectControl.MAX_MOVING_PARTS);
+
+      VertexEffectRotate.enable();
+      VertexEffectQuaternion.enable();
+      BaseEffect.Type.enableEffects();
+
+      DistortedLibrary.onSurfaceCreated(this,1);
+      DistortedLibrary.setCull(true);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void distortedException(Exception ex)
+     {
+     String message = ex.getMessage();
+     String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+     String version = GLES30.glGetString(GLES30.GL_VERSION);
+     String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+     String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+     if( message==null ) message = "exception NULL";
+
+     if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("DISTORTED", message );
+       android.util.Log.e("DISTORTED", "GLSL Version "+shading);
+       android.util.Log.e("DISTORTED", "GL Version "  +version);
+       android.util.Log.e("DISTORTED", "GL Vendor "   +vendor);
+       android.util.Log.e("DISTORTED", "GL Renderer " +renderer);
+       }
+     else
+       {
+       FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+       crashlytics.setCustomKey("DistortedError", message );
+       crashlytics.setCustomKey("GLSL Version"  , shading );
+       crashlytics.setCustomKey("GLversion"     , version );
+       crashlytics.setCustomKey("GL Vendor "    , vendor  );
+       crashlytics.setCustomKey("GLSLrenderer"  , renderer);
+       crashlytics.recordException(ex);
+       }
+
+     int glsl = DistortedLibrary.getGLSL();
+
+     if( glsl< 300 && !mErrorShown )
+       {
+       mErrorShown = true;
+       PatternActivity act = (PatternActivity)mView.getContext();
+       act.OpenGLError();
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public InputStream localFile(int fileID)
+     {
+     return mResources.openRawResource(fileID);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void logMessage(String message)
+     {
+     android.util.Log.e("Rubik", message );
+     }
+}
diff --git a/src/main/java/org/distorted/patternui/PatternSurfaceView.java b/src/main/java/org/distorted/patternui/PatternSurfaceView.java
new file mode 100644
index 00000000..18a5559b
--- /dev/null
+++ b/src/main/java/org/distorted/patternui/PatternSurfaceView.java
@@ -0,0 +1,149 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.patternui;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObjectNode;
+import org.distorted.os.OSInterface;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PatternSurfaceView extends GLSurfaceView
+{
+    private ObjectControl mObjectController;
+    private OSInterface mInterface;
+    private PatternRenderer mRenderer;
+    private boolean mCreated;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setScreenSize(int width, int height)
+      {
+      mObjectController.setScreenSizeAndScaling(width,height, Math.min(width, (int)(0.75f*height)));
+
+      if( !mCreated )
+        {
+        mCreated = true;
+        mObjectController.createNode(width,height);
+        TwistyObjectNode objectNode = mObjectController.getNode();
+        objectNode.glDepthMask(false);
+        objectNode.glStencilMask(0);
+        mRenderer.getScreen().attach(objectNode);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    PatternRenderer getRenderer()
+      {
+      return mRenderer;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    OSInterface getInterface()
+      {
+      return mInterface;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    ObjectControl getObjectControl()
+      {
+      return mObjectController;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public PatternSurfaceView(Context context, AttributeSet attrs)
+      {
+      super(context,attrs);
+
+      mCreated = false;
+
+      if(!isInEditMode())
+        {
+        PatternActivity act = (PatternActivity)context;
+        PatternObjectLibInterface ref = new PatternObjectLibInterface(act);
+        mInterface = new OSInterface(act,ref);
+        mObjectController = new ObjectControl(mInterface);
+        mRenderer = new PatternRenderer(this);
+
+        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        try
+          {
+          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+          int esVersion = configurationInfo.reqGlEsVersion>>16;
+          setEGLContextClientVersion(esVersion);
+          setRenderer(mRenderer);
+          }
+        catch(Exception ex)
+          {
+          act.OpenGLError();
+
+          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+          String version = GLES30.glGetString(GLES30.GL_VERSION);
+          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+          crashlytics.setCustomKey("GLSL Version"  , shading );
+          crashlytics.setCustomKey("GL version"    , version );
+          crashlytics.setCustomKey("GL Vendor "    , vendor  );
+          crashlytics.setCustomKey("GLSL renderer" , renderer);
+          crashlytics.recordException(ex);
+          }
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onPause()
+      {
+      super.onPause();
+      mObjectController.onPause();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onResume()
+      {
+      super.onResume();
+      mObjectController.onResume();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @SuppressLint("ClickableViewAccessibility")
+    @Override
+    public boolean onTouchEvent(MotionEvent event)
+      {
+      mInterface.setMotionEvent(event);
+      int mode = ScreenList.getMode();
+      return mObjectController.onTouchEvent(mode);
+      }
+}
+
diff --git a/src/main/java/org/distorted/patternui/ScreenAbstract.java b/src/main/java/org/distorted/patternui/ScreenAbstract.java
new file mode 100644
index 00000000..e2d4676d
--- /dev/null
+++ b/src/main/java/org/distorted/patternui/ScreenAbstract.java
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.patternui;
+
+import android.content.SharedPreferences;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class ScreenAbstract
+  {
+  abstract void enterScreen(PatternActivity act);
+  abstract void leaveScreen(PatternActivity act);
+  public abstract void savePreferences(SharedPreferences.Editor editor);
+  public abstract void restorePreferences(SharedPreferences preferences);
+  }
diff --git a/src/main/java/org/distorted/patternui/ScreenList.java b/src/main/java/org/distorted/patternui/ScreenList.java
new file mode 100644
index 00000000..fbf62cb9
--- /dev/null
+++ b/src/main/java/org/distorted/patternui/ScreenList.java
@@ -0,0 +1,148 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.patternui;
+
+import static org.distorted.objectlib.main.ObjectControl.MODE_DRAG;
+
+import android.content.SharedPreferences;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public enum ScreenList
+  {
+  PATT ( null, MODE_DRAG, new ScreenPattern()  ),
+  ;
+
+  public static final int LENGTH = values().length;
+  private static final ScreenList[] screens;
+  private final ScreenList mBack;
+  private final int mMode;
+  private final ScreenAbstract mClass;
+
+  private static ScreenList mCurrScreen;
+
+  static
+    {
+    int i = 0;
+    screens = new ScreenList[LENGTH];
+    for(ScreenList state: ScreenList.values()) screens[i++] = state;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getScreen(int ordinal)
+    {
+    return ordinal>=0 && ordinal<LENGTH ?  screens[ordinal] : PATT;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getScreenFromName(String name)
+    {
+    for(int i=0; i<LENGTH; i++)
+      {
+      if( name.equals(screens[i].name()) )
+        {
+        return screens[i];
+        }
+      }
+
+    return PATT;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getCurrentScreen()
+    {
+    return mCurrScreen;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getMode()
+    {
+    return mCurrScreen.mMode;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void savePreferences(SharedPreferences.Editor editor)
+    {
+    editor.putString("curr_state_name", mCurrScreen.name() );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void restorePreferences(SharedPreferences preferences)
+    {
+    String currScreenName = preferences.getString("curr_state_name", ScreenList.PATT.name() );
+    mCurrScreen = getScreenFromName(currScreenName);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void goBack(PatternActivity act)
+    {
+    switchScreen(act, mCurrScreen.mBack );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void setScreen(PatternActivity act)
+    {
+    mCurrScreen.enterScreen(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void switchScreen(PatternActivity act, ScreenList next)
+    {
+    if( next!=null )
+      {
+      if( mCurrScreen !=null ) mCurrScreen.leaveScreen(act);
+      next.enterScreen(act);
+      mCurrScreen = next;
+      }
+    else
+      {
+      act.finish();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ScreenList(ScreenList back, int mode, ScreenAbstract clazz)
+    {
+    mBack = back;
+    mMode = mode;
+    mClass= clazz;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ScreenAbstract getScreenClass()
+    {
+    return mClass;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void leaveScreen(PatternActivity act)
+    {
+    mClass.leaveScreen(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void enterScreen(PatternActivity act)
+    {
+    mClass.enterScreen(act);
+    }
+  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/patternui/ScreenPattern.java b/src/main/java/org/distorted/patternui/ScreenPattern.java
new file mode 100644
index 00000000..e23cc7e1
--- /dev/null
+++ b/src/main/java/org/distorted/patternui/ScreenPattern.java
@@ -0,0 +1,225 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.patternui;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+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.dialogs.RubikDialogError;
+import org.distorted.dialogs.RubikDialogPatternSingle;
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.patterns.RubikPattern;
+import org.distorted.objects.RubikObjectList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenPattern extends ScreenAbstract
+  {
+  private TextView mText;
+  private TransparentImageButton mPrevButton, mNextButton, mBackButton;
+  private TextView mMovesText;
+  private int mNumMoves;
+  private int mPatternOrdinal, mCategory, mPattern;
+  private float mButtonSize;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ScreenPattern()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(PatternActivity act)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final PatternActivity act)
+    {
+    float width = act.getScreenWidthInPixels();
+    mButtonSize = width*PatternActivity.BUTTON_TEXT_SIZE;
+    float titleSize  = width*PatternActivity.TITLE_TEXT_SIZE;
+    LayoutInflater inflater = act.getLayoutInflater();
+
+    mPatternOrdinal = -1;
+    mCategory       = -1;
+    mPattern        = -1;
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+    mText = (TextView)inflater.inflate(R.layout.upper_pattern_text, null);
+    mText.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
+    mText.setText(R.string.patterns);
+    layoutTop.addView(mText);
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    setupPrevButton(act);
+    setupNextButton(act);
+    setupTextView(act,width);
+
+    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);
+
+    layoutLeft.addView(mPrevButton);
+    layoutLeft.addView(mMovesText);
+    layoutLeft.addView(mNextButton);
+
+    setupBackButton(act);
+
+    layoutRight.addView(mBackButton);
+
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutMid);
+    layoutBot.addView(layoutRight);
+
+    Bundle bundle = new Bundle();
+    bundle.putString("argument",RubikObjectList.getCurrentName());
+    RubikDialogPatternSingle diag = new RubikDialogPatternSingle();
+    diag.setArguments(bundle);
+    diag.show( act.getSupportFragmentManager(), RubikDialogPatternSingle.getDialogTag() );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final PatternActivity 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 setupPrevButton(final PatternActivity 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)
+        {
+        RubikPattern pattern = RubikPattern.getInstance();
+        ObjectControl control = act.getControl();
+        pattern.backMove( control, mPatternOrdinal, mCategory, mPattern);
+        int currMove = pattern.getCurMove(mPatternOrdinal, mCategory, mPattern);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,currMove,mNumMoves));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextButton(final PatternActivity 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)
+        {
+        RubikPattern pattern = RubikPattern.getInstance();
+        ObjectControl control = act.getControl();
+        pattern.makeMove( control, mPatternOrdinal, mCategory, mPattern);
+        int currMove = pattern.getCurMove(mPatternOrdinal, mCategory, mPattern);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,currMove,mNumMoves));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextView(final PatternActivity act, final float width)
+    {
+    int padding = (int)(width*PatternActivity.PADDING);
+    int margin  = (int)(width*PatternActivity.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,0,0));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setPattern(final PatternActivity act, int ordinal, int category, int pattern)
+    {
+    mPatternOrdinal = ordinal;
+    mCategory       = category;
+    mPattern        = pattern;
+
+    RubikPattern patt = RubikPattern.getInstance();
+    String patternName = patt.getPatternName(ordinal,category,pattern);
+    mText.setText(patternName);
+
+    mNumMoves   = patt.getNumMoves(ordinal,category,pattern);
+    int currMove= patt.getCurMove(ordinal,category,pattern);
+
+    mMovesText.setText(act.getString(R.string.mo_placeholder,currMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+
+    }
+  }
diff --git a/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java b/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java
index 35325e04..98aaa57c 100644
--- a/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java
+++ b/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java
@@ -18,14 +18,13 @@ import org.distorted.objectlib.helpers.ObjectLibInterface;
 import org.distorted.objectlib.main.ObjectControl;
 import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
-import org.distorted.overlays.ListenerOverlay;
 import org.distorted.solvers.SolverMain;
 
 import java.lang.ref.WeakReference;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public class SolverObjectLibInterface implements ObjectLibInterface, ListenerOverlay
+public class SolverObjectLibInterface implements ObjectLibInterface
 {
   private final WeakReference<SolverActivity> mAct;
   private int mLastCubitColor, mLastCubit, mLastCubitFace;
@@ -47,7 +46,6 @@ public class SolverObjectLibInterface implements ObjectLibInterface, ListenerOve
   public void onSolved() { }
   public void onObjectCreated(long time) { }
   public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum) { }
-  public void overlayFinished(long id) { }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/res/layout/pattern.xml b/src/main/res/layout/pattern.xml
new file mode 100644
index 00000000..023f00bb
--- /dev/null
+++ b/src/main/res/layout/pattern.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout  xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/relativeLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <org.distorted.patternui.PatternSurfaceView
+        android:id="@+id/patternSurfaceView"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentTop="true"/>
+
+    <LinearLayout
+        android:id="@+id/hiddenBar"
+        android:layout_alignParentTop="true"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:gravity="center"
+        android:orientation="horizontal"
+        android:background="@android:color/transparent">
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/upperBar"
+        android:layout_below="@id/hiddenBar"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:gravity="center"
+        android:orientation="horizontal"
+        android:background="@android:color/transparent">
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/lowerBar"
+        android:layout_alignParentBottom="true"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:orientation="horizontal"
+        android:background="@android:color/transparent">
+    </LinearLayout>
+
+</RelativeLayout>
