Merge pull request #6338 from PojavLauncherTeam/fix/QoL_stuff

QoL update
This commit is contained in:
Maksim Belov 2024-12-02 13:07:37 +03:00 committed by GitHub
commit fd6bea9b39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 144 additions and 49 deletions

View File

@ -204,7 +204,7 @@ dependencies {
implementation 'com.github.PojavLauncherTeam:portrait-ssp:6c02fd739b' implementation 'com.github.PojavLauncherTeam:portrait-ssp:6c02fd739b'
implementation 'com.github.Mathias-Boulay:ExtendedView:1.0.0' implementation 'com.github.Mathias-Boulay:ExtendedView:1.0.0'
implementation 'com.github.Mathias-Boulay:android_gamepad_remapper:2.0.3' implementation 'com.github.Mathias-Boulay:android_gamepad_remapper:2.0.3'
implementation 'com.github.Mathias-Boulay:virtual-joystick-android:2e7aa25e50' implementation 'com.github.Mathias-Boulay:virtual-joystick-android:1.14'
// implementation 'com.intuit.sdp:sdp-android:1.0.5' // implementation 'com.intuit.sdp:sdp-android:1.0.5'
// implementation 'com.intuit.ssp:ssp-android:1.0.5' // implementation 'com.intuit.ssp:ssp-android:1.0.5'

View File

@ -57,13 +57,13 @@ public class CustomControls {
this.mControlDataList.add(new ControlData(ctx, R.string.control_jump, new int[]{LwjglGlfwKeycode.GLFW_KEY_SPACE}, "${right} - ${margin} * 2 - ${width}", "${bottom} - ${margin} * 2 - ${height}", true)); this.mControlDataList.add(new ControlData(ctx, R.string.control_jump, new int[]{LwjglGlfwKeycode.GLFW_KEY_SPACE}, "${right} - ${margin} * 2 - ${width}", "${bottom} - ${margin} * 2 - ${height}", true));
//The default controls are conform to the V3 //The default controls are conform to the V3
version = 7; version = 8;
} }
public void save(String path) throws IOException { public void save(String path) throws IOException {
//Current version is the V3.1 so the version as to be marked as 7 ! //Current version is the V3.2 so the version as to be marked as 8 !
version = 7; version = 8;
Tools.write(path, Tools.GLOBAL_GSON.toJson(this)); Tools.write(path, Tools.GLOBAL_GSON.toJson(this));
} }

View File

@ -24,27 +24,56 @@ public class LayoutConverter {
CustomControls layout = LayoutConverter.convertV1Layout(layoutJobj); CustomControls layout = LayoutConverter.convertV1Layout(layoutJobj);
layout.save(jsonPath); layout.save(jsonPath);
return layout; return layout;
} else if (layoutJobj.getInt("version") == 2) {
CustomControls layout = LayoutConverter.convertV2Layout(layoutJobj);
layout.save(jsonPath);
return layout;
}else if (layoutJobj.getInt("version") >= 3 && layoutJobj.getInt("version") <= 5) {
return LayoutConverter.convertV3_4Layout(layoutJobj);
} else if (layoutJobj.getInt("version") == 6 || layoutJobj.getInt("version") == 7) {
return Tools.GLOBAL_GSON.fromJson(jsonLayoutData, CustomControls.class);
} else { } else {
return null; int version = layoutJobj.getInt("version");
if (version == 2) {
CustomControls layout = LayoutConverter.convertV2Layout(layoutJobj);
layout.save(jsonPath);
return layout;
}
if (version == 3 || version == 4 || version == 5) {
return LayoutConverter.convertV3_4Layout(layoutJobj);
}
if (version == 6 || version == 7) {
return convertV6_7Layout(layoutJobj);
}
else if (version == 8) {
return Tools.GLOBAL_GSON.fromJson(jsonLayoutData, CustomControls.class);
}
} }
return null;
} catch (JSONException e) { } catch (JSONException e) {
throw new JsonSyntaxException("Failed to load", e); throw new JsonSyntaxException("Failed to load", e);
} }
} }
/**
* Normalize the layout to v8 from v6/7. An issue from the joystick height and position has to be fixed.
* @param oldLayoutJson The old layout
* @return The new layout with the fixed joystick height
*/
public static CustomControls convertV6_7Layout(JSONObject oldLayoutJson) {
CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
for (ControlJoystickData data : layout.mJoystickDataList) {
if (data.getHeight() > data.getWidth()) {
// Make the size square, adjust the dynamic position related to height
float ratio = data.getHeight() / data.getWidth();
data.dynamicX = data.dynamicX.replace("${height}", "(" + ratio + " * ${height})");
data.dynamicY = data.dynamicY.replace("${height}", "(" + ratio + " * ${height})") + " + (" + (ratio-1) + " * ${height})";
data.setHeight(data.getWidth());
}
}
layout.version = 8;
return layout;
}
/** /**
* Normalize the layout to v6 from v3/4: The stroke width is no longer dependant on the button size * Normalize the layout to v6 from v3/4: The stroke width is no longer dependant on the button size
*/ */
public static CustomControls convertV3_4Layout(JSONObject oldLayoutJson) { private static CustomControls convertV3_4Layout(JSONObject oldLayoutJson) {
CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class); CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
convertStrokeWidth(layout); convertStrokeWidth(layout);
layout.version = 6; layout.version = 6;
@ -52,7 +81,7 @@ public class LayoutConverter {
} }
public static CustomControls convertV2Layout(JSONObject oldLayoutJson) throws JSONException { private static CustomControls convertV2Layout(JSONObject oldLayoutJson) throws JSONException {
CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class); CustomControls layout = Tools.GLOBAL_GSON.fromJson(oldLayoutJson.toString(), CustomControls.class);
JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList"); JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList");
layout.mControlDataList = new ArrayList<>(layoutMainArray.length()); layout.mControlDataList = new ArrayList<>(layoutMainArray.length());
@ -95,7 +124,7 @@ public class LayoutConverter {
return layout; return layout;
} }
public static CustomControls convertV1Layout(JSONObject oldLayoutJson) throws JSONException { private static CustomControls convertV1Layout(JSONObject oldLayoutJson) throws JSONException {
CustomControls empty = new CustomControls(); CustomControls empty = new CustomControls();
JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList"); JSONArray layoutMainArray = oldLayoutJson.getJSONArray("mControlDataList");
for (int i = 0; i < layoutMainArray.length(); i++) { for (int i = 0; i < layoutMainArray.length(); i++) {

View File

@ -16,23 +16,22 @@ public class DrawerPullButton extends View {
public DrawerPullButton(Context context) {super(context); init();} public DrawerPullButton(Context context) {super(context); init();}
public DrawerPullButton(Context context, @Nullable AttributeSet attrs) {super(context, attrs); init();} public DrawerPullButton(Context context, @Nullable AttributeSet attrs) {super(context, attrs); init();}
private final Paint mPaint = new Paint(); private final Paint mBackgroundPaint = new Paint();
private VectorDrawableCompat mDrawable; private VectorDrawableCompat mDrawable;
private void init(){ private void init(){
mDrawable = VectorDrawableCompat.create(getContext().getResources(), R.drawable.ic_sharp_settings_24, null); mDrawable = VectorDrawableCompat.create(getContext().getResources(), R.drawable.ic_sharp_settings_24, null);
setAlpha(0.33f); setAlpha(0.33f);
mBackgroundPaint.setColor(Color.BLACK);
} }
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.BLACK); canvas.drawArc(getPaddingLeft(),-getHeight() + getPaddingBottom(),getWidth() - getPaddingRight(), getHeight() - getPaddingBottom(), 0, 180, true, mBackgroundPaint);
canvas.drawArc(0,-getHeight(),getWidth(), getHeight(), 0, 180, true, mPaint);
mPaint.setColor(Color.WHITE); mDrawable.setBounds(getPaddingLeft()/2, getPaddingTop()/2, getHeight() - getPaddingRight()/2, getHeight() - getPaddingBottom()/2);
mDrawable.setBounds(0, 0, getHeight(), getHeight());
canvas.save(); canvas.save();
canvas.translate((getWidth()-getHeight())/2f, 0); canvas.translate((getWidth()-getHeight())/2f, -getPaddingBottom()/2f);
mDrawable.draw(canvas); mDrawable.draw(canvas);
canvas.restore(); canvas.restore();
} }

View File

@ -332,6 +332,7 @@ public class EditControlSideDialog extends SideDialogView {
/** /**
* A long function linking all the displayed data on the popup and, * A long function linking all the displayed data on the popup and,
* the currently edited mCurrentlyEditedButton * the currently edited mCurrentlyEditedButton
* @noinspection SuspiciousNameCombination
*/ */
private void setupRealTimeListeners() { private void setupRealTimeListeners() {
mNameEditText.addTextChangedListener((SimpleTextWatcher) s -> { mNameEditText.addTextChangedListener((SimpleTextWatcher) s -> {
@ -349,6 +350,10 @@ public class EditControlSideDialog extends SideDialogView {
float width = safeParseFloat(s.toString()); float width = safeParseFloat(s.toString());
if (width >= 0) { if (width >= 0) {
mCurrentlyEditedButton.getProperties().setWidth(width); mCurrentlyEditedButton.getProperties().setWidth(width);
if (mCurrentlyEditedButton.getProperties() instanceof ControlJoystickData) {
// Joysticks are square
mCurrentlyEditedButton.getProperties().setHeight(width);
}
mCurrentlyEditedButton.updateProperties(); mCurrentlyEditedButton.updateProperties();
} }
}); });
@ -359,6 +364,10 @@ public class EditControlSideDialog extends SideDialogView {
float height = safeParseFloat(s.toString()); float height = safeParseFloat(s.toString());
if (height >= 0) { if (height >= 0) {
mCurrentlyEditedButton.getProperties().setHeight(height); mCurrentlyEditedButton.getProperties().setHeight(height);
if (mCurrentlyEditedButton.getProperties() instanceof ControlJoystickData) {
// Joysticks are square
mCurrentlyEditedButton.getProperties().setWidth(height);
}
mCurrentlyEditedButton.updateProperties(); mCurrentlyEditedButton.updateProperties();
} }
}); });

View File

@ -11,6 +11,7 @@ import android.view.ViewParent;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import net.kdt.pojavlaunch.GrabListener;
import net.kdt.pojavlaunch.LwjglGlfwKeycode; import net.kdt.pojavlaunch.LwjglGlfwKeycode;
import net.kdt.pojavlaunch.prefs.LauncherPreferences; import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import net.kdt.pojavlaunch.utils.MCOptionUtils; import net.kdt.pojavlaunch.utils.MCOptionUtils;
@ -26,8 +27,16 @@ public class HotbarView extends View implements MCOptionUtils.MCOptionListener,
LwjglGlfwKeycode.GLFW_KEY_4, LwjglGlfwKeycode.GLFW_KEY_5, LwjglGlfwKeycode.GLFW_KEY_6, LwjglGlfwKeycode.GLFW_KEY_4, LwjglGlfwKeycode.GLFW_KEY_5, LwjglGlfwKeycode.GLFW_KEY_6,
LwjglGlfwKeycode.GLFW_KEY_7, LwjglGlfwKeycode.GLFW_KEY_8, LwjglGlfwKeycode.GLFW_KEY_9}; LwjglGlfwKeycode.GLFW_KEY_7, LwjglGlfwKeycode.GLFW_KEY_8, LwjglGlfwKeycode.GLFW_KEY_9};
private final DropGesture mDropGesture = new DropGesture(new Handler(Looper.getMainLooper())); private final DropGesture mDropGesture = new DropGesture(new Handler(Looper.getMainLooper()));
private final GrabListener mGrabListener = new GrabListener() {
@Override
public void onGrabState(boolean isGrabbing) {
mLastIndex = -1;
mDropGesture.cancel();
}
};
private int mWidth; private int mWidth;
private int mLastIndex; private int mLastIndex = -1;
private int mGuiScale; private int mGuiScale;
public HotbarView(Context context) { public HotbarView(Context context) {
@ -66,6 +75,13 @@ public class HotbarView extends View implements MCOptionUtils.MCOptionListener,
} }
mGuiScale = MCOptionUtils.getMcScale(); mGuiScale = MCOptionUtils.getMcScale();
repositionView(); repositionView();
CallbackBridge.addGrabListener(mGrabListener);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
CallbackBridge.removeGrabListener(mGrabListener);
} }
private void repositionView() { private void repositionView() {

View File

@ -33,8 +33,12 @@ public class InGameEventProcessor implements TouchEventProcessor {
case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_MOVE:
mTracker.trackEvent(motionEvent); mTracker.trackEvent(motionEvent);
float[] motionVector = mTracker.getMotionVector(); float[] motionVector = mTracker.getMotionVector();
CallbackBridge.mouseX += (float) (motionVector[0] * mSensitivity); float deltaX = (float) (motionVector[0] * mSensitivity);
CallbackBridge.mouseY += (float) (motionVector[1] * mSensitivity); float deltaY = (float) (motionVector[1] * mSensitivity);
mLeftClickGesture.setMotion(deltaX, deltaY);
mRightClickGesture.setMotion(deltaX, deltaY);
CallbackBridge.mouseX += deltaX;
CallbackBridge.mouseY += deltaY;
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY); CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
if(LauncherPreferences.PREF_DISABLE_GESTURES) break; if(LauncherPreferences.PREF_DISABLE_GESTURES) break;
checkGestures(); checkGestures();

View File

@ -13,7 +13,7 @@ import org.lwjgl.glfw.CallbackBridge;
public class LeftClickGesture extends ValidatorGesture { public class LeftClickGesture extends ValidatorGesture {
public static final int FINGER_STILL_THRESHOLD = (int) Tools.dpToPx(9); public static final int FINGER_STILL_THRESHOLD = (int) Tools.dpToPx(9);
private float mGestureStartX, mGestureStartY; private float mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY;
private boolean mMouseActivated; private boolean mMouseActivated;
public LeftClickGesture(Handler handler) { public LeftClickGesture(Handler handler) {
@ -22,14 +22,14 @@ public class LeftClickGesture extends ValidatorGesture {
public final void inputEvent() { public final void inputEvent() {
if(submit()) { if(submit()) {
mGestureStartX = CallbackBridge.mouseX; mGestureStartX = mGestureEndX = CallbackBridge.mouseX;
mGestureStartY = CallbackBridge.mouseY; mGestureStartY = mGestureEndY = CallbackBridge.mouseY;
} }
} }
@Override @Override
public boolean checkAndTrigger() { public boolean checkAndTrigger() {
boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, FINGER_STILL_THRESHOLD); boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY, FINGER_STILL_THRESHOLD);
// If the finger is still, fire the gesture. // If the finger is still, fire the gesture.
if(fingerStill) { if(fingerStill) {
sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, true); sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, true);
@ -47,6 +47,11 @@ public class LeftClickGesture extends ValidatorGesture {
} }
} }
public void setMotion(float deltaX, float deltaY) {
mGestureEndX += deltaX;
mGestureEndY += deltaY;
}
/** /**
* Check if the finger is still when compared to mouseX/mouseY in CallbackBridge. * Check if the finger is still when compared to mouseX/mouseY in CallbackBridge.
* @param startX the starting X of the gesture * @param startX the starting X of the gesture
@ -61,4 +66,13 @@ public class LeftClickGesture extends ValidatorGesture {
startY startY
) <= threshold; ) <= threshold;
} }
public static boolean isFingerStill(float startX, float startY, float endX, float endY, float threshold) {
return MathUtils.dist(
endX,
endY,
startX,
startY
) <= threshold;
}
} }

View File

@ -9,7 +9,7 @@ import org.lwjgl.glfw.CallbackBridge;
public class RightClickGesture extends ValidatorGesture{ public class RightClickGesture extends ValidatorGesture{
private boolean mGestureEnabled = true; private boolean mGestureEnabled = true;
private boolean mGestureValid = true; private boolean mGestureValid = true;
private float mGestureStartX, mGestureStartY; private float mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY;
public RightClickGesture(Handler mHandler) { public RightClickGesture(Handler mHandler) {
super(mHandler, 150); super(mHandler, 150);
} }
@ -24,6 +24,11 @@ public class RightClickGesture extends ValidatorGesture{
} }
} }
public void setMotion(float deltaX, float deltaY) {
mGestureEndX += deltaX;
mGestureEndY += deltaY;
}
@Override @Override
public boolean checkAndTrigger() { public boolean checkAndTrigger() {
// If the validate() method was called, it means that the user held on for too long. The cancellation should be ignored. // If the validate() method was called, it means that the user held on for too long. The cancellation should be ignored.
@ -38,7 +43,7 @@ public class RightClickGesture extends ValidatorGesture{
public void onGestureCancelled(boolean isSwitching) { public void onGestureCancelled(boolean isSwitching) {
mGestureEnabled = true; mGestureEnabled = true;
if(!mGestureValid || isSwitching) return; if(!mGestureValid || isSwitching) return;
boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, LeftClickGesture.FINGER_STILL_THRESHOLD); boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY, mGestureEndX, mGestureEndY, LeftClickGesture.FINGER_STILL_THRESHOLD);
if(!fingerStill) return; if(!fingerStill) return;
CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, true); CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, true);
CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, false); CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, false);

View File

@ -107,7 +107,28 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
Tools.removeCurrentFragment(requireActivity()); Tools.removeCurrentFragment(requireActivity());
}); });
mGameDirButton.setOnClickListener(v -> {
View.OnClickListener gameDirListener = getGameDirListener();
mGameDirButton.setOnClickListener(gameDirListener);
mDefaultPath.setOnClickListener(gameDirListener);
View.OnClickListener controlSelectListener = getControlSelectListener();
mControlSelectButton.setOnClickListener(controlSelectListener);
mDefaultControl.setOnClickListener(controlSelectListener);
// Setup the expendable list behavior
View.OnClickListener versionSelectListener = getVersionSelectListener();
mVersionSelectButton.setOnClickListener(versionSelectListener);
mDefaultVersion.setOnClickListener(versionSelectListener);
// Set up the icon change click listener
mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));
loadValues(LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, ""), view.getContext());
}
private View.OnClickListener getGameDirListener() {
return v -> {
Bundle bundle = new Bundle(2); Bundle bundle = new Bundle(2);
bundle.putBoolean(FileSelectorFragment.BUNDLE_SELECT_FOLDER, true); bundle.putBoolean(FileSelectorFragment.BUNDLE_SELECT_FOLDER, true);
bundle.putString(FileSelectorFragment.BUNDLE_ROOT_PATH, Tools.DIR_GAME_HOME); bundle.putString(FileSelectorFragment.BUNDLE_ROOT_PATH, Tools.DIR_GAME_HOME);
@ -116,9 +137,11 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
Tools.swapFragment(requireActivity(), Tools.swapFragment(requireActivity(),
FileSelectorFragment.class, FileSelectorFragment.TAG, bundle); FileSelectorFragment.class, FileSelectorFragment.TAG, bundle);
}); };
}
mControlSelectButton.setOnClickListener(v -> { private View.OnClickListener getControlSelectListener() {
return v -> {
Bundle bundle = new Bundle(3); Bundle bundle = new Bundle(3);
bundle.putBoolean(FileSelectorFragment.BUNDLE_SELECT_FOLDER, false); bundle.putBoolean(FileSelectorFragment.BUNDLE_SELECT_FOLDER, false);
bundle.putString(FileSelectorFragment.BUNDLE_ROOT_PATH, Tools.CTRLMAP_PATH); bundle.putString(FileSelectorFragment.BUNDLE_ROOT_PATH, Tools.CTRLMAP_PATH);
@ -126,20 +149,14 @@ public class ProfileEditorFragment extends Fragment implements CropperUtils.Crop
Tools.swapFragment(requireActivity(), Tools.swapFragment(requireActivity(),
FileSelectorFragment.class, FileSelectorFragment.TAG, bundle); FileSelectorFragment.class, FileSelectorFragment.TAG, bundle);
}); };
}
// Setup the expendable list behavior private View.OnClickListener getVersionSelectListener() {
mVersionSelectButton.setOnClickListener(v -> VersionSelectorDialog.open(v.getContext(), false, (id, snapshot)->{ return v -> VersionSelectorDialog.open(v.getContext(), false, (id, snapshot)-> {
mTempProfile.lastVersionId = id; mTempProfile.lastVersionId = id;
mDefaultVersion.setText(id); mDefaultVersion.setText(id);
})); });
// Set up the icon change click listener
mProfileIcon.setOnClickListener(v -> CropperUtils.startCropper(mCropperLauncher));
loadValues(LauncherPreferences.DEFAULT_PREF.getString(LauncherPreferences.PREF_KEY_CURRENT_PROFILE, ""), view.getContext());
} }

View File

@ -51,8 +51,9 @@
<net.kdt.pojavlaunch.customcontrols.handleview.DrawerPullButton <net.kdt.pojavlaunch.customcontrols.handleview.DrawerPullButton
android:id="@+id/drawer_button" android:id="@+id/drawer_button"
android:layout_width="24dp" android:layout_width="40dp"
android:layout_height="12dp" android:layout_height="20dp"
android:padding="8dp"
android:elevation="10dp" android:elevation="10dp"
android:layout_gravity="center_horizontal"/> android:layout_gravity="center_horizontal"/>
<net.kdt.pojavlaunch.customcontrols.mouse.HotbarView <net.kdt.pojavlaunch.customcontrols.mouse.HotbarView

View File

@ -15,8 +15,9 @@
<net.kdt.pojavlaunch.customcontrols.handleview.DrawerPullButton <net.kdt.pojavlaunch.customcontrols.handleview.DrawerPullButton
android:id="@+id/drawer_button" android:id="@+id/drawer_button"
android:layout_width="32dp" android:layout_width="40dp"
android:layout_height="16dp" android:layout_height="20dp"
android:padding="6dp"
android:elevation="10dp" android:elevation="10dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"