widgetGroups = new ArrayList<> ();
+
// This will make sure to apply listeners, background and data to view that are inflated from XML.
- private final IInflateListener mInflateListener = new IInflateListener() {
-
+ private final IInflateListener mInflateListener = new IInflateListener () {
+
@Override
- public void onBeginInflate() {
+ public void onBeginInflate () {
}
-
+
@Override
- public void onInflateView(IView view, IViewGroup parent) {
+ public void onInflateView (IView view, IViewGroup parent) {
if (view instanceof IViewGroup) {
- View v = view.asView();
- v.setBackgroundResource(R.drawable.bg_design_container_background);
- v.setOnDragListener(getOnDragListener((IViewGroup) view));
+ View v = view.asView ();
+ v.setBackgroundResource (R.drawable.bg_design_container_background);
+ v.setOnDragListener (getOnDragListener ((IViewGroup) view));
}
-
+
+ setupInflatedView (view);
setDragDataToInflatedView (view);
}
-
+
@Override
- public void onApplyAttribute(IAttribute attr, IView view) {
-
+ public void onApplyAttribute (IAttribute attr, IView view) {
+
}
-
+
@Override
- public void onFinishInflate(IView rootView) {
-
+ public void onFinishInflate (IView rootView) {
+
}
};
-
+
@Override
- protected View bindLayout() {
- mBinding = ActivityDesignerBinding.inflate(getLayoutInflater());
- return mBinding.getRoot();
+ protected View bindLayout () {
+ this.mBinding = ActivityDesignerBinding.inflate (getLayoutInflater ());
+ return mBinding.getRoot ();
}
-
+
@Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setSupportActionBar(mBinding.toolbar);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
- final var toggle = new ActionBarDrawerToggle(this, mBinding.getRoot(), mBinding.toolbar, R.string.app_name, R.string.app_name);
- mBinding.getRoot().addDrawerListener(toggle);
- toggle.syncState();
+ protected void onCreate (Bundle savedInstanceState) {
+ super.onCreate (savedInstanceState);
- final var extras = getIntent().getExtras();
- final var path = extras.getString(KEY_LAYOUT_PATH, null);
- final var name = path.substring(path.lastIndexOf(File.separator) + 1);
-
- getSupportActionBar().setTitle(name);
-
- try {
- final ILayoutInflater inflater = getApp().getLayoutInflater();
- inflater.resetContextProvider(newContextProvider());
- inflater.registerInflateListener(this.mInflateListener);
- final IView view = inflater.inflatePath(path, mBinding.layoutContainer);
-
- final var layoutContainerGroup = new UiViewGroup(mBinding.layoutContainer.getClass().getName(), mBinding.layoutContainer);
- layoutContainerGroup.addView(view);
-
- mBinding.layoutContainer.setOnDragListener(getOnDragListener(layoutContainerGroup));
-
- LOG.debug("Generated code: ", view.generateCode());
- } catch (Throwable th) {
- mBinding.layoutContainer.removeAllViews();
- mBinding.layoutContainer.addView(createErrorText(th));
+ setSupportActionBar (mBinding.toolbar);
+ getSupportActionBar ().setDisplayHomeAsUpEnabled (true);
+
+ mBinding.getRoot ().setScrimColor (Color.TRANSPARENT);
+ final var toggle = new ActionBarDrawerToggle (this, mBinding.getRoot (), mBinding.toolbar, R.string.app_name, R.string.app_name) {
- LOG.error (getString(R.string.err_cannot_inflate_layout), th);
+ @Override
+ public void onDrawerSlide (View drawerView, float slideOffset) {
+ super.onDrawerSlide (drawerView, slideOffset);
+ if (isTablet) {
+ mBinding.mainContainer.setTranslationX (drawerView.getWidth () * slideOffset);
+ }
+ }
+ };
+ mBinding.getRoot ().addDrawerListener (toggle);
+ toggle.syncState ();
+
+ final var extras = getIntent ().getExtras ();
+ final var path = extras.getString (KEY_LAYOUT_PATH, null);
+ final var name = path.substring (path.lastIndexOf (File.separator) + 1);
+ getSupportActionBar ().setTitle (name);
+
+ this.selectedViewForeground = generateSelectedViewForeground ();
+
+ try {
+ final ILayoutInflater inflater = getApp ().getLayoutInflater ();
+ inflater.resetContextProvider (newContextProvider ());
+ inflater.registerInflateListener (this.mInflateListener);
+ final IView view = inflater.inflatePath (path, mBinding.layoutContainer);
+
+ final var layoutContainerGroup = new UiViewGroup (mBinding.layoutContainer.getClass ().getName (), mBinding.layoutContainer);
+ layoutContainerGroup.addView (view);
+
+ mBinding.layoutContainer.setOnDragListener (getOnDragListener (layoutContainerGroup));
+ } catch (Throwable th) {
+ mBinding.layoutContainer.removeAllViews ();
+ mBinding.layoutContainer.addView (createErrorText (th));
+
+ LOG.error (getString (R.string.err_cannot_inflate_layout), th);
+ }
+
+ setupWidgets ();
+
+ // Always open the drawer if this device is a tablet.
+ if (isTablet) {
+ mBinding.getRoot ().openDrawer (GravityCompat.START);
+ mBinding.getRoot ().setDrawerLockMode (DrawerLayout.LOCK_MODE_LOCKED_OPEN);
}
-
- setupWidgets();
}
-
- private void onDropWidget(@NonNull IView view ) {
- view.asView().setOnClickListener(v -> onLayoutViewClick (view));
- view.asView().setOnLongClickListener(v -> onLayoutViewLongClick (view));
-
- setDragDataToInflatedView(view);
+
+ private void setupInflatedView (@NonNull IView view) {
+ view.asView ().setSelected (false);
+ view.asView ().setOnTouchListener (
+ new WidgetTouchListener (
+ view,
+ this::onLayoutViewClick,
+ this::onLayoutViewLongClick
+ )
+ );
+
+ setDragDataToInflatedView (view);
}
-
- private boolean onLayoutViewLongClick(@NonNull IView view) {
- final var shadow = new WidgetDragShadowBuilder(view.asView());
- final var item = new ClipData.Item(DesignerActivity.DRAGGING_WIDGET_TAG);
- final var clip = new ClipData(DesignerActivity.DRAGGING_WIDGET_TAG, new String[] {DesignerActivity.DRAGGING_WIDGET_MIME}, item);
- final var data = view.getExtraData();
-
- view.asView().startDragAndDrop(clip, shadow, data, 0);
-
+
+ private boolean onLayoutViewLongClick (@NonNull IView view) {
+ final var shadow = new WidgetDragShadowBuilder (view.asView ());
+ final var item = new ClipData.Item (DesignerActivity.DRAGGING_WIDGET_TAG);
+ final var clip = new ClipData (DesignerActivity.DRAGGING_WIDGET_TAG, new String[]{DesignerActivity.DRAGGING_WIDGET_MIME}, item);
+ final var data = view.getExtraData ();
+
+ view.asView ().startDragAndDrop (clip, shadow, data, 0);
+
return true;
}
-
- private void onLayoutViewClick(IView view) {
-
+
+ private void onLayoutViewClick (@NonNull IView view) {
+ view.asView ().setSelected (!view.asView ().isSelected ());
+ if (view.asView ().isSelected ()) {
+ selectView (view);
+ } else {
+ deselectView (view);
+ }
}
-
- private void setupWidgets() {
- final var android = new UIWidgetGroup(getString(R.string.widget_group_android));
- android.addChild(new UIWidget(getString(R.string.widget_button), R.drawable.ic_widget_button, Button.class));
- android.addChild(new UIWidget(getString(R.string.widget_checkbox), R.drawable.ic_widget_checkbox, CheckBox.class));
- android.addChild(new UIWidget(getString(R.string.widget_checked_textview), R.drawable.ic_widget_checked_textview, CheckedTextView.class));
- android.addChild(new UIWidget(getString(R.string.widget_edittext), R.drawable.ic_widget_edittext, EditText.class));
- android.addChild(new UIWidget(getString(R.string.widget_image_button), R.drawable.ic_widget_image, ImageButton.class));
- android.addChild(new UIWidget(getString(R.string.widget_image_view), R.drawable.ic_widget_image, ImageView.class));
- android.addChild(new UIWidget(getString(R.string.widget_progressbar), R.drawable.ic_widget_progress, ProgressBar.class));
- android.addChild(new UIWidget(getString(R.string.widget_radio_button), R.drawable.ic_widget_radio_button, RadioButton.class));
- android.addChild(new UIWidget(getString(R.string.widget_seekbar), R.drawable.ic_widget_seekbar, SeekBar.class));
- android.addChild(new UIWidget(getString(R.string.widget_spinner), R.drawable.ic_widget_spinner, Spinner.class));
- android.addChild(new UIWidget(getString(R.string.widget_textview), R.drawable.ic_widget_textview, TextView.class));
-
+
+ private void selectView (@NonNull IView view) {
+
+ deselectView (this.selectedView);
+
+ final var v = view.asView ();
+ v.setForeground (this.selectedViewForeground);
+ this.selectedView = view;
+ }
+
+ private void deselectView (IView view) {
+ if (view != null) {
+ view.asView ().setForeground (null);
+ }
+
+ this.selectedView = null;
+ }
+
+ private void setupWidgets () {
+ final var android = new UIWidgetGroup (getString (R.string.widget_group_android));
+ android.addChild (new UIWidget (getString (R.string.widget_button), R.drawable.ic_widget_button, Button.class));
+ android.addChild (new UIWidget (getString (R.string.widget_checkbox), R.drawable.ic_widget_checkbox, CheckBox.class));
+ android.addChild (new UIWidget (getString (R.string.widget_checked_textview), R.drawable.ic_widget_checked_textview, CheckedTextView.class));
+ android.addChild (new UIWidget (getString (R.string.widget_edittext), R.drawable.ic_widget_edittext, EditText.class));
+ android.addChild (new UIWidget (getString (R.string.widget_image_button), R.drawable.ic_widget_image, ImageButton.class));
+ android.addChild (new UIWidget (getString (R.string.widget_image_view), R.drawable.ic_widget_image, ImageView.class));
+ android.addChild (new UIWidget (getString (R.string.widget_progressbar), R.drawable.ic_widget_progress, ProgressBar.class));
+ android.addChild (new UIWidget (getString (R.string.widget_radio_button), R.drawable.ic_widget_radio_button, RadioButton.class));
+ android.addChild (new UIWidget (getString (R.string.widget_seekbar), R.drawable.ic_widget_seekbar, SeekBar.class));
+ android.addChild (new UIWidget (getString (R.string.widget_spinner), R.drawable.ic_widget_spinner, Spinner.class));
+ android.addChild (new UIWidget (getString (R.string.widget_textview), R.drawable.ic_widget_textview, TextView.class));
+
// IMPORTANT: Do not select any other groups here
- android.setSelected(true);
- this.selectedGroup = android;
- widgetGroups.add(android);
-
- final var layouts = new UIWidgetGroup(getString(R.string.widget_group_layouts));
- layouts.addChild(new UIWidget(getString(R.string.layout_linear), R.drawable.ic_widget_linear, LinearLayout.class));
- layouts.addChild(new UIWidget(getString(R.string.layout_relative), R.drawable.ic_widget_relative, RelativeLayout.class));
-
- widgetGroups.add(layouts);
-
- final var adapter = new WidgetGroupItemAdapter(widgetGroups, this::showWidgetGroup);
- mBinding.groupItems.setAdapter(adapter);
-
- showWidgetGroup(android);
+ android.setSelected (true);
+ this.checkedWidgetCategory = android;
+ widgetGroups.add (android);
+
+ final var layouts = new UIWidgetGroup (getString (R.string.widget_group_layouts));
+ layouts.addChild (new UIWidget (getString (R.string.layout_linear), R.drawable.ic_widget_linear, LinearLayout.class));
+ layouts.addChild (new UIWidget (getString (R.string.layout_relative), R.drawable.ic_widget_relative, RelativeLayout.class));
+
+ widgetGroups.add (layouts);
+
+ final var adapter = new WidgetGroupItemAdapter (widgetGroups, this::showWidgetGroup);
+ mBinding.groupItems.setAdapter (adapter);
+
+ showWidgetGroup (android);
}
-
+
@SuppressLint("NotifyDataSetChanged")
private void showWidgetGroup (@NonNull UIWidgetGroup group) {
- final var children = group.getChildren();
- this.mBinding.widgetItems.setAdapter(new WidgetItemAdapter(children, this));
- this.selectedGroup.setSelected(false);
- this.selectedGroup = group;
- this.selectedGroup.setSelected(true);
-
- TransitionManager.beginDelayedTransition(mBinding.navigation,
- new TransitionSet()
- .addTransition(new Slide(Gravity.END))
- .addTransition(new Fade())
+ final var children = group.getChildren ();
+ this.mBinding.widgetItems.setAdapter (new WidgetItemAdapter (children, this));
+ this.checkedWidgetCategory.setSelected (false);
+ this.checkedWidgetCategory = group;
+ this.checkedWidgetCategory.setSelected (true);
+
+ TransitionManager.beginDelayedTransition (mBinding.navigation,
+ new TransitionSet ()
+ .addTransition (new Slide (Gravity.END))
+ .addTransition (new Fade ())
);
-
+
//noinspection ConstantConditions
- this.mBinding.groupItems.getAdapter().notifyDataSetChanged();
+ this.mBinding.groupItems.getAdapter ().notifyDataSetChanged ();
}
-
+
@NonNull
@Contract(pure = true)
- private ILayoutInflater.ContextProvider newContextProvider() {
+ private ILayoutInflater.ContextProvider newContextProvider () {
return () -> DesignerActivity.this;
}
@NonNull
private TextView createErrorText (Throwable th) {
final TextView error = new TextView (this);
- error.setText(ThrowableUtils.getFullStackTrace(th));
- error.setLayoutParams(new ViewGroup.LayoutParams (-2, -2));
- error.setTextColor(ContextCompat.getColor(this, android.R.color.black));
- error.setBackgroundColor(ContextCompat.getColor(this, android.R.color.white));
+ error.setText (ThrowableUtils.getFullStackTrace (th));
+ error.setLayoutParams (new ViewGroup.LayoutParams (-2, -2));
+ error.setTextColor (ContextCompat.getColor (this, android.R.color.black));
+ error.setBackgroundColor (ContextCompat.getColor (this, android.R.color.white));
return error;
}
-
+
@Override
- public void onDragStarted(View view) {
- mBinding.getRoot().closeDrawer(GravityCompat.START);
+ public void onDragStarted (View view) {
+ mBinding.getRoot ().closeDrawer (GravityCompat.START);
}
-
+
@NonNull
@Contract("_ -> new")
private View.OnDragListener getOnDragListener (IViewGroup group) {
// WidgetDragListener cannot be reused
// This is because they keep a reference to the view group in which dragged view will be added
- return new WidgetDragListener(
- this,
- group,
- this::onDropWidget);
+ return new WidgetDragListener (
+ this,
+ group,
+ this::setupInflatedView);
}
-
+
private void setDragDataToInflatedView (@NonNull IView view) {
- view.setExtraData(new WidgetDragData(true, view, null));
+ view.setExtraData (new WidgetDragData (true, view, null));
+ }
+
+ @NonNull
+ private Drawable generateSelectedViewForeground () {
+ final var drawable = new GradientDrawable ();
+ drawable.setShape (GradientDrawable.RECTANGLE);
+ drawable.setColor (ContextCompat.getColor (this, R.color.fg_selected_ui_view));
+ drawable.setCornerRadius (SizeUtils.dp2px (3));
+ return drawable;
}
}
diff --git a/app/src/main/java/com/itsaky/androidide/language/xml/completion/XMLCompletionService.java b/app/src/main/java/com/itsaky/androidide/language/xml/completion/XMLCompletionService.java
index daa31cdc1..c1970ded9 100755
--- a/app/src/main/java/com/itsaky/androidide/language/xml/completion/XMLCompletionService.java
+++ b/app/src/main/java/com/itsaky/androidide/language/xml/completion/XMLCompletionService.java
@@ -113,13 +113,13 @@ public class XMLCompletionService {
private String createAttributeInsertText(Attr attr) {
StringBuilder xml = new StringBuilder();
- xml.append(attr.prefix);
+ xml.append(attr.namespace);
xml.append(":");
xml.append(attr.name);
xml.append("=");
xml.append("\"");
- if(attr.prefix.equals("android")
+ if(attr.namespace.equals("android")
&& attr.name.equals("id")) {
xml.append("@+id/");
}
diff --git a/app/src/main/java/com/itsaky/androidide/ui/WidgetDragListener.java b/app/src/main/java/com/itsaky/androidide/ui/WidgetDragListener.java
index 5fccffbce..196998e77 100755
--- a/app/src/main/java/com/itsaky/androidide/ui/WidgetDragListener.java
+++ b/app/src/main/java/com/itsaky/androidide/ui/WidgetDragListener.java
@@ -94,6 +94,9 @@ public class WidgetDragListener implements View.OnDragListener {
var view = dragData.isAlreadyInflated ? dragData.alreadyInflatedView : createView(dragData.newDragData);
applyBasicAttributes(view);
+ // Remove from parent if it has a parent
+ view.removeFromParent();
+
this.viewGroup.addView(view);
if (this.addedListener != null) {
diff --git a/app/src/main/java/com/itsaky/androidide/ui/WidgetTouchListener.java b/app/src/main/java/com/itsaky/androidide/ui/WidgetTouchListener.java
new file mode 100644
index 000000000..a04e6613f
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/ui/WidgetTouchListener.java
@@ -0,0 +1,109 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.ui;
+
+import android.annotation.SuppressLint;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.itsaky.layoutinflater.IView;
+
+/**
+ * A touch listener for handling touch events of a view in the
+ * UI Designer.
+ *
+ * Some of the views have no effect of setting a click listener
+ * or a long click listener (e.g. SeekBar). In order to fix this,
+ * we use {@link android.view.GestureDetector} and manually handle
+ * click events.
+ *
+ * @author Akash Yadav
+ */
+public class WidgetTouchListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
+
+ private final GestureDetector mGestureDetector;
+ private final OnClickListener clickListener;
+ private final OnLongClickListener longClickListener;
+
+ private final IView mView;
+
+ public WidgetTouchListener (@NonNull IView view, @Nullable OnClickListener clickListener, @Nullable OnLongClickListener longClickListener) {
+ this.mView = view;
+ this.mGestureDetector = new GestureDetector (view.asView().getContext(), this);
+ this.clickListener = clickListener;
+ this.longClickListener = longClickListener;
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouch(View view, @NonNull MotionEvent motionEvent) {
+ switch (motionEvent.getAction ()) {
+ case MotionEvent.ACTION_DOWN :
+ break;
+ }
+ return mGestureDetector.onTouchEvent(motionEvent);
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true; // We would like to get notified about further events.
+ }
+
+ // TODO Should we use onSingleTapConfirmed instead?
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ if (this.clickListener != null) {
+ this.clickListener.onClick(this.mView);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ if (this.longClickListener != null &&
+ this.longClickListener.onLongClick(this.mView)) {
+ this.mView.asView().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ }
+
+ /**
+ * A listener which is invoked when we detect a single
+ * click event.
+ *
+ * @author Akash Yadav
+ */
+ public interface OnClickListener {
+ void onClick (IView view);
+ }
+
+ /**
+ * A listener which is invoked when we detect a long
+ * click event.
+ *
+ * @author Akash Yadav
+ */
+ public interface OnLongClickListener {
+ boolean onLongClick (IView view);
+ }
+}
diff --git a/app/src/main/java/com/itsaky/androidide/utils/ThemeEnforcement.java b/app/src/main/java/com/itsaky/androidide/utils/ThemeEnforcement.java
new file mode 100644
index 000000000..e62954c13
--- /dev/null
+++ b/app/src/main/java/com/itsaky/androidide/utils/ThemeEnforcement.java
@@ -0,0 +1,246 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.utils;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import androidx.annotation.StyleableRes;
+import androidx.appcompat.widget.TintTypedArray;
+
+import com.itsaky.androidide.R;
+
+/**
+ * Utility methods to check Theme compatibility with components.
+ *
+ * This class is copied from the Material Components library without any modifications.
+ * @see ThemeEnforcement
+ */
+public final class ThemeEnforcement {
+
+ private static final int[] APPCOMPAT_CHECK_ATTRS = {R.attr.colorPrimary};
+ private static final String APPCOMPAT_THEME_NAME = "Theme.AppCompat";
+
+ private static final int[] MATERIAL_CHECK_ATTRS = {R.attr.colorPrimaryVariant};
+ private static final String MATERIAL_THEME_NAME = "Theme.MaterialComponents";
+
+ private ThemeEnforcement() {}
+
+ /**
+ * Safely retrieve styled attribute information in this Context's theme, after checking whether
+ * the theme is compatible with the component's given style.
+ *
+ * Set a component's {@code enforceMaterialTheme} attribute to true to ensure that
+ * the Context's theme inherits from {@code Theme.MaterialComponents}. For example, you'll want to
+ * do this if the component uses a new attribute defined in
+ * Theme.MaterialComponents like {@code colorSecondary}.
+ *
+ *
If the {@code enforceTextAppearance} attribute is set to true and
+ * textAppearanceResIndices parameter is specified and has non-negative values, this will also
+ * check that a valid TextAppearance is set on this component for the text appearance resources
+ * passed in.
+ */
+ @NonNull
+ public static TypedArray obtainStyledAttributes(
+ @NonNull Context context,
+ AttributeSet set,
+ @NonNull @StyleableRes int[] attrs,
+ @AttrRes int defStyleAttr,
+ @StyleRes int defStyleRes,
+ @StyleableRes int... textAppearanceResIndices) {
+
+ // First, check for a compatible theme.
+ checkCompatibleTheme(context, set, defStyleAttr, defStyleRes);
+
+ // Then, check that a textAppearance is set if enforceTextAppearance attribute is true
+ checkTextAppearance(context, set, attrs, defStyleAttr, defStyleRes, textAppearanceResIndices);
+
+ // Then, safely retrieve the styled attribute information.
+ return context.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Safely retrieve styled attribute information in this Context's theme using {@link
+ * androidx.appcompat.widget.TintTypedArray}, after checking whether the theme is compatible with
+ * the component's given style.
+ *
+ *
Set a component's {@code enforceMaterialTheme} attribute to true to ensure that
+ * the Context's theme inherits from {@code Theme.MaterialComponents}. For example, you'll want to
+ * do this if the component uses a new attribute defined in Theme.MaterialComponents
+ * like {@code colorSecondary}.
+ *
+ *
New components should prefer to use {@link #obtainStyledAttributes(Context, AttributeSet,
+ * int[], int, int, int...)}, and use {@link
+ * com.google.android.material.resources.MaterialResources} as a replacement for the functionality
+ * in {@link androidx.appcompat.widget.TintTypedArray}.
+ *
+ *
If the {@code enforceTextAppearance} attribute is set to true and
+ * textAppearanceResIndices parameter is specified and has non-negative values, this will also
+ * check that a valid TextAppearance is set on this component for the text appearance resources
+ * passed in.
+ */
+ public static TintTypedArray obtainTintedStyledAttributes(
+ @NonNull Context context,
+ AttributeSet set,
+ @NonNull @StyleableRes int[] attrs,
+ @AttrRes int defStyleAttr,
+ @StyleRes int defStyleRes,
+ @StyleableRes int... textAppearanceResIndices) {
+
+ // First, check for a compatible theme.
+ checkCompatibleTheme(context, set, defStyleAttr, defStyleRes);
+
+ // Then, check that a textAppearance is set if enforceTextAppearance attribute is true
+ checkTextAppearance(context, set, attrs, defStyleAttr, defStyleRes, textAppearanceResIndices);
+
+ // Then, safely retrieve the styled attribute information.
+ return TintTypedArray.obtainStyledAttributes(context, set, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private static void checkCompatibleTheme(
+ @NonNull Context context,
+ AttributeSet set,
+ @AttrRes int defStyleAttr,
+ @StyleRes int defStyleRes) {
+ TypedArray a =
+ context.obtainStyledAttributes(
+ set, R.styleable.ThemeEnforcement, defStyleAttr, defStyleRes);
+ boolean enforceMaterialTheme =
+ a.getBoolean(R.styleable.ThemeEnforcement_enforceMaterialTheme, false);
+ a.recycle();
+
+ if (enforceMaterialTheme) {
+ TypedValue isMaterialTheme = new TypedValue();
+ boolean resolvedValue =
+ context.getTheme().resolveAttribute(R.attr.isMaterialTheme, isMaterialTheme, true);
+
+ if (!resolvedValue
+ || (isMaterialTheme.type == TypedValue.TYPE_INT_BOOLEAN && isMaterialTheme.data == 0)) {
+ // If we were unable to resolve isMaterialTheme boolean attribute, or isMaterialTheme is
+ // false, check for Material Theme color attributes
+ checkMaterialTheme(context);
+ }
+ }
+ checkAppCompatTheme(context);
+ }
+
+ private static void checkTextAppearance(
+ @NonNull Context context,
+ AttributeSet set,
+ @NonNull @StyleableRes int[] attrs,
+ @AttrRes int defStyleAttr,
+ @StyleRes int defStyleRes,
+ @Nullable @StyleableRes int... textAppearanceResIndices) {
+ TypedArray themeEnforcementAttrs =
+ context.obtainStyledAttributes(
+ set, R.styleable.ThemeEnforcement, defStyleAttr, defStyleRes);
+ boolean enforceTextAppearance =
+ themeEnforcementAttrs.getBoolean(R.styleable.ThemeEnforcement_enforceTextAppearance, false);
+
+ if (!enforceTextAppearance) {
+ themeEnforcementAttrs.recycle();
+ return;
+ }
+
+ boolean validTextAppearance;
+
+ if (textAppearanceResIndices == null || textAppearanceResIndices.length == 0) {
+ // No custom TextAppearance attributes passed in, check android:textAppearance
+ validTextAppearance =
+ themeEnforcementAttrs.getResourceId(
+ R.styleable.ThemeEnforcement_android_textAppearance, -1)
+ != -1;
+ } else {
+ // Check custom TextAppearances are valid
+ validTextAppearance =
+ isCustomTextAppearanceValid(
+ context, set, attrs, defStyleAttr, defStyleRes, textAppearanceResIndices);
+ }
+
+ themeEnforcementAttrs.recycle();
+
+ if (!validTextAppearance) {
+ throw new IllegalArgumentException(
+ "This component requires that you specify a valid TextAppearance attribute. Update your "
+ + "app theme to inherit from Theme.MaterialComponents (or a descendant).");
+ }
+ }
+
+ private static boolean isCustomTextAppearanceValid(
+ @NonNull Context context,
+ AttributeSet set,
+ @NonNull @StyleableRes int[] attrs,
+ @AttrRes int defStyleAttr,
+ @StyleRes int defStyleRes,
+ @NonNull @StyleableRes int... textAppearanceResIndices) {
+ TypedArray componentAttrs =
+ context.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes);
+ for (int customTextAppearanceIndex : textAppearanceResIndices) {
+ if (componentAttrs.getResourceId(customTextAppearanceIndex, -1) == -1) {
+ componentAttrs.recycle();
+ return false;
+ }
+ }
+ componentAttrs.recycle();
+ return true;
+ }
+
+ public static void checkAppCompatTheme(@NonNull Context context) {
+ checkTheme(context, APPCOMPAT_CHECK_ATTRS, APPCOMPAT_THEME_NAME);
+ }
+
+ public static void checkMaterialTheme(@NonNull Context context) {
+ checkTheme(context, MATERIAL_CHECK_ATTRS, MATERIAL_THEME_NAME);
+ }
+
+ public static boolean isAppCompatTheme(@NonNull Context context) {
+ return isTheme(context, APPCOMPAT_CHECK_ATTRS);
+ }
+
+ public static boolean isMaterialTheme(@NonNull Context context) {
+ return isTheme(context, MATERIAL_CHECK_ATTRS);
+ }
+
+ private static boolean isTheme(@NonNull Context context, @NonNull int[] themeAttributes) {
+ TypedArray a = context.obtainStyledAttributes(themeAttributes);
+ for (int i = 0; i < themeAttributes.length; i++) {
+ if (!a.hasValue(i)) {
+ a.recycle();
+ return false;
+ }
+ }
+ a.recycle();
+ return true;
+ }
+
+ private static void checkTheme(
+ @NonNull Context context, @NonNull int[] themeAttributes, String themeName) {
+ if (!isTheme(context, themeAttributes)) {
+ throw new IllegalArgumentException(
+ "The style on this component requires your app theme to be "
+ + themeName
+ + " (or a descendant).");
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_ui_view_pressed.xml b/app/src/main/res/drawable/bg_ui_view_pressed.xml
new file mode 100644
index 000000000..949104d81
--- /dev/null
+++ b/app/src/main/res/drawable/bg_ui_view_pressed.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_designer.xml b/app/src/main/res/layout/activity_designer.xml
index ecdd503b3..93d65eb4e 100755
--- a/app/src/main/res/layout/activity_designer.xml
+++ b/app/src/main/res/layout/activity_designer.xml
@@ -5,7 +5,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
- tools:context=".DesignerActivity">
+ tools:context=".DesignerActivity"
+ tools:openDrawer="start">
#4CAF50
#212121
#70000000
+ #9003A9F4
@android:color/tertiary_text_light
#eeeeee
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0f9d514c4..8c88a88d5 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -55,10 +55,6 @@
Uncomment line
Diagnostics
-
- Unable to preview layout!
- Preview Layout
-
Installation failed
Installation process terminated with exit code %1$d. Make sure you downloaded the build tools from original sources.\n\nYou could try to clear data of AndroidIDE and install again.
@@ -290,6 +286,10 @@
Unable to start Language Server for: %s
Failed to write to Socket stream
Language server process unexpectedly exited.
+
+
+ Unable to preview layout!
+ Preview Layout
Main UI designer content
TextView
Button
@@ -306,4 +306,17 @@
Layouts
LinearLayout
RelativeLayout
+ Click on the attribute items to edit.
+ =
+ \'%s\' namespace
+ Unknown package name
+ Select a view to edit its attributes.
+ No attributes has been added for this view.\nClick on the \'+\' button to add.
+ Enter new value for attribute:
+ New value
+ wrap_content
+ match_parent
+ Fixed
+ Enter dimension value
+
diff --git a/layout-inflater/src/main/java/com/itsaky/layoutinflater/IAttribute.java b/layout-inflater/src/main/java/com/itsaky/layoutinflater/IAttribute.java
index ff4480759..b0b738491 100755
--- a/layout-inflater/src/main/java/com/itsaky/layoutinflater/IAttribute.java
+++ b/layout-inflater/src/main/java/com/itsaky/layoutinflater/IAttribute.java
@@ -21,6 +21,8 @@ package com.itsaky.layoutinflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+
+import java.util.Comparator;
import java.util.Set;
/**
@@ -43,7 +45,61 @@ public interface IAttribute {
@NonNull
String getValue ();
-
- @NonNull
- Set getFixedValues ();
+
+ public static final Comparator COMPARATOR = (first, second) -> {
+ var result = compareNull(first, second);
+ if (result != IAttribute.DO_COMPARE) {
+ return result;
+ }
+
+ result = compareNull(first.getNamespace(), second.getNamespace());
+ if (result != IAttribute.DO_COMPARE) {
+ return result;
+ }
+
+ // If we reach here, then it means that the attributes and their namespaces are not null
+ // First, the attributes must be compared by their namespaces
+
+ final var firstNs = first.getNamespace();
+ final var secondNs = second.getNamespace();
+
+ if (firstNs.equals(secondNs)) {
+
+ // Compare by attribute names if the namespaces are same
+ return first.getAttributeName().compareTo(second.getAttributeName());
+ }
+
+ // xmlns declarations must be on top
+ if (firstNs.equals("xmlns") && !secondNs.equals("xmlns")) {
+ return -1;
+ }
+
+ if (!firstNs.equals("xmlns") && secondNs.equals("xmlns")) {
+ return 1;
+ }
+
+ return 0;
+ };
+
+ /**
+ * Returned by {@link #compareNull(Object, Object)} to indicate
+ * that the comparison must be done further.
+ */
+ static final int DO_COMPARE = 2;
+
+ private static int compareNull (Object first, Object second) {
+ if (first == null && second == null) {
+ return 0;
+ }
+
+ if (first != null && second == null) {
+ return -1;
+ }
+
+ if (first == null && second != null) {
+ return 1;
+ }
+
+ return DO_COMPARE;
+ }
}
diff --git a/layout-inflater/src/main/java/com/itsaky/layoutinflater/IView.java b/layout-inflater/src/main/java/com/itsaky/layoutinflater/IView.java
index 1f975d59d..dc088275f 100755
--- a/layout-inflater/src/main/java/com/itsaky/layoutinflater/IView.java
+++ b/layout-inflater/src/main/java/com/itsaky/layoutinflater/IView.java
@@ -21,7 +21,7 @@ import android.view.View;
import androidx.annotation.Nullable;
-import com.itsaky.layoutinflater.IResourceFinder;
+import java.util.Set;
/**
* Represents a view in the UI Designer
@@ -65,13 +65,34 @@ public interface IView {
* @param index Index of the attribute to remove
*/
void removeAttributeAt (int index);
-
+
+ /**
+ * Get the attributes of this view.
+ * @return The attributes.
+ */
+ Set getAttributes ();
+
+ /**
+ * Get the attributes of this view as an array.
+ * @return The attributes.
+ * @see #getAttributes()
+ */
+ IAttribute[] getAttrArray ();
+
/**
* Register this attribute adapter
*
* @param adapter The adapter to register
*/
void registerAttributeAdapter (IAttributeAdapter adapter);
+
+ /**
+ * Set the parent of this view.
+ * This is set by {@link IViewGroup} when {@link IViewGroup#addView(IView, int)}
+ * is called.
+ * @param newParent The new parent of this view.
+ */
+ void setParent (IViewGroup newParent);
/**
* Get the parent of this view
@@ -79,6 +100,14 @@ public interface IView {
* @return The parent of this view or {@code null} if this is the root view.
*/
IViewGroup getParent();
+
+ /**
+ * Remove this view from its parent. This should take care of removing
+ * the actual {@link View} from its parent.
+ *
+ * @return {@code true} if the view was successfully removed, {@code false} otherwise.
+ */
+ boolean removeFromParent ();
/**
* Is this view a placeholder for another view?
diff --git a/layout-inflater/src/main/java/com/itsaky/layoutinflater/adapters/android/view/ViewAttrAdapter.java b/layout-inflater/src/main/java/com/itsaky/layoutinflater/adapters/android/view/ViewAttrAdapter.java
index 08302dc23..dd48bd3ac 100755
--- a/layout-inflater/src/main/java/com/itsaky/layoutinflater/adapters/android/view/ViewAttrAdapter.java
+++ b/layout-inflater/src/main/java/com/itsaky/layoutinflater/adapters/android/view/ViewAttrAdapter.java
@@ -31,13 +31,17 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
+
+import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
+
import com.itsaky.androidide.utils.Logger;
import com.itsaky.layoutinflater.IAttribute;
import com.itsaky.layoutinflater.IAttributeAdapter;
import com.itsaky.layoutinflater.IDTable;
import com.itsaky.layoutinflater.IResourceFinder;
+
import java.io.File;
import java.lang.reflect.Field;
import java.util.regex.Pattern;
@@ -51,7 +55,7 @@ import java.util.regex.Pattern;
*/
public class ViewAttrAdapter implements IAttributeAdapter {
- private final Pattern HEX_COLOR = Pattern.compile("#[a-fA-F0-9]{6,8}");
+ private static final Pattern HEX_COLOR = Pattern.compile("#[a-fA-F0-9]{6,8}");
protected static final Logger LOG = Logger.instance ("BaseViewAttrAdapter");
public static final int ENUM_NOT_FOUND = -4116;
@@ -192,7 +196,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
view.setScaleY(parseFloat(value));
break;
case "scrollX" :
- view.setScrollY((int) parseFloat(value));
+ view.setScrollX((int) parseFloat(value));
break;
case "scrollY" :
view.setScrollY((int) parseFloat(value));
@@ -234,11 +238,11 @@ public class ViewAttrAdapter implements IAttributeAdapter {
// ----- Handle attributes related to parent ------
if (!handled && params instanceof LinearLayout.LayoutParams) {
- handled = handleLinearLayoutParams ((LinearLayout.LayoutParams) params, view, name, value, dm, resFinder);
+ handled = handleLinearLayoutParams ((LinearLayout.LayoutParams) params, name, value, dm, resFinder);
}
if (!handled && params instanceof RelativeLayout.LayoutParams) {
- handled = handleRelativeLayoutParams ((RelativeLayout.LayoutParams) params, view, name, value, resFinder);
+ handled = handleRelativeLayoutParams ((RelativeLayout.LayoutParams) params, name, value, resFinder);
}
if (!handled && params instanceof ViewGroup.MarginLayoutParams) {
@@ -249,8 +253,8 @@ public class ViewAttrAdapter implements IAttributeAdapter {
return handled;
}
-
- protected int parseId(String value) {
+
+ protected int parseId(@NonNull String value) {
if (value.startsWith("@id/")) {
final String name = value.substring("@id/".length());
return IDTable.getId(name);
@@ -260,8 +264,8 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
return View.NO_ID;
}
-
- protected boolean handleRelativeLayoutParams(RelativeLayout.LayoutParams params, View view, String name, String value, IResourceFinder resFinder) {
+
+ protected boolean handleRelativeLayoutParams (RelativeLayout.LayoutParams params, @NonNull String name, String value, IResourceFinder resFinder) {
boolean handled = true;
switch (name) {
case "layout_above" :
@@ -347,7 +351,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
}
- protected boolean handleLinearLayoutParams(LinearLayout.LayoutParams params, View view, String name, String value, DisplayMetrics dm, IResourceFinder resFinder) {
+ protected boolean handleLinearLayoutParams (LinearLayout.LayoutParams params, @NonNull String name, String value, DisplayMetrics dm, IResourceFinder resFinder) {
boolean handled = true;
switch (name) {
case "layout_gravity" :
@@ -362,8 +366,8 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
return handled;
}
-
- protected boolean handleMarginParams(ViewGroup.MarginLayoutParams params, String name, String value, DisplayMetrics dm, IResourceFinder resFinder) {
+
+ protected boolean handleMarginParams(ViewGroup.MarginLayoutParams params, @NonNull String name, String value, DisplayMetrics dm, IResourceFinder resFinder) {
boolean handled = true;
switch (name) {
case "layout_margin" :
@@ -408,8 +412,8 @@ public class ViewAttrAdapter implements IAttributeAdapter {
return defaultVal;
}
}
-
- protected int parseDrawingCacheQuality(String value) {
+
+ protected int parseDrawingCacheQuality(@NonNull String value) {
switch (value) {
case "high" :
return View.DRAWING_CACHE_QUALITY_HIGH;
@@ -420,8 +424,8 @@ public class ViewAttrAdapter implements IAttributeAdapter {
return View.DRAWING_CACHE_QUALITY_AUTO;
}
}
-
- protected int parseVisibility(String value) {
+
+ protected int parseVisibility(@NonNull String value) {
switch (value) {
case "gone" :
return View.GONE;
@@ -433,7 +437,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
}
- protected int parseGravity (String value) {
+ protected int parseGravity (@NonNull String value) {
final String[] splits = value.split(Pattern.quote("|"));
int result = -1;
for (String split : splits) {
@@ -446,8 +450,8 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
return result;
}
-
- protected int gravityFor(String gravity) {
+
+ protected int gravityFor(@NonNull String gravity) {
switch (gravity) {
case "center" :
return Gravity.CENTER;
@@ -472,7 +476,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
}
- protected int parseTextAlignment (String value) {
+ protected int parseTextAlignment (@NonNull String value) {
switch (value) {
case "center" :
return View.TEXT_ALIGNMENT_CENTER;
@@ -492,7 +496,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
}
- protected int parseTextDirection (String value) {
+ protected int parseTextDirection (@NonNull String value) {
switch (value) {
case "anyRtl" :
return View.TEXT_DIRECTION_ANY_RTL;
@@ -514,14 +518,14 @@ public class ViewAttrAdapter implements IAttributeAdapter {
}
}
- protected String parseString(String value, IResourceFinder resFinder) {
+ protected String parseString(@NonNull String value, IResourceFinder resFinder) {
if (value.startsWith("@")) {
return parseString(resFinder.findString(value.substring("@string/".length())), resFinder);
}
return value;
}
- protected boolean parseBoolean(String value, IResourceFinder resFinder) {
+ protected boolean parseBoolean(@NonNull String value, IResourceFinder resFinder) {
if (!value.startsWith("@")) {
if ("true".equals(value)) {
return true;
@@ -534,7 +538,7 @@ public class ViewAttrAdapter implements IAttributeAdapter {
return false;
}
- protected PorterDuff.Mode parsePorterDuffMode(String mode) {
+ protected PorterDuff.Mode parsePorterDuffMode(@NonNull String mode) {
switch (mode) {
case "add" :
return PorterDuff.Mode.ADD;
diff --git a/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/BaseView.java b/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/BaseView.java
index 017cebdf4..63966c168 100755
--- a/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/BaseView.java
+++ b/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/BaseView.java
@@ -5,12 +5,12 @@
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see .
*
@@ -18,34 +18,34 @@
package com.itsaky.layoutinflater.impl;
import android.view.View;
-import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.itsaky.layoutinflater.IResourceFinder;
+import com.itsaky.androidide.utils.Logger;
import com.itsaky.layoutinflater.IAttribute;
import com.itsaky.layoutinflater.IAttributeAdapter;
+import com.itsaky.layoutinflater.IResourceFinder;
import com.itsaky.layoutinflater.IView;
import com.itsaky.layoutinflater.IViewGroup;
-import java.util.ArrayList;
+
import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
public abstract class BaseView implements IView {
-
- protected final Set attributes = new HashSet<>();
+
+ protected final Set attributes = new TreeSet(IAttribute.COMPARATOR);
protected final Set attrAdapters = new HashSet<>();
-
+
protected final String qualifiedName;
protected final View view;
protected IViewGroup parent;
-
protected Object stored;
+ private boolean isPlaceholder;
- private boolean isPlaceholder = false;
+ protected final Logger LOG = Logger.instance(getClass().getSimpleName());
public BaseView(String qualifiedName, View view) {
this(qualifiedName, view, false);
@@ -56,20 +56,21 @@ public abstract class BaseView implements IView {
this.view = view;
this.isPlaceholder = isPlaceholder;
}
-
+
+ @Override
public void setParent (IViewGroup parent) {
this.parent = parent;
}
-
+
public void setPlaceholder (boolean placeholder) {
this.isPlaceholder = placeholder;
}
-
+
@Override
public boolean isPlaceholder() {
return isPlaceholder;
}
-
+
@Override
public View asView() {
return view;
@@ -79,15 +80,26 @@ public abstract class BaseView implements IView {
public IViewGroup getParent() {
return parent;
}
-
+
+ @Override
+ public boolean removeFromParent() {
+ if (getParent() != null) {
+ getParent().removeView(this);
+ return true;
+ }
+
+ LOG.info("View does not have a parent. Cannot remove.");
+ return false;
+ }
+
@Override
public void addAttribute(IAttribute attr, IResourceFinder resFinder) {
if (attr == null || this.attributes.contains(attr)) {
return;
}
-
+
this.attributes.add(attr);
-
+
for (IAttributeAdapter adapter : attrAdapters) {
if (adapter.isApplicableTo(asView())
&& adapter.apply(attr, asView(), resFinder)) {
@@ -95,7 +107,7 @@ public abstract class BaseView implements IView {
}
}
}
-
+
@Override
public void removeAttribute(IAttribute attr) {
this.attributes.remove(attr);
@@ -106,15 +118,25 @@ public abstract class BaseView implements IView {
this.attributes.remove(index);
}
+ @Override
+ public Set getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public IAttribute[] getAttrArray() {
+ return getAttributes().toArray(new IAttribute[0]);
+ }
+
@Override
public void registerAttributeAdapter(IAttributeAdapter adapter) {
if (adapter == null) {
return;
}
-
+
this.attrAdapters.add(adapter);
}
-
+
protected Set getAttributeAdapters () {
return attrAdapters;
}
diff --git a/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/UiAttribute.java b/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/UiAttribute.java
index 666882ec9..6def7fa91 100755
--- a/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/UiAttribute.java
+++ b/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/UiAttribute.java
@@ -17,11 +17,8 @@
**************************************************************************************/
package com.itsaky.layoutinflater.impl;
-import com.itsaky.layoutinflater.AttrType;
import com.itsaky.layoutinflater.IAttribute;
-import com.itsaky.layoutinflater.IFixedValue;
-import java.util.HashSet;
-import java.util.Set;
+
import java.util.Objects;
public class UiAttribute implements IAttribute {
@@ -29,17 +26,11 @@ public class UiAttribute implements IAttribute {
private final String namespace;
private final String name;
private final String value;
- private final Set fixedValues;
- public UiAttribute(String namespace, String name, String value, Set fixedValues) {
+ public UiAttribute(String namespace, String name, String value) {
this.namespace = namespace;
this.name = name;
this.value = value;
- this.fixedValues = fixedValues;
- }
-
- public UiAttribute(String namespace, String name, String value) {
- this (namespace, name, value, new HashSet());
}
@Override
@@ -56,11 +47,6 @@ public class UiAttribute implements IAttribute {
public String getValue() {
return value;
}
-
- @Override
- public Set getFixedValues() {
- return fixedValues;
- }
@Override
public String toString() {
@@ -69,20 +55,21 @@ public class UiAttribute implements IAttribute {
" namespace = " + namespace + "\n" +
" name = " + name + "\n" +
" value = " + value + "\n" +
- " fixedValue = " + fixedValues + "\n" +
"]";
}
@Override
- public boolean equals(Object obj) {
- if (obj == null) {
- return false;
- }
- return this.hashCode() == obj.hashCode();
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UiAttribute that = (UiAttribute) o;
+ return Objects.equals(getNamespace(), that.getNamespace())
+ && Objects.equals(name, that.name)
+ && Objects.equals(getValue(), that.getValue());
}
@Override
public int hashCode() {
- return Objects.hash(namespace, name, value, fixedValues);
+ return Objects.hash(getNamespace(), name, getValue());
}
}
diff --git a/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/UiViewGroup.java b/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/UiViewGroup.java
index 6d062fe82..9761a5a17 100755
--- a/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/UiViewGroup.java
+++ b/layout-inflater/src/main/java/com/itsaky/layoutinflater/impl/UiViewGroup.java
@@ -18,6 +18,9 @@
package com.itsaky.layoutinflater.impl;
import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
import com.itsaky.layoutinflater.IView;
import java.util.ArrayList;
import java.util.List;
@@ -49,7 +52,8 @@ public class UiViewGroup extends BaseViewGroup {
super.addView(view, index);
this.viewGroup.addView(view.asView(), index);
this.children.add(index, view);
-
+
+ view.setParent(this);
onViewAdded(view);
}
@@ -65,7 +69,7 @@ public class UiViewGroup extends BaseViewGroup {
}
@Override
- public void removeView(IView view) {
+ public void removeView(@NonNull IView view) {
this.viewGroup.removeView(view.asView());
this.children.remove(view);
diff --git a/sdk-info/build.gradle b/sdk-info/build.gradle
index b3f6c7bce..5469e238b 100755
--- a/sdk-info/build.gradle
+++ b/sdk-info/build.gradle
@@ -27,4 +27,8 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation common.utilcode
implementation common.jsoup
+
+ implementation androidx.annotation
+
+ compileOnly project(":common")
}
diff --git a/sdk-info/src/main/java/com/itsaky/attrinfo/AttrInfo.java b/sdk-info/src/main/java/com/itsaky/attrinfo/AttrInfo.java
index 8d184acd4..4150ed56a 100755
--- a/sdk-info/src/main/java/com/itsaky/attrinfo/AttrInfo.java
+++ b/sdk-info/src/main/java/com/itsaky/attrinfo/AttrInfo.java
@@ -1,83 +1,127 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
- *
+ *
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see .
*
-**************************************************************************************/
+ **************************************************************************************/
package com.itsaky.attrinfo;
import android.content.Context;
import android.content.res.Resources;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.itsaky.androidide.utils.Logger;
import com.itsaky.attrinfo.models.Attr;
+
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
+
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
-import java.io.IOException;
public class AttrInfo {
- private final Map attrs;
+ private final Map attrs = new HashMap<> ();
- public AttrInfo(Context ctx) throws IOException {
- this.attrs = new HashMap<>();
- readAttributes(ctx.getResources());
+ private static final Logger LOG = Logger.instance ("AttrInfo");
+
+ public AttrInfo (@NonNull Context ctx) throws Exception {
+ readAttributes (ctx.getResources ());
}
- public Map getAttrs() {
+ @NonNull
+ public Map getAttrs () {
return this.attrs;
}
-
- private void readAttributes(Resources resources) throws IOException {
- final InputStream in = resources.openRawResource(com.itsaky.sdkinfo.R.raw.attrs);
+
+ @Nullable
+ public Attr getAttribute (String name) {
+ return this.attrs.get (name);
+ }
+
+ private void readAttributes (@NonNull Resources resources) throws Exception {
+ final InputStream in = resources.openRawResource (com.itsaky.sdkinfo.R.raw.attrs);
- if(in == null) return;
+ if (in == null) {
+ throw new IllegalStateException ("attrs.xml: Resource not found.");
+ }
- Document doc = Jsoup.parse(in, null, "file://android_res/");
- Elements attrs = doc.getElementsByTag("resources").first().getElementsByTag("attr");
- for(Element attr : attrs) {
- Attr a = new Attr(attr.attr("name"), true);
- if(a.name.contains(":")) {
- String[] split = a.name.split(":");
- a.prefix = split[0];
- a.name = split[1];
+ Document doc = Jsoup.parse (in, null, "file://android_res/");
+ Elements attributes = doc.getElementsByTag ("resources").first ().getElementsByTag ("attr");
+ for (Element attribute : attributes) {
+ final var attr = new Attr (attribute.attr ("name"), true);
+
+ if (attr.name.contains (":")) {
+ String[] split = attr.name.split (":");
+ attr.namespace = split[0];
+ attr.name = split[1];
}
- if(attr.hasAttr("format") && attr.attr("format").contains("boolean")) {
- a.possibleValues.add("true");
- a.possibleValues.add("false");
+
+ // If there is an implicit declaration od format attribute,
+ // then we parse its values and set the format bits accordingly
+ if (attribute.hasAttr ("format")) {
+ final var format = attribute.attr ("format");
+ attr.format = Attr.formatForName (format);
+
+ if (format.contains ("boolean")) {
+ attr.possibleValues.add ("true");
+ attr.possibleValues.add ("false");
+ }
}
- Elements enums = attr.getElementsByTag("enum");
- Elements flags = attr.getElementsByTag("flag");
- if(enums != null && enums.size() > 0) {
- for(Element e : enums)
- a.possibleValues.add(e.attr("name"));
+
+ // If there is no implicit declaration of format or
+ // this attribute has or children tags,
+ // then format will be set here along with the possible values
+ Elements enums = attribute.getElementsByTag ("enum");
+ Elements flags = attribute.getElementsByTag ("flag");
+ if (enums.size () > 0) {
+ for (Element e : enums) {
+ attr.possibleValues.add (e.attr ("name"));
+ }
+
+ attr.format |= Attr.ENUM;
}
- if(flags != null && flags.size() > 0) {
- for(Element e : flags)
- a.possibleValues.add(e.attr("name"));
+
+ if (flags.size () > 0) {
+ for (Element e : flags) {
+ attr.possibleValues.add (e.attr ("name"));
+ }
+
+ attr.format |= Attr.FLAG;
}
- if(this.attrs.containsKey(a.name)) {
- Attr b = this.attrs.get(a.name);
- if(b != null && b.hasPossibleValues())
- a.possibleValues.addAll(b.possibleValues);
+
+ if (this.attrs.containsKey (attr.name)) {
+ final var present = this.attrs.get (attr.name);
+ if (present != null) {
+ if (present.hasPossibleValues ()) {
+ attr.possibleValues.addAll (present.possibleValues);
+ }
+
+ if (attr.format == 0 && present.format != 0) {
+ attr.format = present.format;
+ }
+ }
}
- this.attrs.put(a.name, a);
+
+ this.attrs.put (attr.name, attr);
}
}
}
diff --git a/sdk-info/src/main/java/com/itsaky/attrinfo/models/Attr.java b/sdk-info/src/main/java/com/itsaky/attrinfo/models/Attr.java
index 39958539e..0242894fb 100755
--- a/sdk-info/src/main/java/com/itsaky/attrinfo/models/Attr.java
+++ b/sdk-info/src/main/java/com/itsaky/attrinfo/models/Attr.java
@@ -1,45 +1,146 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
- *
+ *
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see .
*
-**************************************************************************************/
+ **************************************************************************************/
package com.itsaky.attrinfo.models;
+import android.graphics.Color;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.itsaky.androidide.utils.Logger;
+
+import org.jetbrains.annotations.Contract;
+
+import java.util.Collection;
import java.util.HashSet;
+import java.util.Objects;
import java.util.Set;
+import java.util.regex.Pattern;
+/**
+ * Represents an xml attribute which is read from {@code attrs.xml}.
+ *
+ * @author Akash Yadav
+ * @see com.itsaky.attrinfo.AttrInfo
+ */
public class Attr {
- public String prefix;
+
+ public static final int REFERENCE = 1;
+ public static final int COLOR = 1 << 1;
+ public static final int BOOLEAN = 1 << 2;
+ public static final int DIMENSION = 1 << 3;
+ public static final int FLOAT = 1 << 4;
+ public static final int INTEGER = 1 << 5;
+ public static final int FRACTION = 1 << 6;
+ public static final int STRING = 1 << 7;
+ public static final int ENUM = 1 << 8;
+ public static final int FLAG = 1 << 9;
+ public static final int UNKNOWN = 1 << 10;
+
+ public String namespace;
public String name;
-
public Set possibleValues;
-
- public Attr(String name, boolean isAndroid) {
+
+ public int format;
+
+ private static final Logger LOG = Logger.instance ("AttrInfo::Attr");
+
+ public Attr (String name, boolean isAndroid) {
this.name = name;
- this.prefix = isAndroid ? "android" : "app";
- this.possibleValues = new HashSet<>();
+ this.namespace = isAndroid ? "android" : "app";
+ this.possibleValues = new HashSet<> ();
}
-
- public boolean hasPossibleValues() {
- return possibleValues != null && possibleValues.size() > 0;
+
+ public boolean hasPossibleValues () {
+ return possibleValues != null && possibleValues.size () > 0;
}
-
+
@Override
- public String toString() {
- return String.format("[%s=%s]", name, possibleValues.toString());
+ public String toString () {
+ return "Attr [" +
+ " name: " + name + "\n" +
+ " namespace: " + namespace + "\n" +
+ " values: " + TextUtils.join (", ", this.possibleValues) + "\n" +
+ " format: " + format + "\n" +
+ "]";
+ }
+
+ @Override
+ public boolean equals (Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (o == null || getClass () != o.getClass ()) {
+ return false;
+ }
+
+ Attr attr = (Attr) o;
+ return format == attr.format
+ && Objects.equals (namespace, attr.namespace)
+ && Objects.equals (name, attr.name)
+ && Objects.equals (possibleValues, attr.possibleValues);
+ }
+
+ @Override
+ public int hashCode () {
+ return Objects.hash (namespace, name, possibleValues, format);
+ }
+
+ public static int formatForName (@NonNull String names) {
+ var result = 0;
+ if (names.contains ("|")) {
+ for (var name : names.split (Pattern.quote ("|"))) {
+ result |= formatForSingleName (name);
+ }
+ } else {
+ result = formatForSingleName (names);
+ }
+ return result;
+ }
+
+ @Contract(pure = true)
+ public static int formatForSingleName (@NonNull String name) {
+ switch (name) {
+ case "reference":
+ return REFERENCE;
+ case "color":
+ return COLOR;
+ case "boolean":
+ return BOOLEAN;
+ case "dimension":
+ return DIMENSION;
+ case "float":
+ return FLOAT;
+ case "integer":
+ return INTEGER;
+ case "fraction":
+ return FRACTION;
+ case "string":
+ return STRING;
+ case "enum":
+ return ENUM;
+ case "flag":
+ return FLAG;
+ default:
+ return UNKNOWN;
+ }
}
}
diff --git a/sdk-info/src/main/res/raw/attrs.xml b/sdk-info/src/main/res/raw/attrs.xml
index a5f505176..59899d45b 100755
--- a/sdk-info/src/main/res/raw/attrs.xml
+++ b/sdk-info/src/main/res/raw/attrs.xml
@@ -3118,6 +3118,7 @@
+