diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..4bec4ea8a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,117 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..a55e7a179 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c489ddea4..05aa85ea5 100755 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,6 +8,7 @@ + @@ -15,10 +16,15 @@ - + + + + + + diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml new file mode 100644 index 000000000..dde5d2b09 --- /dev/null +++ b/.idea/render.experimental.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/DesignerActivity.java b/app/src/main/java/com/itsaky/androidide/DesignerActivity.java index 45423ab5c..6b2b40d45 100755 --- a/app/src/main/java/com/itsaky/androidide/DesignerActivity.java +++ b/app/src/main/java/com/itsaky/androidide/DesignerActivity.java @@ -1,24 +1,27 @@ /************************************************************************************ * 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; import android.annotation.SuppressLint; import android.content.ClipData; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.view.Gravity; import android.view.View; @@ -41,21 +44,25 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.core.content.ContextCompat; import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; import androidx.transition.Fade; import androidx.transition.Slide; import androidx.transition.TransitionManager; import androidx.transition.TransitionSet; +import com.blankj.utilcode.util.DeviceUtils; +import com.blankj.utilcode.util.SizeUtils; import com.blankj.utilcode.util.ThrowableUtils; import com.itsaky.androidide.adapters.WidgetGroupItemAdapter; import com.itsaky.androidide.adapters.WidgetItemAdapter; import com.itsaky.androidide.app.StudioActivity; import com.itsaky.androidide.databinding.ActivityDesignerBinding; -import com.itsaky.androidide.ui.WidgetDragData; -import com.itsaky.androidide.ui.WidgetDragListener; import com.itsaky.androidide.models.UIWidget; import com.itsaky.androidide.models.UIWidgetGroup; +import com.itsaky.androidide.ui.WidgetDragData; +import com.itsaky.androidide.ui.WidgetDragListener; import com.itsaky.androidide.ui.WidgetDragShadowBuilder; +import com.itsaky.androidide.ui.WidgetTouchListener; import com.itsaky.androidide.utils.Logger; import com.itsaky.layoutinflater.IAttribute; import com.itsaky.layoutinflater.IInflateListener; @@ -73,194 +80,251 @@ import java.util.List; public class DesignerActivity extends StudioActivity implements WidgetItemAdapter.OnDragStartListener { private ActivityDesignerBinding mBinding; - private UIWidgetGroup selectedGroup; - + private UIWidgetGroup checkedWidgetCategory; + private IView selectedView; + + private Drawable selectedViewForeground; + public static final String KEY_LAYOUT_PATH = "designer_layoutPath"; public static final String DRAGGING_WIDGET_TAG = "DRAGGING_WIDGET"; public static final String DRAGGING_WIDGET_MIME = "application/ide_widget"; - - private static final Logger LOG = Logger.instance("DesignerActivity"); - - private final List widgetGroups = new ArrayList<>(); - + + private static final Logger LOG = Logger.instance ("DesignerActivity"); + + private final boolean isTablet = DeviceUtils.isTablet (); + private final List 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 @@ +