Removed UI designer

This commit is contained in:
Akash Yadav
2022-11-02 13:48:41 +05:30
parent 1262409dc5
commit 9e16d827c8
148 changed files with 198 additions and 17953 deletions
+1
View File
@@ -42,6 +42,7 @@
<module name="AndroidIDE.gradle-tooling.plugin" target="1.8" />
<module name="AndroidIDE.javac" target="1.8" />
<module name="AndroidIDE.treesitter.test" target="1.7" />
<module name="AndroidIDE.xml-inflater" target="11" />
</bytecodeTargetLevel>
</component>
</project>
-1
View File
@@ -154,7 +154,6 @@ dependencies {
implementation project (':preferences')
implementation project (':resources')
implementation project (':treeview')
implementation project (':xml-inflater')
implementation fileTree (dir: 'libs', include: ['*.jar'])
-4
View File
@@ -44,10 +44,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".DesignerActivity"
android:configChanges="orientation|screenSize|uiMode"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".EditorActivity"
android:launchMode="singleTop"
@@ -1,531 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CheckedTextView;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
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.itsaky.androidide.adapters.WidgetGroupItemAdapter;
import com.itsaky.androidide.adapters.WidgetItemAdapter;
import com.itsaky.androidide.app.IDEActivity;
import com.itsaky.androidide.databinding.ActivityDesignerBinding;
import com.itsaky.androidide.fragments.sheets.AttrEditorSheet;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.models.UIWidget;
import com.itsaky.androidide.models.UIWidgetGroup;
import com.itsaky.androidide.tasks.TaskExecutor;
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.DialogUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IInflateListener;
import com.itsaky.inflater.ILayoutInflater;
import com.itsaky.inflater.IView;
import com.itsaky.inflater.IViewGroup;
import com.itsaky.inflater.LayoutInflaterConfiguration;
import com.itsaky.inflater.impl.BaseView;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
public class DesignerActivity extends IDEActivity
implements WidgetItemAdapter.OnDragStartListener,
AttrEditorSheet.OnViewDeletionFailedListener,
ILayoutInflater.ContextProvider {
public static final String KEY_LAYOUT_PATH = "designer_layoutPath";
public static final String KEY_RES_DIRS = "designer_resDirs";
public static final String KEY_GENERATED_CODE = "designer_xmlCode";
public static final String DRAGGING_WIDGET_TAG = "DRAGGING_WIDGET";
public static final String DRAGGING_WIDGET_MIME = "application/ide_widget";
private static final ILogger LOG = ILogger.newInstance("DesignerActivity");
public final String EDITOR_SHEET_DIALOG_TAG = "attribute_editor_dialog";
private final boolean isTablet = DeviceUtils.isTablet();
private final List<UIWidgetGroup> widgetGroups = new ArrayList<>();
private ActivityDesignerBinding mBinding;
private UIWidgetGroup checkedWidgetCategory;
private ILayoutInflater layoutInflater;
private IViewGroup inflatedRoot;
private File layout;
private boolean inflationFailed = false;
private AttrEditorSheet mEditorSheet;
// This will make sure to apply listeners, background and data to view that are inflated from
// XML.
private final IInflateListener mInflateListener =
new IInflateListener() {
@Override
public void onBeginInflate() {}
@Override
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));
}
setupInflatedView(view);
setDragDataToInflatedView(view);
}
@Override
public void onApplyAttribute(IAttribute attr, IView view) {}
@Override
public void onFinishInflate(IView rootView) {
if (rootView instanceof IViewGroup) {
inflatedRoot = (IViewGroup) rootView;
}
}
};
@Override
public void onBackPressed() {
if (inflationFailed) {
super.onBackPressed();
return;
}
// When the user presses the back button, set the activity result and finish this activity
final var progress = new ProgressSheet();
progress.setSubMessageEnabled(true);
progress.setShowTitle(false);
progress.setMessage(getString(R.string.title_generating_xml));
progress.setSubMessage(getString(R.string.please_wait));
progress.show(getSupportFragmentManager(), "generate_code_progress");
final var future =
CompletableFuture.supplyAsync(
() -> BaseView.XML_HEADER.concat("\n").concat(inflatedRoot.generateCode()));
future.whenComplete(
(s, throwable) -> {
progress.dismiss();
if (throwable != null) {
notifyXmlGenerationFailed(throwable);
return;
}
try {
finishWithResult(future.get());
} catch (ExecutionException | InterruptedException e) {
notifyXmlGenerationFailed(e);
}
});
}
private void notifyXmlGenerationFailed(Throwable error) {
final var errorMessage = getString(R.string.msg_generate_xml_failed);
LOG.error(errorMessage, error);
DialogUtils.newYesNoDialog(
this,
getString(R.string.title_code_generation_failed),
getString(R.string.msg_code_generation_failed),
(dialog1, which) -> finishWithError(),
(dialog1, which) -> dialog1.dismiss())
.show();
}
private void finishWithError() {
final var intent = new Intent();
setResult(-123, intent);
finish();
}
private void finishWithResult(String result) {
final var intent = new Intent();
intent.putExtra(KEY_GENERATED_CODE, result);
setResult(RESULT_OK, intent);
finish();
}
@Override
public void onDragStarted(View view) {
mBinding.getRoot().closeDrawer(GravityCompat.START);
}
@Override
public boolean onDeletionFailed(@NonNull IView view) {
final var v = view.asView();
if (mBinding.layoutContainer.indexOfChild(v) >= 0) {
mBinding.layoutContainer.removeView(v);
return true;
}
return false;
}
@Override
public Context getContext() {
return this;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setSupportActionBar(mBinding.toolbar);
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
final var toggle =
new ActionBarDrawerToggle(
this, mBinding.getRoot(), mBinding.toolbar, R.string.app_name, R.string.app_name) {
@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);
final var resDirs = extras.getStringArrayList(KEY_RES_DIRS);
getSupportActionBar().setTitle(name);
try {
this.layout = new File(path);
LOG.debug("Inflating layout file:", this.layout);
LOG.debug("Resource directories:", TextUtils.join(",\n", resDirs));
//noinspection deprecation
final var pd = ProgressDialog.show(this, null, getString(R.string.please_wait), true, false);
final var dirs = resDirs.stream().map(File::new).collect(Collectors.toSet());
final var future = getApp().createInflaterConfig(this, dirs);
TaskExecutor.executeAsyncProvideError(
future::get,
(config, throwable) -> {
pd.dismiss();
if (config == null || throwable != null) {
LOG.error("Unable to create inflater configuration", throwable);
return;
}
inflatePath(path, config);
});
} catch (Throwable th) {
onLayoutInflationFailed(th);
}
setupWidgets();
// Always open the drawer if this device is a tablet.
if (isTablet) {
mBinding.getRoot().setScrimColor(Color.TRANSPARENT);
mBinding.getRoot().openDrawer(GravityCompat.START);
mBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN);
}
}
@Override
protected View bindLayout() {
this.mBinding = ActivityDesignerBinding.inflate(getLayoutInflater());
return mBinding.getRoot();
}
@Override
protected void onDestroy() {
super.onDestroy();
LOG.logThis();
if (isAttrEditorShowing()) {
getAttrEditorSheet().dismiss();
}
this.mEditorSheet = null;
// Release the reference to inflate listener and context from the layout inflater
// Failing to do so will lead getSupportFragmentManager() to return a destroyed fragment
// manager
try {
if (layoutInflater == null) {
LOG.debug("Skipping layout inflater context reference release. Inflater is null");
return;
}
this.layoutInflater.unregisterListener(mInflateListener);
this.layoutInflater.resetContextProvider(null);
} catch (Throwable e) {
LOG.error("Unable to release activity references from ILayoutInflater", e);
}
mBinding = null;
}
private boolean isAttrEditorShowing() {
return getAttrEditorSheet().getDialog() != null && getAttrEditorSheet().getDialog().isShowing();
}
private AttrEditorSheet getAttrEditorSheet() {
return this.mEditorSheet == null
? mEditorSheet = new AttrEditorSheet().setLayout(this.layout)
: mEditorSheet;
}
protected void onLayoutInflationFailed(Throwable th) {
mBinding.layoutContainer.removeAllViews();
mBinding.layoutContainer.addView(createErrorText(th));
LOG.error(getString(R.string.err_cannot_inflate_layout), th);
inflationFailed = true;
}
private void inflatePath(String path, LayoutInflaterConfiguration config) {
try {
this.layoutInflater = ILayoutInflater.newInstance(config);
this.layoutInflater.resetContextProvider(this);
this.layoutInflater.registerInflateListener(this.mInflateListener);
final IView view = this.layoutInflater.inflatePath(path, mBinding.layoutContainer);
if (this.inflatedRoot != null) {
this.inflatedRoot.asView().setOnDragListener(getOnDragListener(this.inflatedRoot));
}
mBinding.layoutContainer.addView(view.asView());
mBinding.layoutContainer.setOnClickListener(
v -> mBinding.getRoot().openDrawer(GravityCompat.START));
tryDismiss();
} catch (Throwable error) {
onLayoutInflationFailed(error);
}
}
private void tryDismiss() {
final var manager = getSupportFragmentManager();
final var frag = manager.findFragmentByTag(EDITOR_SHEET_DIALOG_TAG);
if (frag != null) {
final var transaction = manager.beginTransaction();
transaction.remove(frag);
transaction.commit();
}
}
private void setupInflatedView(@NonNull IView view) {
final var listener =
new WidgetTouchListener(view, this::onLayoutViewClick, this::onLayoutViewLongClick);
view.asView().setOnTouchListener(listener);
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);
view.removeFromParent();
return true;
}
private void onLayoutViewClick(@NonNull IView view) {
if (isAttrEditorShowing()) {
getAttrEditorSheet().dismiss();
}
getAttrEditorSheet().setSelectedView(view);
if (!getSupportFragmentManager().isDestroyed()) {
getAttrEditorSheet().show(getSupportFragmentManager(), EDITOR_SHEET_DIALOG_TAG);
}
}
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.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.checkedWidgetCategory.setSelected(false);
this.checkedWidgetCategory = group;
this.checkedWidgetCategory.setSelected(true);
TransitionManager.beginDelayedTransition(
mBinding.navigation,
new TransitionSet().addTransition(new Slide(Gravity.END)).addTransition(new Fade()));
this.mBinding.groupItems.getAdapter().notifyDataSetChanged();
}
@NonNull
private TextView createErrorText(Throwable th) {
final TextView error = new TextView(this);
error.setText(createErrorMessage(th));
error.setLayoutParams(new ViewGroup.LayoutParams(-1, -1));
error.setTextColor(ContextCompat.getColor(this, android.R.color.black));
error.setBackgroundColor(ContextCompat.getColor(this, android.R.color.white));
error.setGravity(Gravity.CENTER);
return error;
}
private CharSequence createErrorMessage(final Throwable th) {
final var sb = new StringBuilder();
if (th == null) {
sb.append("Unknown error");
return sb;
}
var err = th;
var first = true;
while (err != null) {
if (!first) {
sb.append("\n");
}
sb.append(err.getMessage());
err = err.getCause();
}
return sb;
}
@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::setupInflatedView);
}
private void setDragDataToInflatedView(@NonNull IView view) {
view.setExtraData(new WidgetDragData(true, view, null));
}
}
@@ -54,9 +54,6 @@ import android.widget.CheckBox;
import android.widget.LinearLayout;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.GravityInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -128,19 +125,14 @@ import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.androidide.xml.resources.ResourceTableRegistry;
import com.itsaky.androidide.xml.versions.ApiVersionsRegistry;
import com.itsaky.androidide.xml.widgets.WidgetTableRegistry;
import com.itsaky.inflater.values.ValuesTableFactory;
import com.itsaky.toaster.Toaster;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -167,7 +159,7 @@ public class EditorActivity extends IDEActivity
private final EditorEventListener mBuildEventListener = new EditorEventListener();
private final EditorActivityLifecyclerObserver mLifecycleObserver =
new EditorActivityLifecyclerObserver();
private ActivityEditorBinding mBinding;
private LayoutDiagnosticInfoBinding mDiagnosticInfoBinding;
private final LogReceiver mLogReceiver = new LogReceiver().setLogListener(this::appendApkLog);
@@ -176,7 +168,6 @@ public class EditorActivity extends IDEActivity
private TextSheetFragment mDaemonStatusFragment;
private ProgressSheet mSearchingProgress;
private AlertDialog mFindInProjectDialog;
private ActivityResultLauncher<Intent> mUIDesignerLauncher;
private BottomSheetBehavior<? extends View> mEditorBottomSheet;
private EditorViewModel mViewModel;
private final ServiceConnection mGradleServiceConnection =
@@ -404,7 +395,7 @@ public class EditorActivity extends IDEActivity
if (mBinding == null) {
return null;
}
for (int i = 0; i < mViewModel.getOpenedFileCount(); i++) {
final CodeEditorView editor = (CodeEditorView) mBinding.editorContainer.getChildAt(i);
if (file.equals(editor.getFile())) {
@@ -429,7 +420,7 @@ public class EditorActivity extends IDEActivity
IntentUtils.openImage(this, file);
return null;
}
if (mBinding == null) {
return null;
}
@@ -713,27 +704,7 @@ public class EditorActivity extends IDEActivity
}
}
public void previewLayout() {
try {
if (getCurrentEditor() == null || getCurrentEditor().getFile() == null) {
LOG.error("No file is opened. Cannot preview layout.");
return;
}
saveAll(false);
final Intent intent = new Intent(this, DesignerActivity.class);
intent.putExtra(
DesignerActivity.KEY_LAYOUT_PATH, getCurrentEditor().getFile().getAbsolutePath());
intent.putStringArrayListExtra(DesignerActivity.KEY_RES_DIRS, getResourceDirPaths());
LOG.info("Launching UI Designer...");
mUIDesignerLauncher.launch(intent);
} catch (Throwable th) {
LOG.error(getString(string.err_cannot_preview_layout), th);
toast(string.msg_cannot_preview_layout, Toaster.Type.ERROR);
}
}
public void previewLayout() {}
public boolean saveAll(boolean notify) {
return saveAll(notify, false);
@@ -931,10 +902,6 @@ public class EditorActivity extends IDEActivity
setupContainers();
setupDiagnosticInfo();
mUIDesignerLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), this::onGetUIDesignerResult);
EditorActivityActions.register(this);
}
@@ -1081,25 +1048,6 @@ public class EditorActivity extends IDEActivity
} catch (Throwable th) {
// ignored
}
CompletableFuture.runAsync(
() -> {
final var resDirs = getResourceDirs();
resDirs.removeIf(Objects::isNull);
ValuesTableFactory.setupWithResDirectories(resDirs.toArray(new File[0]));
});
}
private Set<File> getResourceDirs() {
return ProjectManager.INSTANCE.getApplicationResDirectories();
}
@NonNull
private ArrayList<String> getResourceDirPaths() {
return getResourceDirs().stream()
.filter(Objects::nonNull)
.map(File::getAbsolutePath)
.collect(Collectors.toCollection(ArrayList::new));
}
private void setupDrawerToggle() {
@@ -1297,31 +1245,6 @@ public class EditorActivity extends IDEActivity
builder.show();
}
@Contract(pure = true)
private void onGetUIDesignerResult(@NonNull ActivityResult result) {
if (mBinding == null) {
return;
}
final var index = mBinding.editorContainer.getDisplayedChild();
final var editor = getEditorAtIndex(index);
if (editor != null && result.getResultCode() == RESULT_OK) {
final var data = result.getData();
if (data != null && data.hasExtra(DesignerActivity.KEY_GENERATED_CODE)) {
final var code = data.getStringExtra(DesignerActivity.KEY_GENERATED_CODE);
editor.getEditor().replaceContent(code);
editor.markModified();
saveAll();
} else {
final var msg = getString(string.msg_invalid_designer_result);
toast(msg, Toaster.Type.ERROR);
LOG.error(msg, "Data returned by UI Designer is null or is invalid.");
}
} else {
LOG.error(
"UI Designer returned an invalid result code.", "Result code: " + result.getResultCode());
}
}
private void openTerminal() {
final Intent intent = new Intent(this, TerminalActivity.class);
intent.putExtra(
@@ -1,79 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters;
import androidx.annotation.NonNull;
import com.itsaky.androidide.resources.R;
import com.itsaky.attrinfo.models.Attr;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* An {@link com.itsaky.androidide.adapters.IconTextAdapter} for {@link
* com.itsaky.attrinfo.models.Attr}.
*
* @author Akash Yadav
*/
public class AttrListAdapter extends IconTextAdapter<Attr> {
private final Consumer<Attr> clickConsumer;
private final List<Attr> attrs;
public AttrListAdapter(Consumer<Attr> clickConsumer, List<Attr> attrs) {
this.clickConsumer = clickConsumer;
if (attrs == null) {
attrs = new ArrayList<>();
}
this.attrs = attrs;
}
@Override
public int getItemCount() {
return attrs.size();
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
super.onBindViewHolder(holder, position);
if (clickConsumer != null) {
holder.binding.getRoot().setOnClickListener(v -> clickConsumer.accept(getItemAt(position)));
}
}
@NonNull
@Override
public Attr getItemAt(int index) {
return attrs.get(index);
}
@Override
public int getIconResource(int index) {
return R.drawable.ic_xml_attribute;
}
@NonNull
@Override
public String getItemText(int index) {
return getItemAt(index).name;
}
}
@@ -1,126 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/**
* Adapter for showing the autocomplete items while editing attribute values in UI Designer.
*
* @author Akash Yadav
*/
public class AttrValueCompletionAdapter extends BaseAdapter implements Filterable {
private final List<String> items;
private final List<String> filtered;
private final Context context;
private final Filter mFilter =
new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
final var result = new FilterResults();
if (TextUtils.isEmpty(constraint)) {
final var list = new ArrayList<>(items);
result.values = list;
result.count = list.size();
return result;
}
final var match = constraint.toString().trim().toLowerCase(Locale.ROOT);
final var list = new ArrayList<String>();
for (String item : items) {
if (item.contains(match)) {
list.add(item);
}
}
result.values = list;
result.count = list.size();
return result;
}
@Override
protected void publishResults(CharSequence constraint, @NonNull FilterResults results) {
//noinspection unchecked
List<String> items = (List<String>) results.values;
Objects.requireNonNull(items);
filtered.clear();
filtered.addAll(items);
if (items.isEmpty()) {
notifyDataSetInvalidated();
} else {
notifyDataSetChanged();
}
}
};
public AttrValueCompletionAdapter(Context context, List<String> items) {
this.context = context;
this.items = items;
this.filtered = new ArrayList<>(items);
}
@Override
public int getCount() {
return this.filtered.size();
}
@Override
public Object getItem(int position) {
return filtered.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView =
((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(android.R.layout.simple_list_item_1, parent, false);
}
((TextView) convertView).setText(getItem(position).toString());
return convertView;
}
@Override
public Filter getFilter() {
return mFilter;
}
}
@@ -1,124 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import com.itsaky.androidide.fragments.attr.BaseValueEditorFragment;
import com.itsaky.androidide.models.XMLAttribute;
import com.itsaky.androidide.utils.ILogger;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Adapter for value format tabs of attribute editor in UI Designer.
*
* @author Akash Yadav
*/
@SuppressWarnings("deprecation")
public class AttrValueFormatTabAdapter extends FragmentStatePagerAdapter {
private static final ILogger LOG = ILogger.newInstance("AttrValueEditorTabAdapter");
private final List<Fragment> mFragments = new ArrayList<>();
private final List<String> mTitles = new ArrayList<>();
private final XMLAttribute attribute;
public AttrValueFormatTabAdapter(@NonNull FragmentManager manager, XMLAttribute attribute) {
super(manager);
Objects.requireNonNull(attribute);
this.attribute = attribute;
}
public void addFragment(final Class<? extends BaseValueEditorFragment> fragment, String title) {
Objects.requireNonNull(fragment);
Objects.requireNonNull(title);
if (TextUtils.isEmpty(title.trim())) {
throw new IllegalArgumentException("Invalid tab title");
}
mTitles.add(capitalize(title));
mFragments.add(createFragment(fragment, title));
}
@NonNull
private String capitalize(String title) {
title = title.trim();
final var sb = new StringBuilder();
for (int i = 0; i < title.length(); i++) {
var ch = title.charAt(i);
if (i == 0) {
ch = Character.toUpperCase(ch);
} else {
ch = Character.toLowerCase(ch);
}
sb.append(ch);
}
return sb.toString();
}
@NonNull
public Fragment createFragment(
Class<? extends BaseValueEditorFragment> clazz, @NonNull final String name) {
try {
final var frag = clazz.newInstance();
final var args = new Bundle();
args.putParcelable(BaseValueEditorFragment.KEY_ATTR, attribute);
args.putString(BaseValueEditorFragment.KEY_NAME, name);
frag.setArguments(args);
return frag;
} catch (Throwable th) {
LOG.error("Unable to create value editor fragment", th);
throw new RuntimeException(th);
}
}
public void removeAll() {
mFragments.clear();
mTitles.clear();
}
@NonNull
@Override
public Fragment getItem(int position) {
return mFragments.get(position);
}
@Override
public int getCount() {
return mFragments.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return mTitles.get(position);
}
}
@@ -1,74 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters;
import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.models.IconTextListItem;
import com.itsaky.androidide.models.UIWidgetGroup;
import java.util.List;
public class WidgetGroupItemAdapter extends SimpleIconTextAdapter
implements SimpleIconTextAdapter.OnBindListener<IconTextListItem> {
private final OnGroupClickListener clickListener;
public WidgetGroupItemAdapter(
@NonNull List<UIWidgetGroup> groups, @Nullable OnGroupClickListener clickListener) {
super(groups);
this.clickListener = clickListener;
super.setOnBindListener(this);
}
@Override
public boolean onBind(IconTextListItem item, VH holder, int position) {
// Ignored
return false;
}
@Override
public void postBind(IconTextListItem item, @NonNull VH holder, int position) {
final var binding = holder.binding;
final var group = (UIWidgetGroup) item;
if (group.isSelected()) {
binding
.getRoot()
.setBackground(
ContextCompat.getDrawable(
binding.getRoot().getContext(), R.drawable.bg_widget_group_selected));
} else {
binding.getRoot().setBackgroundColor(Color.TRANSPARENT);
}
if (this.clickListener != null) {
binding.getRoot().setOnClickListener(v -> this.clickListener.onGroupClick(group));
}
}
public interface OnGroupClickListener {
void onGroupClick(UIWidgetGroup group);
}
}
@@ -1,79 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters;
import android.content.ClipData;
import android.view.View;
import androidx.annotation.NonNull;
import com.itsaky.androidide.DesignerActivity;
import com.itsaky.androidide.models.IconTextListItem;
import com.itsaky.androidide.models.UIWidget;
import com.itsaky.androidide.ui.WidgetDragData;
import com.itsaky.androidide.ui.WidgetDragShadowBuilder;
import java.util.List;
public class WidgetItemAdapter extends SimpleIconTextAdapter
implements SimpleIconTextAdapter.OnBindListener<IconTextListItem> {
private final OnDragStartListener dragStartListener;
public WidgetItemAdapter(List<UIWidget> widgets, OnDragStartListener dragStartListener) {
super(widgets);
this.dragStartListener = dragStartListener;
setOnBindListener(this);
}
@Override
public boolean onBind(IconTextListItem item, @NonNull VH holder, int position) {
return false; // Returning true will not set the text and icon
}
@Override
public void postBind(IconTextListItem item, VH holder, int position) {
final var binding = holder.binding;
final var root = binding.getRoot();
final var widget = (UIWidget) getItemAt(position);
root.setOnLongClickListener(
v -> {
final var dragData = new WidgetDragData(false, null, widget);
final var shadow = new WidgetDragShadowBuilder(binding.icon);
final var dataItem = new ClipData.Item(DesignerActivity.DRAGGING_WIDGET_TAG);
final var data =
new ClipData(
DesignerActivity.DRAGGING_WIDGET_TAG,
new String[] {DesignerActivity.DRAGGING_WIDGET_MIME},
dataItem);
binding.icon.startDragAndDrop(data, shadow, dragData, 0);
if (this.dragStartListener != null) {
this.dragStartListener.onDragStarted(v);
}
return true;
});
}
public interface OnDragStartListener {
void onDragStarted(View view);
}
}
@@ -1,103 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.databinding.LayoutAttrEditorSheetItemBinding;
import com.itsaky.androidide.models.XMLAttribute;
import java.util.List;
public class XMLAttributeListAdapter extends RecyclerView.Adapter<XMLAttributeListAdapter.VH> {
private final List<XMLAttribute> attributes;
private final OnClickListener clickListener;
public XMLAttributeListAdapter(List<XMLAttribute> attributes, OnClickListener clickListener) {
this.attributes = attributes;
this.clickListener = clickListener;
}
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new VH(
LayoutAttrEditorSheetItemBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
final var binding = holder.binding;
final var attr = attributes.get(position);
final var sb = new SpannableStringBuilder();
sb.append(attr.getAttributeName());
if (attr.isApplied()) {
sb.append(" = ");
sb.append(attr.getValue(), new StyleSpan(Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
final var length = attr.getValue().length();
sb.setSpan(
new ForegroundColorSpan(
ContextCompat.getColor(binding.text.getContext(), R.color.primaryTextColor)),
sb.length() - length,
sb.length() - 1,
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
}
binding.text.setText(sb);
binding
.getRoot()
.setOnClickListener(
v -> {
if (this.clickListener != null) {
this.clickListener.onClick(binding, attr);
}
});
}
@Override
public int getItemCount() {
return attributes.size();
}
static class VH extends RecyclerView.ViewHolder {
LayoutAttrEditorSheetItemBinding binding;
VH(@NonNull LayoutAttrEditorSheetItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
public interface OnClickListener {
void onClick(LayoutAttrEditorSheetItemBinding binding, XMLAttribute attribute);
}
}
@@ -23,36 +23,20 @@ import android.net.Uri;
import com.blankj.utilcode.util.ThrowableUtils;
import com.itsaky.androidide.BuildConfig;
import com.itsaky.androidide.CrashHandlerActivity;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.events.AppEventsIndex;
import com.itsaky.androidide.events.LspApiEventsIndex;
import com.itsaky.androidide.events.LspJavaEventsIndex;
import com.itsaky.androidide.events.ProjectsApiEventsIndex;
import com.itsaky.androidide.preferences.internal.XmlPreferencesKt;
import com.itsaky.androidide.projects.ProjectResourceTable;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.attrinfo.AttrInfo;
import com.itsaky.inflater.ILayoutInflater;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.LayoutInflaterConfiguration;
import com.itsaky.sdk.SDKInfo;
import com.itsaky.toaster.ToastUtilsKt;
import com.itsaky.toaster.Toaster;
import com.itsaky.widgets.WidgetInfo;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
public class IDEApplication extends BaseApplication {
private static final ILogger LOG = ILogger.newInstance("IDEApplication");
private static IDEApplication instance;
private static SDKInfo sdkInfo;
private IResourceTable mResTable;
private Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
public static IDEApplication getInstance() {
@@ -72,8 +56,6 @@ public class IDEApplication extends BaseApplication {
.addIndex(new LspApiEventsIndex())
.addIndex(new LspJavaEventsIndex())
.installDefaultEventBus();
getApiInformation();
}
private void handleCrash(Thread thread, Throwable th) {
@@ -95,54 +77,6 @@ public class IDEApplication extends BaseApplication {
}
}
/** Reads API version information from api-versions.xml */
public CompletableFuture<SDKInfo> getApiInformation() {
return CompletableFuture.supplyAsync(
() -> {
if (sdkInfo == null) {
try {
sdkInfo = new SDKInfo(IDEApplication.this);
} catch (Throwable th) {
LOG.error(getString(R.string.err_init_sdkinfo), th);
}
}
return sdkInfo;
});
}
public CompletableFuture<LayoutInflaterConfiguration> createInflaterConfig(
ILayoutInflater.ContextProvider contextProvider, Set<File> resDirs) {
return CompletableFuture.supplyAsync(
() -> {
try {
final var sdkInfo = getApiInformation().get();
return new LayoutInflaterConfiguration.Builder()
.setAttrInfo(sdkInfo.getAttrInfo())
.setWidgetInfo(sdkInfo.getWidgetInfo())
.setResourceFinder(getResourceTable())
.setResourceDirectories(resDirs)
.setContextProvider(contextProvider)
.create();
} catch (Throwable e) {
throw new CompletionException(e);
}
});
}
public IResourceTable getResourceTable() {
return mResTable == null ? mResTable = new ProjectResourceTable() : mResTable;
}
public AttrInfo attrInfo() {
return sdkInfo.getAttrInfo();
}
public WidgetInfo widgetInfo() {
return sdkInfo.getWidgetInfo();
}
public void showChangelog() {
final var intent = new Intent(Intent.ACTION_VIEW);
intent.setData(
@@ -1,103 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.text.Editable;
import android.text.TextUtils;
import android.widget.AutoCompleteTextView;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.adapters.AttrValueCompletionAdapter;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.TextWatcherAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* @author Akash Yadav
*/
public abstract class AbstractReferenceEditor extends BaseValueEditorFragment {
private static final ILogger LOG = ILogger.newInstance("ReferenceEditor");
public TextWatcherAdapter resInputWatcher;
protected void setupReferenceInput(MaterialAutoCompleteTextView referenceInput) {
Objects.requireNonNull(referenceInput);
referenceInput.setText(attribute.getValue());
if (resInputWatcher != null) {
referenceInput.removeTextChangedListener(resInputWatcher);
}
resInputWatcher =
new TextWatcherAdapter() {
@Override
public void afterTextChanged(@NonNull Editable s) {
final var text = s.toString().trim();
if (TextUtils.isEmpty(text)) {
return;
}
notifyValueChanged(text);
}
};
referenceInput.addTextChangedListener(resInputWatcher);
CompletableFuture.supplyAsync(this::computeReferenceItems)
.whenComplete((list, error) -> handleAutoCompleteResult(list, error, referenceInput));
}
@Override
protected void notifyValueChanged(@NonNull String newValue) {
try {
super.notifyValueChanged(newValue);
} catch (Throwable e) {
LOG.error("Unable to update resource value to '" + newValue + "'", e);
}
}
protected void handleAutoCompleteResult(
List<String> list, Throwable error, AutoCompleteTextView referenceInput) {
if (list == null || list.isEmpty()) {
LOG.error("No completion items found");
LOG.error("Error was:", error);
return;
}
LOG.debug("Found", list.size(), "resource items for completion");
ThreadUtils.runOnUiThread(
() -> {
final var adapter = new AttrValueCompletionAdapter(requireContext(), list);
referenceInput.setThreshold(1);
referenceInput.setAdapter(adapter);
adapter.notifyDataSetChanged();
});
}
protected List<String> computeReferenceItems() {
return new ArrayList<>();
}
}
@@ -1,113 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.widget.NestedScrollView;
import androidx.fragment.app.Fragment;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.models.XMLAttribute;
import com.itsaky.inflater.IAttribute;
import java.util.Objects;
/**
* Base class for value editor fragment.
*
* @author Akash Yadav
*/
public abstract class BaseValueEditorFragment extends Fragment {
public static final String KEY_ATTR = "editor_attr";
public static final String KEY_NAME = "editor_name";
protected OnValueChangeListener mValueChangeListener;
protected XMLAttribute attribute;
protected String name;
protected NestedScrollView scrollView;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mValueChangeListener = (OnValueChangeListener) getParentFragment();
}
@Nullable
@Override
public View onCreateView(
@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
if (scrollView != null) {
return scrollView;
}
scrollView = new NestedScrollView(inflater.getContext());
scrollView.setFillViewport(true);
scrollView.setLayoutParams(new ViewGroup.LayoutParams(-1, -1));
final var child = createView(inflater, scrollView, savedInstanceState);
if (child != null) {
scrollView.addView(child);
}
return scrollView;
}
@Nullable
protected abstract View createView(
@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState);
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final var args = requireArguments();
this.attribute = args.getParcelable(KEY_ATTR);
this.name = args.getString(KEY_NAME);
}
protected void notifyValueChanged(@NonNull String newValue) {
Objects.requireNonNull(newValue);
if (mValueChangeListener != null) {
mValueChangeListener.onValueChanged(this.attribute, newValue);
}
}
/** Listener for listening for value change event. */
public interface OnValueChangeListener {
/**
* Called when the value was changed.
*
* @param attribute The attribute whose value was changed.
* @param newValue The new value for the editor.
*/
void onValueChanged(IAttribute attribute, String newValue);
}
}
@@ -1,95 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.databinding.LayoutBooleanAttrEditorBinding;
import com.itsaky.inflater.values.FrameworkValues;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Akash Yadav
*/
public class BooleanEditor extends AbstractReferenceEditor {
private LayoutBooleanAttrEditorBinding binding;
@Override
protected @Nullable View createView(
@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
this.binding = LayoutBooleanAttrEditorBinding.inflate(inflater, container, false);
return this.binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.binding.choiceGroup.setOnCheckedChangeListener(
(group, checkedId) -> {
final var checked = (RadioButton) group.findViewById(checkedId);
notifyValueChanged(checked.getText().toString().trim());
});
if (!TextUtils.isEmpty(attribute.getValue())) {
switch (attribute.getValue()) {
case "true":
this.binding.boolTrue.setChecked(true);
break;
case "false":
this.binding.boolFalse.setChecked(true);
}
}
setupReferenceInput((MaterialAutoCompleteTextView) this.binding.booleanResInput.getEditText());
}
@Override
protected List<String> computeReferenceItems() {
final var list = new ArrayList<String>();
final var tables = ValuesTableFactory.getAllTables();
for (var entry : tables.entrySet()) {
final var dimens = entry.getValue().getTable("bool");
if (dimens != null) {
list.addAll(dimens.keySet().stream().map("@bool/"::concat).collect(Collectors.toSet()));
}
}
list.addAll(
FrameworkValues.listBools().stream()
.map("@android:bool/"::concat)
.collect(Collectors.toList()));
return list;
}
}
@@ -1,123 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.app.IDEApplication;
import com.itsaky.androidide.colorpicker.ColorPickerView;
import com.itsaky.androidide.databinding.LayoutColorAttrEditorBinding;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.inflater.util.CommonParseUtils;
import com.itsaky.inflater.values.FrameworkValues;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Akash Yadav
*/
public class ColorEditor extends AbstractReferenceEditor {
private LayoutColorAttrEditorBinding binding;
private Dialog colorPickerDialog;
@Override
protected @Nullable View createView(
@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
this.binding = LayoutColorAttrEditorBinding.inflate(inflater, container, false);
return this.binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.binding.pickColor.setOnClickListener(v -> getColorPickerDialog().show());
setupReferenceInput((MaterialAutoCompleteTextView) this.binding.colorResInput.getEditText());
try {
CommonParseUtils parser =
new CommonParseUtils(
IDEApplication.getInstance().getResourceTable(),
IDEApplication.getInstance().getApplicationContext().getResources().getDisplayMetrics());
final var col = parser.parseColor(attribute.getValue(), requireContext());
this.binding.colorPreview.setCardBackgroundColor(col);
} catch (Throwable th) {
// ignored
}
}
private Dialog getColorPickerDialog() {
if (colorPickerDialog == null) {
final var builder = DialogUtils.newMaterialDialogBuilder(requireContext());
ColorPickerView colorPickerView = new ColorPickerView(requireContext());
builder.setView(colorPickerView);
colorPickerView.setOnPickListener(
(color, hexCode) -> {
if (colorPickerDialog != null) {
colorPickerDialog.dismiss();
}
this.binding.colorPreview.setCardBackgroundColor(color);
notifyValueChanged(hexCode);
});
colorPickerDialog = builder.create();
}
return colorPickerDialog;
}
@Override
protected List<String> computeReferenceItems() {
final var list = new ArrayList<String>();
final var tables = ValuesTableFactory.getAllTables();
for (var entry : tables.entrySet()) {
final var dimens = entry.getValue().getTable("color");
if (dimens != null) {
list.addAll(dimens.keySet().stream().map("@color/"::concat).collect(Collectors.toSet()));
}
}
list.addAll(
FrameworkValues.listColors().stream()
.map("@android:color/"::concat)
.collect(Collectors.toList()));
final var resTable = IDEApplication.getInstance().getResourceTable();
list.addAll(resTable.listResourceNames("color"));
return list;
}
}
@@ -1,138 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.databinding.LayoutDimensionAttrEditorBinding;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.TextWatcherAdapter;
import com.itsaky.inflater.values.FrameworkValues;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Akash Yadav
*/
public class DimensionEditor extends AbstractReferenceEditor {
private static final ILogger LOG = ILogger.newInstance("DimensionEditor");
public TextWatcherAdapter dimensionInputWatcher;
private LayoutDimensionAttrEditorBinding binding;
private String[] dimensionUnits;
@Override
protected @Nullable View createView(
@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
this.binding = LayoutDimensionAttrEditorBinding.inflate(inflater, container, false);
return this.binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final var dimensionInput = Objects.requireNonNull(this.binding.dimensionInput.getEditText());
setDimensionValue(dimensionInput);
if (dimensionInputWatcher != null) {
dimensionInput.removeTextChangedListener(dimensionInputWatcher);
}
dimensionInputWatcher =
new TextWatcherAdapter() {
@Override
public void afterTextChanged(@NonNull Editable s) {
final var text = s.toString().trim();
final var unit = getDimensionUnits()[binding.unitSelector.getSelectedItemPosition()];
// This will call CommonParseUtils#parseDimension
notifyValueChanged(text.concat(unit));
}
};
dimensionInput.addTextChangedListener(dimensionInputWatcher);
final var dimensionResInput =
(MaterialAutoCompleteTextView)
Objects.requireNonNull(binding.dimensionResInput.getEditText());
setupReferenceInput(dimensionResInput);
}
private void setDimensionValue(EditText dimensionInput) {
final var val = attribute.getValue();
if (TextUtils.isEmpty(val)) {
return;
}
var dimension = val.substring(0, val.length() - 2);
if (TextUtils.isDigitsOnly(dimension)) {
dimensionInput.setText(dimension);
final var arr = requireContext().getResources().getStringArray(R.array.dimension_units);
final var index = Arrays.asList(arr).indexOf(val.substring(dimension.length()));
if (index >= 0) {
this.binding.unitSelector.setSelection(index);
}
}
}
@NonNull
private String[] getDimensionUnits() {
if (dimensionUnits == null) {
dimensionUnits = requireContext().getResources().getStringArray(R.array.dimension_units);
}
return dimensionUnits;
}
@Override
protected List<String> computeReferenceItems() {
final var list = new ArrayList<String>();
final var tables = ValuesTableFactory.getAllTables();
for (var entry : tables.entrySet()) {
final var dimens = entry.getValue().getTable("dimen");
if (dimens != null) {
list.addAll(dimens.keySet().stream().map("@dimen/"::concat).collect(Collectors.toSet()));
}
}
list.addAll(
FrameworkValues.listDimens().stream()
.map("@android:dimen/"::concat)
.collect(Collectors.toList()));
return list;
}
}
@@ -1,67 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.itsaky.androidide.utils.ILogger;
/**
* @author Akash Yadav
*/
public class EnumEditor extends FixedValueEditor {
private static final ILogger LOG = ILogger.newInstance("EnumEditor");
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.chipGroup.setSingleSelection(true);
this.chipGroup.setSelectionRequired(true);
final var val = this.attribute.getValue();
if (!TextUtils.isEmpty(val)) {
for (int i = 0; i < this.chipGroup.getChildCount(); i++) {
final var chip = (Chip) this.chipGroup.getChildAt(i);
if (val.equals(chip.getText().toString().trim())) {
chip.setChecked(true);
break;
}
}
}
}
@Override
protected void onCheckChanged(@NonNull ChipGroup group, int checkedId) {
final var chip = (Chip) group.findViewById(group.getCheckedChipId());
if (chip == null) {
LOG.error("Unable to update enum value. Checked Chip view is null.");
return;
}
final var val = chip.getText().toString().trim();
notifyValueChanged(val);
}
}
@@ -1,101 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.itsaky.androidide.resources.R;
import java.util.Objects;
/**
* @author Akash Yadav
*/
public abstract class FixedValueEditor extends BaseValueEditorFragment {
protected ChipGroup chipGroup;
@Nullable
@Override
protected View createView(
@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
this.chipGroup = new ChipGroup(inflater.getContext());
this.chipGroup.setLayoutParams(
new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
this.chipGroup.setSelectionRequired(true);
this.chipGroup.setSingleSelection(false);
return this.chipGroup;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Objects.requireNonNull(this.attribute);
Objects.requireNonNull(this.chipGroup);
this.chipGroup.setSingleSelection(false);
this.chipGroup.setSelectionRequired(true);
final var attr = this.attribute.getAttr();
for (var value : attr.possibleValues) {
if (TextUtils.isEmpty(value)) {
continue;
}
this.chipGroup.addView(newChip(value, value.equals(this.attribute.getValue())));
}
this.chipGroup.setOnCheckedChangeListener(this::onCheckChanged);
}
protected abstract void onCheckChanged(@NonNull ChipGroup group, int checkedId);
protected Chip newChip(String title, boolean checked) {
final var chip = new Chip(requireContext());
chip.setId(View.generateViewId());
chip.setText(title);
chip.setCheckedIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_ok));
chip.setChecked(checked);
chip.setChipBackgroundColor(
ContextCompat.getColorStateList(requireContext(), R.color.bg_enum_chips));
chip.setTextColor(ContextCompat.getColor(requireContext(), R.color.primaryTextColor));
chip.setLayoutParams(
new ChipGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
chip.setChipStrokeWidth(1f);
chip.setChipStrokeColor(
ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.primaryDarkColor)));
return chip;
}
}
@@ -1,90 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.itsaky.androidide.utils.ILogger;
import java.util.ArrayList;
import java.util.Arrays;
/**
* @author Akash Yadav
*/
public class FlagEditor extends FixedValueEditor {
private static final ILogger LOG = ILogger.newInstance("FlagEditor");
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.chipGroup.setSingleSelection(false);
this.chipGroup.setSelectionRequired(true);
this.chipGroup.setOnCheckedChangeListener(null); // does not work for multiple selections
if (!TextUtils.isEmpty(attribute.getValue())) {
final var values = Arrays.asList(this.attribute.getValue().split("\\|"));
for (int i = 0; i < this.chipGroup.getChildCount(); i++) {
final var chip = (Chip) this.chipGroup.getChildAt(i);
if (values.contains(chip.getText().toString().trim())) {
chip.setChecked(true);
}
}
}
}
@Override
protected void onCheckChanged(@NonNull ChipGroup group, int checkedId) {
// Does noting...
}
@Override
protected Chip newChip(String title, boolean checked) {
final var chip = super.newChip(title, checked);
chip.setOnCheckedChangeListener(this::onCheckChangedMultiple);
return chip;
}
protected void onCheckChangedMultiple(CompoundButton compoundButton, boolean b) {
final var items = new ArrayList<String>();
for (var id : this.chipGroup.getCheckedChipIds()) {
final var chip = (Chip) this.chipGroup.findViewById(id);
if (chip == null) {
LOG.error("Unable to find chip with checked id:", id);
continue;
}
final var val = chip.getText().toString().trim();
items.add(val);
}
notifyValueChanged(TextUtils.join("|", items));
}
}
@@ -1,23 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
/**
* @author Akash Yadav
*/
public class FloatEditor extends UnimplementedEditorFragment {}
@@ -1,23 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
/**
* @author Akash Yadav
*/
public class FractionEditor extends UnimplementedEditorFragment {}
@@ -1,23 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
/**
* @author Akash Yadav
*/
public class IntegerEditor extends UnimplementedEditorFragment {}
@@ -1,83 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.app.IDEApplication;
import com.itsaky.androidide.databinding.LayoutReferenceAttrEditorBinding;
import com.itsaky.inflater.values.FrameworkValues;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Akash Yadav
*/
public class ReferenceEditor extends AbstractReferenceEditor {
private LayoutReferenceAttrEditorBinding binding;
@Nullable
@Override
protected View createView(
@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
this.binding = LayoutReferenceAttrEditorBinding.inflate(inflater, container, false);
return this.binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupReferenceInput((MaterialAutoCompleteTextView) this.binding.resInput.getEditText());
}
@Override
protected List<String> computeReferenceItems() {
final var resTable = IDEApplication.getInstance().getResourceTable();
final var list = new ArrayList<>(resTable.listResourceNames(null));
final var tables = ValuesTableFactory.getAllTables();
for (var entry : tables.entrySet()) {
final var resourceMap = entry.getValue().getResourceMap();
for (var resourceEntries : resourceMap.entrySet()) {
final var name = resourceEntries.getKey();
list.addAll(
resourceEntries.getValue().keySet().stream()
.map(("@android:" + name + "/")::concat)
.collect(Collectors.toList()));
}
}
list.addAll(FrameworkValues.listAllResources());
return list;
}
}
@@ -1,90 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.os.Bundle;
import android.text.Editable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.databinding.LayoutStringAttrEditorBinding;
import com.itsaky.androidide.utils.TextWatcherAdapter;
import com.itsaky.inflater.values.FrameworkValues;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Akash Yadav
*/
public class StringEditor extends AbstractReferenceEditor {
private LayoutStringAttrEditorBinding binding;
@Nullable
@Override
protected View createView(
@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
this.binding = LayoutStringAttrEditorBinding.inflate(inflater, container, false);
return this.binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final var stringInput = Objects.requireNonNull(this.binding.stringInput.getEditText());
stringInput.addTextChangedListener(
new TextWatcherAdapter() {
@Override
public void afterTextChanged(@NonNull Editable s) {
notifyValueChanged(s.toString());
}
});
stringInput.setText(this.attribute.getValue());
setupReferenceInput((MaterialAutoCompleteTextView) this.binding.stringResInput.getEditText());
}
@Override
protected List<String> computeReferenceItems() {
final var list = new ArrayList<String>();
final var tables = ValuesTableFactory.getAllTables();
for (var entry : tables.entrySet()) {
final var dimens = entry.getValue().getTable("string");
if (dimens != null) {
list.addAll(dimens.keySet().stream().map("@string/"::concat).collect(Collectors.toSet()));
}
}
list.addAll(
FrameworkValues.listStrings().stream()
.map("@android:string/"::concat)
.collect(Collectors.toList()));
return list;
}
}
@@ -1,34 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
/** @author Akash Yadav */
open class UnimplementedEditorFragment : BaseValueEditorFragment() {
override fun createView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return super.onCreateView(inflater, container, savedInstanceState)
}
}
@@ -1,358 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.sheets;
import static com.itsaky.androidide.resources.R.drawable;
import static com.itsaky.androidide.resources.R.string;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.transition.TransitionManager;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.adapters.SimpleIconTextAdapter;
import com.itsaky.androidide.adapters.XMLAttributeListAdapter;
import com.itsaky.androidide.app.IDEApplication;
import com.itsaky.androidide.databinding.LayoutAttrEditorSheetBinding;
import com.itsaky.androidide.databinding.LayoutAttrEditorSheetItemBinding;
import com.itsaky.androidide.fragments.attr.BaseValueEditorFragment;
import com.itsaky.androidide.models.IconTextListItem;
import com.itsaky.androidide.models.XMLAttribute;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IView;
import com.itsaky.inflater.impl.UiAttribute;
import com.itsaky.toaster.Toaster;
import com.itsaky.toaster.ToastUtilsKt;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class AttrEditorSheet extends BottomSheetDialogFragment
implements SimpleIconTextAdapter.OnBindListener<IconTextListItem>,
Consumer<Attr>,
BaseValueEditorFragment.OnValueChangeListener {
private static final List<IconTextListItem> VIEW_ACTIONS = new ArrayList<>();
private static final ILogger LOG = ILogger.newInstance("AttrBottomSheet");
private AttributeListSheet mAttrListSheet;
private AttrValueEditorSheet mValueEditorSheet;
private IView selectedView;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private File layout;
private LayoutAttrEditorSheetBinding binding;
private OnViewDeletionFailedListener mDeletionFailedListener;
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
this.binding =
LayoutAttrEditorSheetBinding.inflate(LayoutInflater.from(getContext()), container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (VIEW_ACTIONS.isEmpty()) {
Collections.addAll(
VIEW_ACTIONS,
IconTextListItem.create(getString(string.msg_viewaction_add_attr), drawable.ic_add),
IconTextListItem.create(getString(string.title_viewaction_delete), drawable.ic_delete),
IconTextListItem.create(
getString(string.msg_viewaction_select_parent), drawable.ic_view_select_parent));
}
setupViewData();
}
private void setupViewData() {
binding.actionsList.setAdapter(new SimpleIconTextAdapter(VIEW_ACTIONS).setOnBindListener(this));
if (dismissIfSelectedNull()) {
LOG.error("Cannot edit attributes of a null view.");
return;
}
binding.widgetName.setText(this.selectedView.getXmlTag());
binding.attrList.setAdapter(
new XMLAttributeListAdapter(
this.selectedView.getAttributes().stream()
.map(XMLAttribute::new)
.collect(Collectors.toList()),
this::onAttrClick));
}
private void onAttrClick(
LayoutAttrEditorSheetItemBinding binding, @NonNull XMLAttribute attribute) {
if (dismissIfSelectedNull()) {
LOG.error("Cannot edit attributes of a null view.");
return;
}
final var format = attribute.findFormat();
if (format == -1) {
ToastUtilsKt.toast(getString(string.msg_no_attr_format), Toaster.Type.ERROR);
LOG.error(getString(string.msg_no_attr_format), attribute);
return;
}
showValueEditorSheet(attribute);
}
private void showValueEditorSheet(@NonNull XMLAttribute attribute) {
getValueEditorSheet(attribute).show(getChildFragmentManager(), "attr_value_editor_sheet");
}
@NonNull
public AttrValueEditorSheet getValueEditorSheet(@NonNull XMLAttribute attribute) {
if (mValueEditorSheet == null) {
mValueEditorSheet = AttrValueEditorSheet.newInstance(attribute);
}
mValueEditorSheet.setAttribute(attribute);
return mValueEditorSheet;
}
private boolean dismissIfSelectedNull() {
if (selectedView == null) {
dismiss();
return true;
}
return false;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mDeletionFailedListener = (OnViewDeletionFailedListener) context;
}
public AttrEditorSheet setLayout(File layout) {
Objects.requireNonNull(layout);
this.layout = layout;
return this;
}
public void setSelectedView(IView view) {
this.selectedView = view;
}
@Override
public void postBind(
IconTextListItem item, @NonNull SimpleIconTextAdapter.VH holder, int position) {
final var binding = holder.binding;
binding.getRoot().setOnClickListener(v -> onViewActionClick(position));
}
private void onViewActionClick(int position) {
if (dismissIfSelectedNull()) {
return;
}
if (position == 0) { // Add attribute
var tag = this.selectedView.getXmlTag();
if ("include".equals(tag) || "merge".equals(tag)) {
tag = "View";
}
final var attrs = IDEApplication.getInstance().attrInfo();
final var style = attrs.getStyle(tag);
if (style == null) {
LOG.error("Unable to retrieve attributes for tag:", tag);
return;
}
final var attributes = new TreeSet<Attr>(Comparator.comparing(attr -> attr.name));
attributes.addAll(style.attributes);
attributes.addAll(attrs.NO_PARENT.attributes);
final var widgetInfo = IDEApplication.getInstance().widgetInfo();
final var widget = widgetInfo.getWidgetBySimpleName(tag);
if (widget != null) {
for (var superclass : widget.superclasses) {
if ("java.lang.Object".equals(superclass)) {
break;
}
final var simpleName = superclass.substring(superclass.lastIndexOf(".") + 1);
final var superStyle = attrs.getStyle(simpleName);
if (superStyle != null) {
attributes.addAll(superStyle.attributes);
}
}
}
final var parent = this.selectedView.getParent();
if (parent != null) {
final var parentTag = parent.getXmlTag();
final var parentWidget = widgetInfo.getWidgetBySimpleName(parentTag);
if (parentWidget != null) {
final var parentLayoutParams = attrs.getStyle(parentTag + "_Layout");
if (parentLayoutParams != null) {
attributes.addAll(parentLayoutParams.attributes);
}
final var paramSuperclasses = widgetInfo.getLayoutParamSuperClasses(parentWidget.name);
if (paramSuperclasses != null) {
for (var superclass : paramSuperclasses) {
if ("java.lang.Object".equals(superclass)) {
continue;
}
final var split = superclass.split("\\.");
final var simpleClassName = split[split.length - 2];
var paramName = split[split.length - 1];
paramName = paramName.substring(0, paramName.length() - "Params".length());
final var superParamEntry = attrs.getStyle(simpleClassName + "_" + paramName);
if (superParamEntry != null) {
attributes.addAll(superParamEntry.attributes);
}
}
}
}
}
final var sheet = getAttrListSheet();
sheet.setItems(filterAppliedAttributes(attributes));
sheet.show(getChildFragmentManager(), "attr_list_sheet");
} else if (position == 1) { // Delete
DialogUtils.newYesNoDialog(
getContext(),
(dialog, which) -> {
var handled = selectedView.removeFromParent();
if (!handled) {
handled =
mDeletionFailedListener != null
&& mDeletionFailedListener.onDeletionFailed(this.selectedView);
}
if (!handled) {
ToastUtilsKt.toast(getString(string.msg_view_deletion_failed), Toaster.Type.ERROR);
} else {
dismiss();
}
},
(dialog, which) -> dialog.dismiss())
.show();
} else if (position == 2) { // Select parent
if (this.selectedView.getParent() == null) {
ToastUtilsKt.toast(getString(string.msg_no_view_parent), Toaster.Type.ERROR);
return;
}
this.selectedView = this.selectedView.getParent();
TransitionManager.beginDelayedTransition(binding.getRoot());
setupViewData();
}
}
@NonNull
@Contract("_ -> new")
private List<Attr> filterAppliedAttributes(@NonNull TreeSet<Attr> attributes) {
attributes.removeIf(attr -> this.selectedView.hasAttribute(attr.namespace, attr.name));
return new ArrayList<>(attributes);
}
@NonNull
private AttributeListSheet getAttrListSheet() {
if (mAttrListSheet == null) {
mAttrListSheet = new AttributeListSheet();
mAttrListSheet.setCancelable(true);
}
return mAttrListSheet;
}
@Override
public void accept(Attr attr) {
addNewAttribute(attr);
}
private void addNewAttribute(@NonNull Attr attr) {
final XMLAttribute attribute = new XMLAttribute(attr.namespace, attr.name, "", false);
attribute.setAttr(attr);
showValueEditorSheet(attribute);
}
@Override
public void onValueChanged(@NonNull IAttribute attribute, String newValue) {
if (dismissIfSelectedNull()) {
LOG.warn("Cannot update attribute with new value. Selected view is null.");
return;
}
final var attributeName = attribute.getAttributeName();
final var namespace = attribute.getNamespace();
if (this.selectedView.hasAttribute(namespace, attributeName)) {
this.selectedView.updateAttribute(namespace, attributeName, newValue);
} else {
final var attr = new UiAttribute(namespace, attributeName, newValue);
this.selectedView.addAttribute(attr);
}
}
/**
* A listener can be used to get notified when we fail to remove a view from its parent. This is
* used in {@link com.itsaky.androidide.DesignerActivity DesignerActivity}.
*
* <p>When the user tries to remove the outermost view of the inflated layout, AttrEditorSheet
* fails to remove the view from its parent ({@link IView#getParent()} is null for root XML
* layout). In this case, DesignerActivity check if the view that we were trying to delete is the
* root layout or not. If it is the root layout, then it deletes the layout from the layout
* container.
*
* @author Akash Yadav
*/
public interface OnViewDeletionFailedListener {
/**
* @param view The view that was not removed from its parent.
* @return {@code true} if the listener handled the error. {@code false} otherwise.
*/
boolean onDeletionFailed(IView view);
}
}
@@ -1,192 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.sheets;
import static com.itsaky.attrinfo.models.Attr.FORMAT_BOOLEAN;
import static com.itsaky.attrinfo.models.Attr.FORMAT_COLOR;
import static com.itsaky.attrinfo.models.Attr.FORMAT_DIMENSION;
import static com.itsaky.attrinfo.models.Attr.FORMAT_ENUM;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FLAG;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FLOAT;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FRACTION;
import static com.itsaky.attrinfo.models.Attr.FORMAT_INTEGER;
import static com.itsaky.attrinfo.models.Attr.FORMAT_REFERENCE;
import static com.itsaky.attrinfo.models.Attr.FORMAT_STRING;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.adapters.AttrValueFormatTabAdapter;
import com.itsaky.androidide.databinding.LayoutAttrValueEditorBinding;
import com.itsaky.androidide.fragments.attr.BaseValueEditorFragment;
import com.itsaky.androidide.fragments.attr.BooleanEditor;
import com.itsaky.androidide.fragments.attr.ColorEditor;
import com.itsaky.androidide.fragments.attr.DimensionEditor;
import com.itsaky.androidide.fragments.attr.EnumEditor;
import com.itsaky.androidide.fragments.attr.FlagEditor;
import com.itsaky.androidide.fragments.attr.FloatEditor;
import com.itsaky.androidide.fragments.attr.FractionEditor;
import com.itsaky.androidide.fragments.attr.IntegerEditor;
import com.itsaky.androidide.fragments.attr.ReferenceEditor;
import com.itsaky.androidide.fragments.attr.StringEditor;
import com.itsaky.androidide.models.XMLAttribute;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.inflater.IAttribute;
import org.jetbrains.annotations.Contract;
import java.util.Objects;
/**
* Sheet for editing value of an attribute.
*
* @author Akash Yadav
*/
public class AttrValueEditorSheet extends BottomSheetDialogFragment
implements BaseValueEditorFragment.OnValueChangeListener {
private static final String KEY_ATTRIBUTE = "attrEditorSheet_attribute";
private BaseValueEditorFragment.OnValueChangeListener valueChangeListener;
private LayoutAttrValueEditorBinding binding;
private AttrValueFormatTabAdapter adapter;
private XMLAttribute attribute;
@NonNull
public static AttrValueEditorSheet newInstance(final XMLAttribute attribute) {
Objects.requireNonNull(attribute);
final var args = new Bundle();
args.putParcelable(KEY_ATTRIBUTE, attribute);
final var fragment = new AttrValueEditorSheet();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
this.valueChangeListener = (BaseValueEditorFragment.OnValueChangeListener) getParentFragment();
}
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
this.binding = LayoutAttrValueEditorBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (attribute == null) {
this.attribute = requireArguments().getParcelable(KEY_ATTRIBUTE);
}
Objects.requireNonNull(this.attribute);
final var namespace = this.attribute.getNamespace();
final var formatText = Attr.createFormatText(attribute.getFormat());
this.binding.attributeName.setText(
String.format("%s:%s", namespace.getPrefix(), attribute.getAttributeName()));
this.binding.attributeFormat.setText(formatText);
setupTabs(formatText);
}
@SuppressLint("NotifyDataSetChanged")
private void setupTabs(@NonNull String formatText) {
this.binding.tabs.removeAllTabs();
this.binding.pager.setSaveEnabled(false);
if (this.adapter != null) {
this.adapter.removeAll();
this.adapter.notifyDataSetChanged();
}
this.adapter = new AttrValueFormatTabAdapter(getChildFragmentManager(), attribute);
final var formats = formatText.split("\\|");
for (var formatName : formats) {
if (TextUtils.isEmpty(formatName) || formatName.length() < 2) {
continue;
}
final var frag = getFragmentClass(formatName);
if (frag != null) {
this.adapter.addFragment(frag, formatName);
}
}
binding.pager.setAdapter(this.adapter);
binding.tabs.setupWithViewPager(binding.pager);
}
@Nullable
@Contract(pure = true)
private Class<? extends BaseValueEditorFragment> getFragmentClass(@NonNull String name) {
switch (name) {
case FORMAT_REFERENCE:
return ReferenceEditor.class;
case FORMAT_COLOR:
return ColorEditor.class;
case FORMAT_BOOLEAN:
return BooleanEditor.class;
case FORMAT_DIMENSION:
return DimensionEditor.class;
case FORMAT_FLOAT:
return FloatEditor.class;
case FORMAT_INTEGER:
return IntegerEditor.class;
case FORMAT_FRACTION:
return FractionEditor.class;
case FORMAT_STRING:
return StringEditor.class;
case FORMAT_ENUM:
return EnumEditor.class;
case FORMAT_FLAG:
return FlagEditor.class;
}
return null;
}
public void setAttribute(XMLAttribute attribute) {
Objects.requireNonNull(attribute);
this.attribute = attribute;
}
@Override
public void onValueChanged(IAttribute attribute, String newValue) {
if (valueChangeListener != null) {
valueChangeListener.onValueChanged(attribute, newValue);
}
}
}
@@ -1,132 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.sheets;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SearchView;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.adapters.AttrListAdapter;
import com.itsaky.androidide.databinding.LayoutAddAttrSheetBinding;
import com.itsaky.attrinfo.models.Attr;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* A bottom sheet dialog for showing a simple list of text icon pair.
*
* @author Akash Yadav
* @see com.itsaky.androidide.models.IconTextListItem
*/
public class AttributeListSheet extends BottomSheetDialogFragment {
private LayoutAddAttrSheetBinding binding;
private Consumer<Attr> clickConsumer;
private List<Attr> mItems;
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
this.binding = LayoutAddAttrSheetBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.searchBar.setOnQueryTextListener(
new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return filterAttributes(query.trim());
}
@Override
public boolean onQueryTextChange(String newText) {
return filterAttributes(newText.trim());
}
});
binding.attrList.setAdapter(new AttrListAdapter(clickConsumer, mItems));
}
private boolean filterAttributes(@NonNull String query) {
if (mItems == null || TextUtils.isEmpty(query)) {
update();
return true;
}
final var filtered =
this.mItems.stream()
.filter(
attr -> attr.name.toLowerCase(Locale.ROOT).contains(query.toLowerCase(Locale.ROOT)))
.collect(Collectors.toList());
update(filtered);
return true;
}
public void update() {
update(mItems);
}
public void update(List<Attr> items) {
if (binding != null) {
binding.attrList.setAdapter(new AttrListAdapter(clickConsumer, items));
}
}
public void setItems(@Nullable List<Attr> items) {
if (items == null) {
items = new ArrayList<>();
}
mItems = items;
update();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
//noinspection unchecked
this.clickConsumer = (Consumer<Attr>) getParentFragment();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NORMAL, R.style.AppTheme_SheetAboveKeyboard);
}
}
@@ -18,8 +18,8 @@
package com.itsaky.androidide.language.xml
import android.graphics.Color
import android.graphics.Color.parseColor
import com.google.common.collect.EvictingQueue
import com.itsaky.androidide.app.IDEApplication
import com.itsaky.androidide.language.incremental.BaseIncrementalAnalyzeManager
import com.itsaky.androidide.language.incremental.IncrementalToken
import com.itsaky.androidide.language.incremental.LineState
@@ -48,10 +48,10 @@ import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.OPERATOR
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.TEXT_NORMAL
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.XML_TAG
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forComment
import com.itsaky.inflater.util.CommonParseUtils
import io.github.rosemoe.sora.lang.analysis.IncrementalAnalyzeManager.LineTokenizeResult
import io.github.rosemoe.sora.lang.styling.Span
import io.github.rosemoe.sora.lang.styling.TextStyle.makeStyle
import java.util.regex.Pattern
/**
* Syntax analyzer for XML.
@@ -85,7 +85,7 @@ class XMLAnalyzer : BaseIncrementalAnalyzeManager(XMLLexer::class.java) {
override fun popTokensAfterIncomplete(
incompleteToken: IncrementalToken,
tokens: MutableList<IncrementalToken>
tokens: MutableList<IncrementalToken>,
) {
// Do nothing
}
@@ -93,8 +93,6 @@ class XMLAnalyzer : BaseIncrementalAnalyzeManager(XMLLexer::class.java) {
override fun generateSpans(
tokens: LineTokenizeResult<LineState, IncrementalToken>,
): MutableList<Span> {
val app = IDEApplication.getInstance()
val parser = CommonParseUtils(app.resourceTable, app.resources.displayMetrics)
val spans = mutableListOf<Span>()
var previous = XMLLexer.SEA_WS
@@ -109,7 +107,7 @@ class XMLAnalyzer : BaseIncrementalAnalyzeManager(XMLLexer::class.java) {
COMMENT_START,
COMMENT_END,
CommentModeEnd,
CommentText -> spans.add(Span.obtain(offset, forComment()))
CommentText, -> spans.add(Span.obtain(offset, forComment()))
OPEN,
DASH,
NOT,
@@ -126,8 +124,7 @@ class XMLAnalyzer : BaseIncrementalAnalyzeManager(XMLLexer::class.java) {
val text: String = token.text
val textVar = text.replace("\"", "")
if (isColorValue(textVar)) {
val color: Int =
parser.parseColor(textVar, IDEApplication.getInstance().applicationContext)
val color: Int = parseColor(textVar)
val span = Span.obtain(offset + 1, makeStyle(LITERAL))
span.setUnderlineColor(color)
spans.add(span)
@@ -163,8 +160,7 @@ class XMLAnalyzer : BaseIncrementalAnalyzeManager(XMLLexer::class.java) {
try {
val textVar: String = token.text
if (isColorValue(textVar)) {
val color: Int =
parser.parseColor(textVar, IDEApplication.getInstance().applicationContext)
val color: Int = parseColor(textVar)
val span = Span.obtain(offset, makeStyle(TEXT_NORMAL))
span.setUnderlineColor(color)
spans.add(span)
@@ -187,8 +183,7 @@ class XMLAnalyzer : BaseIncrementalAnalyzeManager(XMLLexer::class.java) {
override fun handleIncompleteToken(token: IncrementalToken) {}
private fun isColorValue(value: String): Boolean {
return (value.startsWith("#") ||
value.startsWith("@android:color") ||
value.startsWith("@color/"))
return hexColorMatcher.matcher(value).matches()
}
private val hexColorMatcher: Pattern = Pattern.compile("#[a-fA-F\\d]{6,8}")
}
@@ -1,122 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.models;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.View;
import androidx.annotation.DrawableRes;
import java.util.Objects;
/**
* Holds data about the widget item in widgets list shown in UI Designer screen.
*
* @author Akash Yadav
*/
public class UIWidget implements IconTextListItem, Parcelable {
public static final Creator<UIWidget> CREATOR =
new Creator<UIWidget>() {
@Override
public UIWidget createFromParcel(Parcel in) {
return new UIWidget(in);
}
@Override
public UIWidget[] newArray(int size) {
return new UIWidget[size];
}
};
private final String name;
private final int icon;
private final String clazz;
private UIWidget(Parcel in) {
this.name = in.readString();
this.icon = in.readInt();
this.clazz = in.readString();
}
public UIWidget(String name, @DrawableRes int icon, Class<?> clazz) {
this.name = name;
this.icon = icon;
this.clazz = clazz.getName();
}
@Override
public int hashCode() {
return Objects.hash(getName(), getIcon(), clazz);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UIWidget widget = (UIWidget) o;
return getIcon() == widget.getIcon()
&& getName().equals(widget.getName())
&& clazz.equals(widget.clazz);
}
public String getName() {
return name;
}
public int getIcon() {
return icon;
}
/**
* Get the class of this widget.
*
* @return The class that this widget represents
* @throws ClassNotFoundException If there is no class with this widget's class name. However,
* this will never be thrown.
*/
public Class<? extends View> asClass() throws ClassNotFoundException {
return getClass().getClassLoader().loadClass(this.clazz).asSubclass(View.class);
}
public String getWidgetClassName() {
return this.clazz;
}
@Override
public String getText() {
return getName();
}
@Override
public int getIconResource() {
return getIcon();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(icon);
dest.writeString(clazz);
}
}
@@ -1,165 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.models;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.jetbrains.annotations.Contract;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Model class for holding data about a group that will be displayed in Widgets list in UI Designer
* screen.
*
* <p>A UIWidgetGroup contains multiple {@link UIWidget}.
*
* @author Akash Yadav
*/
public class UIWidgetGroup implements IconTextListItem, Parcelable {
public static final Creator<UIWidgetGroup> CREATOR =
new Creator<UIWidgetGroup>() {
@NonNull
@Contract("_ -> new")
@Override
public UIWidgetGroup createFromParcel(Parcel in) {
return new UIWidgetGroup(in);
}
@NonNull
@Contract(value = "_ -> new", pure = true)
@Override
public UIWidgetGroup[] newArray(int size) {
return new UIWidgetGroup[size];
}
};
/** Name of this group. */
private final String name;
/** Children of this group. */
private final List<UIWidget> children;
/** Is this group currently selected in the group list? */
private boolean selected;
public UIWidgetGroup(String name) {
this.name = name;
this.selected = false;
this.children = new ArrayList<>();
}
protected UIWidgetGroup(@NonNull Parcel in) {
this.name = in.readString();
this.children = in.createTypedArrayList(UIWidget.CREATOR);
this.selected = in.readByte() != 0;
}
/**
* Add this widget to this group. Does nothing if the child is already present.
*
* @param child The child to add.
* @return This instance for chained method calls.
*/
public UIWidgetGroup addChild(UIWidget child) {
if (children.contains(child)) {
return this;
}
this.children.add(child);
return this;
}
/**
* Get the child at the given index.
*
* @param index The index of the child.
* @return The child at the index.
* @throws IndexOutOfBoundsException If the given index is not valid.
*/
public UIWidget getChild(int index) {
return children.get(index);
}
@Override
public int hashCode() {
return Objects.hash(getName(), getChildren(), isSelected());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UIWidgetGroup group = (UIWidgetGroup) o;
return isSelected() == group.isSelected()
&& getName().equals(group.getName())
&& getChildren().equals(group.getChildren());
}
/**
* Get the name of this group.
*
* @return The name of this group.
*/
public String getName() {
return name;
}
/**
* Get the children of this group.
*
* @return The children.
*/
@NonNull
public List<UIWidget> getChildren() {
return children;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public String getText() {
return getName();
}
@Override
public int getIconResource() {
return -1;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(name);
dest.writeTypedList(children);
dest.writeByte((byte) (selected ? 1 : 0));
}
}
@@ -1,178 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.models;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import com.itsaky.androidide.app.IDEApplication;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.impl.UiAttribute;
import com.itsaky.xml.INamespace;
import org.jetbrains.annotations.Contract;
import java.util.Objects;
/**
* A model that is used to show attributes list of a view.
*
* @author Akash Yadav
*/
public class XMLAttribute extends UiAttribute implements Parcelable {
public static final Creator<XMLAttribute> CREATOR =
new Creator<>() {
@NonNull
@Contract("_ -> new")
@Override
public XMLAttribute createFromParcel(Parcel in) {
return new XMLAttribute(in);
}
@NonNull
@Contract(value = "_ -> new", pure = true)
@Override
public XMLAttribute[] newArray(int size) {
return new XMLAttribute[size];
}
};
public Attr attr;
/** Is this attribute applied to any view? */
private boolean isApplied;
/**
* Format of this attribute. May be stored here so we don't have to look in {@link
* com.itsaky.attrinfo.AttrInfo AttrInfo} again and again.
*/
private int format = -1;
public XMLAttribute(IAttribute attribute) {
this(attribute, true);
}
public XMLAttribute(@NonNull IAttribute attribute, boolean isApplied) {
this(attribute.getNamespace(), attribute.getAttributeName(), attribute.getValue(), isApplied);
}
public XMLAttribute(INamespace namespace, String name, String value, boolean isApplied) {
super(namespace, name, value);
this.isApplied = isApplied;
}
private XMLAttribute(@NonNull Parcel in) {
this(
in.readParcelable(INamespace.class.getClassLoader()),
in.readString(),
in.readString(),
in.readByte() != 0);
this.attr = in.readParcelable(Attr.class.getClassLoader());
this.format = in.readInt();
}
/**
* Create a new attribute instance with the android namespace. {@link #isApplied()} will return
* true by default.
*
* @param name The name of the attribute.
* @param value The value of the attribute.
* @return A new instance of this class with the provided data.
*/
@NonNull
@Contract("_, _ -> new")
public static XMLAttribute newAndroidAttribute(String name, String value) {
return new XMLAttribute(INamespace.ANDROID, name, value, true);
}
/**
* Create a new attribute instance with the android namespace. The {@link #isApplied()} will
* return {@code false} by default.
*
* @param name The name of the attribute.
* @return The new attribute instance.
*/
@NonNull
@Contract("_ -> new")
public static XMLAttribute newAndroidAttribute(String name) {
return new XMLAttribute(INamespace.ANDROID, name, "", false);
}
public int findFormat() {
if (getFormat() > 0) {
return getFormat();
}
final var attrs = IDEApplication.getInstance().attrInfo();
this.attr = attrs.getAttribute(getAttributeName());
if (this.attr != null) {
this.format = this.attr.format;
}
return getFormat();
}
public int getFormat() {
return format;
}
public void setFormat(int format) {
this.format = format;
}
public boolean hasFormat(int format) {
return (this.format & format) != 0;
}
@Override
public void apply(String value) {
super.apply(value);
isApplied = true;
}
public boolean isApplied() {
return isApplied;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(getNamespace(), flags);
dest.writeString(getAttributeName());
dest.writeString(getValue());
dest.writeByte((byte) (isApplied ? 1 : 0));
dest.writeParcelable(getAttr(), flags);
dest.writeInt(this.format);
}
public Attr getAttr() {
return attr;
}
public void setAttr(Attr attr) {
Objects.requireNonNull(attr);
this.attr = attr;
setFormat(attr.format);
}
}
@@ -1,312 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.projects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.io.File;
import java.io.FileFilter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
public class ProjectResourceTable implements IResourceTable {
private static final ILogger LOG = ILogger.newInstance("ProjectResourceFinder");
private File resDir;
private File color;
private File layout;
private File[] drawables;
private File[] mipmaps;
@Override
public File findDrawable(@NonNull String name) {
for (File drawable : drawables) {
final File file = findFileWithName(drawable.listFiles(), name);
if (file != null) {
return file;
}
}
return null;
}
@Override
public File findLayout(@NonNull String name) {
return findFileWithName(this.layout.listFiles(), name);
}
@Override
public String findString(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findString(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@string/")) {
var ref = value.substring("@string/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@Override
public String findColor(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findColor(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@color/")) {
var ref = value.substring("@color/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@Override
public String[] findArray(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return new String[] {name};
}
final var resource = table.findArray(name);
if (resource == null) {
return new String[] {name};
}
return resource.getValues();
}
@Override
public String findDimension(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findDimension(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@dimen/")) {
var ref = value.substring("@dimen/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@Override
public String findBoolean(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findBoolean(name);
if (resource == null) {
return name;
}
final var val = resource.getValue();
if (val.startsWith("@bool/")) {
final var ref = val.substring("@bool/".length());
if (ref.equals(name)) {
// recursive reference
return name;
}
}
return val;
}
@Override
public String findInteger(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findInteger(name);
if (resource == null) {
return name;
}
final var val = resource.getValue();
if (val.startsWith("@integer/")) {
final var ref = val.substring("@integer/".length());
if (ref.equals(name)) {
// recursive reference
return name;
}
}
return val;
}
@Override
public void setInflatingFile(@NonNull File file) {
if (file.getParentFile() == null) {
throw new IllegalArgumentException("Invalid inflating file");
}
setupDirectories(
file // layout file
.getParentFile() // layout dir
.getParentFile()); // res dir
}
@NonNull
@Override
public Collection<String> listResourceNames(@Nullable String type) {
if (type == null) {
return listAllResourceNames();
} else {
final var set = new HashSet<String>();
switch (type) {
case "drawable":
for (var folder : this.drawables) {
listAndAddTo(set, folder, type);
}
break;
case "layout":
listAndAddTo(set, this.layout, type);
break;
case "color":
listAndAddTo(set, this.color, type);
break;
case "mipmap":
for (var folder : this.mipmaps) {
listAndAddTo(set, folder, type);
}
break;
}
return set;
}
}
private void setupDirectories(File resDir) {
if (resDir == null) {
LOG.error("Null resource directory passed to resource finder. Ignoring.");
return;
}
this.resDir = resDir;
this.color = new File(resDir, "color");
this.layout = new File(resDir, "layout");
final File[] drawables = resDir.listFiles(new NameStartsWith("drawable"));
this.drawables = drawables == null ? new File[0] : drawables;
final File[] mipmaps = resDir.listFiles(new NameStartsWith("mipmap"));
this.mipmaps = mipmaps == null ? new File[0] : mipmaps;
}
private File findFileWithName(File[] files, String name) {
if (files == null || name == null) {
return null;
}
for (File file : files) {
var simpleName = file.getName();
if (simpleName.startsWith(name)) {
return file;
}
}
return null;
}
private void listAndAddTo(HashSet<String> set, @NonNull File dir, String type) {
final var files = dir.listFiles();
if (files == null) {
return;
}
for (var file : files) {
var name = file.getName();
if (!name.contains(".")) {
continue;
}
name = name.substring(0, name.lastIndexOf('.'));
name = String.format(Locale.ROOT, "@%s/%s", type, name);
set.add(name);
}
}
@NonNull
private Collection<String> listAllResourceNames() {
final var set = new HashSet<>(listResourceNames("color"));
set.addAll(listResourceNames("drawable"));
set.addAll(listResourceNames("mipmap"));
set.addAll(listResourceNames("layout"));
return set;
}
private static class NameStartsWith implements FileFilter {
private final String name;
public NameStartsWith(String name) {
this.name = name;
}
@Override
public boolean accept(@NonNull File file) {
return file.getName().startsWith(name);
}
}
}
@@ -1,55 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.ui;
import com.itsaky.androidide.models.UIWidget;
import com.itsaky.inflater.IView;
/**
* Store data about a draggable widget. Used in UI Designer.
*
* @author Akash Yadav
*/
public class WidgetDragData {
/**
* Set this to true if the view that is being dragged was already inflated in the layout. If this
* is set to {@code true}, {@link #alreadyInflatedView} must NOT be {@code null}.
*
* <p>If this is {@code false}, then {@link #newDragData} must be not be null.
*/
public boolean isAlreadyInflated;
/**
* The view that was already inflated and is being relocated. This must not be null if {@link
* #isAlreadyInflated} is {@code true}.
*/
public IView alreadyInflatedView;
/** The UIWidget that is being dragged. */
public UIWidget newDragData;
private WidgetDragData() {}
public WidgetDragData(
boolean isAlreadyInflated, IView alreadyInflatedView, UIWidget newDragData) {
this.isAlreadyInflated = isAlreadyInflated;
this.alreadyInflatedView = alreadyInflatedView;
this.newDragData = newDragData;
}
}
@@ -1,226 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.ui;
import android.content.Context;
import android.graphics.RectF;
import android.view.DragEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.DesignerActivity;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.models.UIWidget;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IView;
import com.itsaky.inflater.IViewGroup;
import com.itsaky.inflater.impl.ErrorUiView;
import com.itsaky.inflater.impl.UiAttribute;
import com.itsaky.inflater.impl.UiView;
import com.itsaky.inflater.impl.UiViewGroup;
import com.itsaky.xml.INamespace;
import org.jetbrains.annotations.Contract;
/**
* Common drag listener for handling drag events in all ViewGroups in the UI Designer.
*
* @author Akash Yadav
*/
public class WidgetDragListener implements View.OnDragListener {
public static final String ANDROID_NS = "android";
private static final ILogger LOG = ILogger.newInstance("WidgetDragListener");
private final IView placeholder;
private final IViewGroup viewGroup;
private final OnViewAddedListener addedListener;
private final int PLACEHOLDER_HEIGHT = 20; // in dp
private final int PLACEHOLDER_WIDTH = 40; // in dp
public WidgetDragListener(
@NonNull Context context, IViewGroup viewGroup, OnViewAddedListener addedListener) {
this.placeholder = new UiView("android.view.View", new View(context), true);
this.viewGroup = viewGroup;
this.addedListener = addedListener;
this.placeholder.asView().setBackgroundResource(R.drawable.bg_widget_drag_placeholder);
this.placeholder
.asView()
.setLayoutParams(
new ViewGroup.LayoutParams(
SizeUtils.dp2px(PLACEHOLDER_WIDTH), SizeUtils.dp2px(PLACEHOLDER_HEIGHT)));
}
@Override
public boolean onDrag(View v, @NonNull DragEvent event) {
if (this.viewGroup instanceof ErrorUiView) {
return false;
}
final var action = event.getAction();
final var clipDesc = event.getClipDescription();
final var dragData = (WidgetDragData) event.getLocalState();
var handled = true;
switch (action) {
case DragEvent.ACTION_DRAG_STARTED:
handled = clipDesc.hasMimeType(DesignerActivity.DRAGGING_WIDGET_MIME);
break;
case DragEvent.ACTION_DRAG_ENTERED:
case DragEvent.ACTION_DRAG_LOCATION:
this.placeholder.removeFromParent(); // If placeholder is added in another group, remove it.
this.viewGroup.addView(this.placeholder, findPlaceHolderIndex(event.getX(), event.getY()));
break;
case DragEvent.ACTION_DRAG_EXITED:
this.placeholder.removeFromParent();
break;
case DragEvent.ACTION_DROP:
final var index = this.viewGroup.indexOfChild(this.placeholder);
this.placeholder.removeFromParent();
var view =
dragData.isAlreadyInflated
? dragData.alreadyInflatedView
: createView(dragData.newDragData);
if (!dragData.isAlreadyInflated) {
applyBasicAttributes(view);
}
// Remove from parent if it has a parent
view.removeFromParent();
this.viewGroup.addView(view, index);
if (this.addedListener != null) {
this.addedListener.onViewAdded(view);
}
break;
}
return handled;
}
private void applyBasicAttributes(@NonNull IView view) {
view.addAttribute(layoutWidthAttr());
view.addAttribute(layoutHeightAttr());
}
@NonNull
@Contract(" -> new")
private IAttribute layoutHeightAttr() {
return new UiAttribute(INamespace.ANDROID, "layout_height", "wrap_content");
}
@NonNull
@Contract(" -> new")
private IAttribute layoutWidthAttr() {
return new UiAttribute(INamespace.ANDROID, "layout_width", "wrap_content");
}
/**
* Finds the index at which the placeholder will be added in the current view group.
*
* @param x The x coordinate of the touch event
* @param y The y coordinate of the touch event
* @return The index at which the placeholder of the view should be added. The returned index is
* always valid index in the {@link #viewGroup}.
*/
private int findPlaceHolderIndex(final float x, final float y) {
final var count = this.viewGroup.getChildCount();
for (int i = 0; i < this.viewGroup.getChildCount(); i++) {
final var child = this.viewGroup.getChildAt(i);
final var rect = getViewRect(child);
if (rect.contains(x, y)) {
final var top = topHalf(rect);
final var bottom = bottomHalf(rect);
if (top.contains(x, y)) {
return Math.max(0, i - 1);
} else if (bottom.contains(x, y)) {
return Math.min(count, i + 1);
}
}
}
// If we don't find a suitable index, return the last index
return count;
}
@NonNull
private RectF getViewRect(@NonNull IView view) {
final var v = view.asView();
final var rect = new RectF();
rect.left = v.getLeft();
rect.top = v.getTop();
rect.right = rect.left + v.getWidth();
rect.bottom = rect.top + v.getHeight();
return rect;
}
private RectF topHalf(RectF src) {
final RectF result = new RectF(src);
result.bottom -= result.height() / 2;
return src;
}
private RectF bottomHalf(RectF src) {
final RectF result = new RectF(src);
result.top += result.height() / 2;
return src;
}
@NonNull
private IView createView(@NonNull UIWidget widget) {
try {
final var clazz = widget.asClass();
final var constructor = clazz.getConstructor(Context.class);
constructor.setAccessible(true);
final var instance = constructor.newInstance(this.viewGroup.asView().getContext());
if (instance instanceof ViewGroup) {
return new UiViewGroup(clazz.getName(), (ViewGroup) instance);
} else {
return new UiView(clazz.getName(), instance);
}
} catch (Throwable e) {
LOG.error("Unable to add widget", e);
throw new RuntimeException("Unable to add widget", e);
}
}
private RectF leftHalf(RectF src) {
final RectF result = new RectF(src);
result.right -= result.width() / 2;
return src;
}
private RectF rightHalf(RectF src) {
final RectF result = new RectF(src);
result.left += result.width() / 2;
return src;
}
public static interface OnViewAddedListener {
void onViewAdded(IView view);
}
}
@@ -1,42 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.ui;
import android.graphics.Point;
import android.view.View;
public class WidgetDragShadowBuilder extends View.DragShadowBuilder {
private final double TOUCH_Y_OFFSET = 0.3;
public WidgetDragShadowBuilder(View view) {
super(view);
}
@Override
public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) {
if (getView() != null) {
final var width = getView().getWidth();
final var height = getView().getHeight();
outShadowSize.set(width, height);
outShadowTouchPoint.set(width / 2, height + (int) (height * TOUCH_Y_OFFSET));
} else {
super.onProvideShadowMetrics(outShadowSize, outShadowTouchPoint);
}
}
}
@@ -1,110 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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.inflater.IView;
/**
* A touch listener for handling touch events of a view in the UI Designer.
*
* <p>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) {
return mGestureDetector.onTouchEvent(motionEvent);
}
// 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.mView.getParent() == null) {
// This is the root layout so this should not be draggable
return;
}
if (this.longClickListener != null && this.longClickListener.onLongClick(this.mView)) {
this.mView.asView().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
}
@Override
public boolean onDown(MotionEvent e) {
return true; // We would like to get notified about further events.
}
/**
* 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);
}
}
@@ -1,58 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.ui.edit;
import com.itsaky.androidide.app.IDEApplication;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.values.ValuesTable;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.io.File;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Base class for attribute edit handlers.
*
* @author Akash Yadav
*/
public abstract class BaseSuggestionProvider implements IValueSuggestionProvider {
protected final File resDir;
protected final File layout;
protected BaseSuggestionProvider(File layout) {
Objects.requireNonNull(layout);
final var path = layout.getAbsolutePath();
if (!Pattern.compile(".*/src/.*/res/layout/(\\w|_)+\\.xml").matcher(path).matches()) {
throw new IllegalArgumentException("Given layout file is not valid");
}
this.layout = layout;
this.resDir = Objects.requireNonNull(layout.getParentFile()).getParentFile();
Objects.requireNonNull(this.resDir);
}
protected ValuesTable getValuesTable() {
return ValuesTableFactory.getTable(this.resDir);
}
protected IResourceTable getResourceTable() {
return IDEApplication.getInstance().getResourceTable();
}
}
@@ -1,88 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.ui.edit;
import androidx.annotation.NonNull;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.inflater.IAttribute;
import com.itsaky.androidide.lsp.util.StringUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Handles editing dimension attributes.
*
* @author Akash Yadav
*/
public class DimensionSuggestionProvider extends BaseSuggestionProvider {
/** The dimension units that will be suggested. */
private final List<String> dimensions = new ArrayList<>();
public DimensionSuggestionProvider(File layout) {
super(layout);
this.dimensions.add("dp");
this.dimensions.add("sp");
this.dimensions.add("px");
this.dimensions.add("pt");
this.dimensions.add("in");
this.dimensions.add("mm");
}
@Override
public boolean checkFormat(int format) {
return (format & Attr.DIMENSION) != 0;
}
@Override
public @NonNull List<String> suggest(IAttribute attribute, @NonNull String prefix) {
final var list = new ArrayList<String>();
if (prefix.length() > 0 && Character.isDigit(prefix.charAt(0))) {
// Fixed dimension values
// e.g. 10dp
var val = extractDimensionVal(prefix);
for (var unit : this.dimensions) {
final var dimension = val + unit;
if (StringUtils.matchesFuzzy(dimension, prefix, true)) {
list.add(dimension);
}
}
}
return list;
}
private int extractDimensionVal(@NonNull String prefix) {
StringBuilder s = new StringBuilder();
var i = 0;
while (i < prefix.length()) {
if (Character.isDigit(prefix.charAt(i))) {
s.append(prefix.charAt(i));
} else {
break;
}
i++;
}
return Integer.parseInt(s.toString());
}
}
@@ -1,51 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.ui.edit;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import java.util.List;
/**
* An attribute value provider provides suggestions while editing an attribute value.
*
* @author Akash Yadav
*/
public interface IValueSuggestionProvider {
/**
* Check if the given format is supported by this provider.
*
* @param format The formats to check.
* @return <code>true</code> if the given formats contain a supported format, <code>false</code>
* otherwise.
*/
boolean checkFormat(int format);
/**
* Provide suggestions for the given attribute.
*
* @param attribute The attribute to provide suggestions for.
* @param prefix The prefix (partial identifier) to filter the suggestions.
* @return The suggestion items. Must not be <code>null</code>.
*/
@NonNull
List<String> suggest(IAttribute attribute, String prefix);
}
@@ -24,9 +24,9 @@ import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FLA
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FLAG_WS_TRAILING;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FONT_LIGATURES;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.FONT_SIZE;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.USE_CUSTOM_FONT;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.USE_ICU;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.USE_MAGNIFER;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.USE_CUSTOM_FONT;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.WORD_WRAP;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDrawEmptyLineWs;
import static com.itsaky.androidide.preferences.internal.EditorPreferencesKt.getDrawInnerWs;
@@ -70,7 +70,6 @@ import com.itsaky.androidide.utils.FileUtil;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.utils.TypefaceUtilsKt;
import com.itsaky.inflater.values.ValuesTableFactory;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -78,7 +77,6 @@ import org.greenrobot.eventbus.ThreadMode;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import io.github.rosemoe.sora.lang.EmptyLanguage;
@@ -385,9 +383,6 @@ public class CodeEditorView extends LinearLayout {
private void notifySaved() {
binding.editor.dispatchDocumentSaveEvent();
if (Objects.requireNonNull(getFile()).getName().endsWith(".xml")) {
ValuesTableFactory.syncWithFile(getFile());
}
}
public LayoutCodeEditorBinding getBinding() {
@@ -422,7 +417,7 @@ public class CodeEditorView extends LinearLayout {
LOG.warn("Editor layout is null content=" + binding + ", searchLayout=" + searchLayout);
return;
}
searchLayout.beginSearchMode();
}
@@ -1,100 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
tools:context=".DesignerActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimaryDark"
android:gravity="center"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primaryDarkColor"
app:title="@string/app_name" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/label_ui_designer_content"
android:fillViewport="true"
android:paddingStart="16dp"
android:paddingTop="32dp"
android:paddingEnd="16dp"
android:paddingBottom="32dp"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<com.itsaky.androidide.views.DesignerLayoutContainer
android:id="@+id/layout_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_design_container_background"
android:orientation="vertical" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/navigation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true">
<LinearLayout
android:id="@+id/navigation_contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="2.0">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/group_items"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.7"
android:paddingStart="8dp"
android:paddingTop="16dp"
android:paddingEnd="0dp"
android:paddingBottom="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="1"
tools:listitem="@layout/layout_simple_icon_text" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/widget_items"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.3"
android:background="@color/bg_widget_selected"
android:paddingStart="0dp"
android:paddingTop="16dp"
android:paddingEnd="8dp"
android:paddingBottom="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="3"
tools:listitem="@layout/layout_simple_icon_text" />
</LinearLayout>
</com.google.android.material.navigation.NavigationView>
</androidx.drawerlayout.widget.DrawerLayout>
@@ -19,14 +19,17 @@ package com.itsaky.androidide.aaptcompiler
import com.android.aaptcompiler.BlameLogger
import com.android.aaptcompiler.ResourceCompilerOptions
import com.android.aaptcompiler.ResourceFile
import com.android.aaptcompiler.ResourceName
import com.android.aaptcompiler.XmlProcessor
import com.android.aaptcompiler.compileResource
import com.android.aaptcompiler.extractPathData
import com.android.utils.StdLogger
import com.android.utils.StdLogger.Level.VERBOSE
import java.nio.file.Paths
import kotlin.io.path.absolute
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.robolectric.RobolectricTestRunner
/** @author Akash Yadav */
@@ -45,4 +48,19 @@ class CompilerTest {
compileResource(input, output, ResourceCompilerOptions(), BlameLogger(StdLogger(VERBOSE)))
println("Compiled in ${System.nanoTime() - start} nano seconds")
}
@Test
fun `test xml processor`() {
val input =
Paths.get(".", "src/test/resources/layout/activity_editor.xml").absolute().normalize().toFile()
val pathData = extractPathData(input, input.absolutePath)
val resFile = ResourceFile(
ResourceName("", pathData.type!!, pathData.name),
pathData.config,
pathData.source,
ResourceFile.Type.ProtoXml
)
val processor = XmlProcessor(pathData.source, BlameLogger(StdLogger(VERBOSE)))
processor.process(resFile, input.inputStream().buffered())
}
}
@@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<com.itsaky.androidide.views.ContentTranslatingDrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:aapt="http://schemas.android.com/aapt"
android:id="@+id/editor_drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
tools:ignore="DuplicateIncludedIds">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/realContainer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<aapt:attr name="android:background">
<shape><solid android:color="#ffffff"/></shape>
</aapt:attr>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/editor_appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryDarkColor"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/editor_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="10dp" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/build_progress_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
app:indicatorColor="@color/secondaryColor" />
<com.itsaky.androidide.views.MaterialBanner
android:id="@+id/syncBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryDarkColor"
app:tabGravity="start"
style="@style/AppTheme.TabLayout"/>
</com.google.android.material.appbar.AppBarLayout>
<ViewFlipper
android:id="@+id/view_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/content_background"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<!--Views in this flipper are dynamically added and must be instances of CodeEditorView-->
<ViewFlipper
android:id="@+id/editor_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/editor_sheet_peek_height"
android:background="@color/content_background" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/no_editor_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="@color/secondaryTextColor"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4" />
<TextView
android:id="@+id/no_editor_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/secondaryTextColor_light"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="@id/no_editor_title"
app:layout_constraintStart_toStartOf="@id/no_editor_title"
app:layout_constraintTop_toBottomOf="@+id/no_editor_title"
tools:text="Open right drawer for files\nSwipe from bottom for build output" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ViewFlipper>
<com.itsaky.androidide.views.EditorBottomSheet
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:behavior_hideable="false"
app:behavior_peekHeight="@dimen/editor_sheet_peek_height"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
<include
android:id="@+id/diagnosticInfo"
layout="@layout/layout_diagnostic_info" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/startNav"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/layout_nav_header"
app:itemIconTint="@color/primaryIconColor"
app:itemTextColor="@color/primaryTextColor"
app:menu="@menu/menu_editor_nav" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/endNav"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:fitsSystemWindows="false">
<!-- When changing android:tag here, also change it in FileTreeFragment.kt -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/file_tree"
android:name="com.itsaky.androidide.fragments.FileTreeFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="editor.fileTree" />
</com.google.android.material.navigation.NavigationView>
</com.itsaky.androidide.views.ContentTranslatingDrawerLayout>
@@ -14,18 +14,21 @@
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.values.models;
import androidx.annotation.NonNull;
package com.itsaky.androidide.utils
/**
* Represents a dimension value.
*
* @author Akash Yadav
*/
public class DimensionResource extends AbstractResource {
import android.content.Context
import android.content.res.Resources.Theme
import android.util.TypedValue
public DimensionResource(@NonNull String name, @NonNull String value) {
super(name, value);
}
@JvmOverloads
fun Context.resolveAttr(id: Int, resolveRefs: Boolean = true): Int {
return theme.resolveAttr(id)
}
@JvmOverloads
fun Theme.resolveAttr(id: Int, resolveRefs: Boolean = true): Int =
TypedValue().let {
resolveAttribute(id, it, resolveRefs)
it.data
}
+1 -1
View File
@@ -69,7 +69,7 @@ dependencies {
api project (':lsp:models')
api project (':preferences')
compileOnly project (':xml-inflater')
compileOnly project (':sdk-info')
compileOnly project (':actions')
compileOnly project (':common')
}
@@ -42,7 +42,6 @@ import com.itsaky.androidide.lsp.xml.providers.CodeFormatProvider;
import com.itsaky.androidide.lsp.xml.providers.XmlCompletionProvider;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.api.Project;
import com.itsaky.sdk.SDKInfo;
import org.jetbrains.annotations.Nullable;
@@ -59,12 +58,11 @@ public class XMLLanguageServer implements ILanguageServer {
public static final String SERVER_ID = "xml";
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
private ILanguageClient client;
private IServerSettings settings;
public XMLLanguageServer() {}
@Override
public String getServerId() {
@@ -1,186 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.apiinfo;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import com.itsaky.apiinfo.models.ClassInfo;
import com.itsaky.apiinfo.models.FieldInfo;
import com.itsaky.apiinfo.models.MethodInfo;
import com.itsaky.androidide.resources.R;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class ApiInfo {
public static final String NAME = "name";
public static final String SINCE = "since";
public static final String DEPRECATED = "deprecated";
public static final String REMOVED = "removed";
public static final String EXTENDS = "extends";
public static final String IMPLEMENTS = "implements";
public static final String FIELD = "field";
public static final String METHOD = "method";
private final Map<String, ClassInfo> classInfos;
private boolean read = false;
public ApiInfo(@NonNull final Resources resources) throws Exception {
this.classInfos = new HashMap<>();
doRead(resources);
}
public void doRead(Resources resources) throws Exception {
final InputStream in = resources.openRawResource(R.raw.versions_xml);
if (in == null) {
throw new Resources.NotFoundException("Cannot find versions.xml");
}
Document doc = Jsoup.parse(readStream(in));
Elements classes = doc.getElementsByTag("api").first().getElementsByTag("class");
if (classes.size() <= 0) {
return;
}
for (Element clazz : classes) {
ClassInfo info = parseClassInfo(clazz);
if (info != null) classInfos.put(info.name, info);
}
read = true;
}
private String readStream(InputStream in) throws IOException {
final StringBuilder sb = new StringBuilder();
final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line.trim());
sb.append("\n");
}
return sb.substring(sb.indexOf("<api"));
}
private ClassInfo parseClassInfo(Element clazz) {
if (!clazz.hasAttr(NAME)) return null;
ClassInfo info = new ClassInfo();
info.name = clazz.attr(NAME).replace("/", ".").replace("$", ".");
if (clazz.hasAttr(SINCE)) {
info.since = Integer.parseInt(clazz.attr(SINCE));
}
if (clazz.hasAttr(DEPRECATED)) {
info.deprecated = Integer.parseInt(clazz.attr(DEPRECATED));
}
if (clazz.hasAttr(REMOVED)) {
info.removed = Integer.parseInt(clazz.attr(REMOVED));
}
Elements superClass = clazz.getElementsByTag(EXTENDS);
if (superClass.size() == 1) {
info.superClass = superClass.first().attr(NAME);
}
Elements interfaces = clazz.getElementsByTag(IMPLEMENTS);
if (interfaces.size() > 0) {
for (Element iface : interfaces) {
info.interfaces.add(iface.attr(NAME));
}
}
Elements fields = clazz.getElementsByTag(FIELD);
if (fields.size() > 0) {
for (Element field : fields) {
putField(field, info);
}
}
Elements methods = clazz.getElementsByTag(METHOD);
if (methods.size() > 0) {
for (Element method : methods) {
putMethod(method, info);
}
}
return info;
}
private void putField(Element field, ClassInfo clazz) {
if (!field.hasAttr(NAME)) return;
FieldInfo info = new FieldInfo();
info.name = field.attr(NAME);
if (field.hasAttr(SINCE)) {
info.since = Integer.parseInt(field.attr(SINCE));
}
if (field.hasAttr(DEPRECATED)) {
info.deprecated = Integer.parseInt(field.attr(DEPRECATED));
}
if (field.hasAttr(REMOVED)) {
info.removed = Integer.parseInt(field.attr(REMOVED));
}
clazz.fields.put(info.name, info);
}
private void putMethod(Element method, ClassInfo clazz) {
if (!method.hasAttr(NAME)) return;
MethodInfo info = new MethodInfo();
info.name = method.attr(NAME);
info.simpleName = info.name.substring(0, info.name.indexOf("("));
if (method.hasAttr(SINCE)) {
info.since = Integer.parseInt(method.attr(SINCE));
}
if (method.hasAttr(DEPRECATED)) {
info.deprecated = Integer.parseInt(method.attr(DEPRECATED));
}
if (method.hasAttr(REMOVED)) {
info.removed = Integer.parseInt(method.attr(REMOVED));
}
clazz.methods.put(info.name, info);
}
public boolean hasRead() {
return read;
}
public ClassInfo getClassByName(String qualifiedName) {
return classInfos.get(qualifiedName);
}
}
@@ -1,88 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.apiinfo.models;
import android.text.TextUtils;
import org.eclipse.jdt.core.Signature;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Holds information about a class */
public class ClassInfo extends Info {
/** Interfaces that this class implements */
public final List<String> interfaces = new ArrayList<>();
/** Fields of this class */
public final Map<String, FieldInfo> fields = new HashMap<>();
/** Methods of this class */
public final Map<String, MethodInfo> methods = new HashMap<>();
/** Superclass of this class */
public String superClass;
/**
* Get the field by its simple name
*
* @param name Name of the field to find
*/
public FieldInfo getFieldByName(String name) {
return fields.get(name);
}
/**
* Get method by its name and parameter types
*
* @param name Name of the method to find
* @param parameters Parameter types of the method
*/
public MethodInfo getMethod(String name, String[] parameters) {
// Convert parameter types to their binary version
final String[] parameterTypes = new String[parameters.length];
for (int i = 0; i < parameters.length; i++) {
String paramType = parameters[i];
String binaryParamType = Signature.createTypeSignature(paramType, true /* isResolved */);
parameterTypes[i] = binaryParamType;
}
for (Map.Entry<String, MethodInfo> entry : methods.entrySet()) {
MethodInfo method = entry.getValue();
// Check if this method's simple name is what we are looking for
if (method != null && method.simpleName.trim().equals(name.trim())) {
// Get parameter types of this method
// Takes binary representation, returns binary representation
String[] paramTypes = Signature.getParameterTypes(method.name);
// Join the parameters to check if they are equal This is just more efficient
final String params1 = TextUtils.join(",", paramTypes);
final String params2 = TextUtils.join(",", parameterTypes);
// We found what we were looking for!
if (params1.equals(params2)) return method;
}
}
return null;
}
}
@@ -1,27 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.apiinfo.models;
/**
* Holds information about a field
*
* <p>No difference as compared to Info
*/
public class FieldInfo extends Info {}
@@ -1,36 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.apiinfo.models;
/** Base class for information about a class, method, field */
public abstract class Info {
/** Name of this class, method or field */
public String name = "";
/** API level in which this info was deprecated */
public int deprecated = -1;
/** API level in which this was added */
public int since = -1;
/** API level in which this was removed */
public int removed = -1;
}
@@ -1,35 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.apiinfo.models;
import java.util.ArrayList;
import java.util.List;
/** Holds information about a method */
public class MethodInfo extends Info {
/** Parameter types of this method */
public final List<String> parameters = new ArrayList<>();
/**
* In case of a method, the {@link com.itsaky.apiinfo.models.Info#name} contains signature of the
* method This field contains the actual simple name
*/
public String simpleName;
}
@@ -1,229 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.attrinfo;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.attrinfo.models.Styleable;
import com.itsaky.androidide.resources.R;
import com.itsaky.xml.INamespace;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
/**
* A parser for parsing <b>attrs.xml</b>. This parser maps the attributes with the declared
* styleables.
*
* @author Akash Yadav
*/
public class AttrInfo {
/** Styles mapped by names; */
public final Map<String, Styleable> styles = new HashMap<>();
public final Map<String, Attr> attributes = new TreeMap<>();
public final Styleable NO_PARENT;
public AttrInfo(@NonNull final Resources resources) throws Exception {
NO_PARENT = new Styleable("<unknown_parent>");
Objects.requireNonNull(resources, "Cannot read from null resources.");
final var in = resources.openRawResource(R.raw.attrs_xml);
parseFromStream(in);
}
@Nullable
public Attr getAttribute(@NonNull final String name) {
var attr = this.attributes.get(name);
if (attr == null) {
final var a = NO_PARENT.attributes.stream().filter(aa -> aa.name.equals(name)).findFirst();
attr = a.orElse(null);
}
return attr;
}
@NonNull
public Map<String, Attr> getAttributes() {
return this.attributes;
}
@Nullable
public Styleable getStyle(@NonNull final String name) {
return this.styles.get(name);
}
@NonNull
public Map<String, Styleable> getStyles() {
return styles;
}
@Override
public int hashCode() {
return Objects.hash(NO_PARENT, styles);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AttrInfo)) {
return false;
}
AttrInfo that = (AttrInfo) o;
return Objects.equals(NO_PARENT, that.NO_PARENT) && Objects.equals(styles, that.styles);
}
@NonNull
@Override
public String toString() {
return "AttrInfoCompat{" + "NO_PARENT=" + NO_PARENT + ", styles=" + styles + '}';
}
protected void parseFromStream(InputStream in) throws Exception {
Objects.requireNonNull(in, "Cannot read from null input stream");
Document doc = Jsoup.parse(in, null, "", Parser.xmlParser());
final var resources = doc.getElementsByTag("resources").first();
parseFromResources(resources);
}
protected void parseFromResources(Element resources) {
Objects.requireNonNull(resources, "Cannot parse from null resource element");
this.styles.clear();
for (int i = 0; i < resources.childrenSize(); i++) {
final var child = resources.child(i);
if (child.tagName().equals("declare-styleable")) {
this.styles.put(parseStyleable(child).name, parseStyleable(child));
} else if (child.tagName().equals("attr")) {
NO_PARENT.attributes.add(parseAttr(child));
}
}
this.styles.put(NO_PARENT.name, NO_PARENT);
}
@NonNull
protected Styleable parseStyleable(@NonNull final Element styleable) {
checkName(styleable);
final var name = styleable.attr("name");
final var style = new Styleable(name);
for (int i = 0; i < styleable.childrenSize(); i++) {
final var attr = styleable.child(i);
if (!attr.tagName().equals("attr")) {
continue;
}
style.attributes.add(parseAttr(attr));
}
return style;
}
@NonNull
protected Attr parseAttr(@NonNull final Element attr) {
checkName(attr);
final var attribute = new Attr(attr.attr("name"));
if (attribute.name.contains(":")) {
final var split = attribute.name.split(":", 2);
attribute.namespace = INamespace.ANDROID;
attribute.name = split[1];
}
if (attribute.name.equals("gravity")) {
System.out.println("This is it");
}
if (attr.hasAttr("format")) {
attribute.format = Attr.formatForName(attr.attr("format"));
if (attribute.hasFormat(Attr.BOOLEAN)) {
attribute.possibleValues.add("true");
attribute.possibleValues.add("false");
}
}
final var enums = attr.getElementsByTag("enum");
final var flags = attr.getElementsByTag("flag");
if (enums.size() > 0) {
for (var enumEntry : enums) {
checkName(enumEntry);
attribute.possibleValues.add(enumEntry.attr("name"));
}
if (!attribute.hasFormat(Attr.ENUM)) {
attribute.format |= Attr.ENUM;
}
}
if (flags.size() > 0) {
for (var flagEntry : flags) {
checkName(flagEntry);
attribute.possibleValues.add(flagEntry.attr("name"));
}
if (!attribute.hasFormat(Attr.FLAG)) {
attribute.format |= Attr.FLAG;
}
}
if (this.attributes.containsKey(attribute.name)) {
final var present = this.attributes.get(attribute.name);
if (present != null) {
if (present.hasPossibleValues()) {
attribute.possibleValues.addAll(present.possibleValues);
}
if (attribute.format == 0 && present.format != 0) {
attribute.format = present.format;
}
}
}
this.attributes.put(attribute.name, attribute);
return attribute;
}
private void checkName(@NonNull Element child) {
if (!child.hasAttr("name")) {
throw new IllegalStateException("Element does not have 'name' attribute: " + child);
}
}
}
@@ -1,255 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.attrinfo.models;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.xml.INamespace;
import org.jetbrains.annotations.Contract;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
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 implements Parcelable {
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 static final String FORMAT_REFERENCE = "reference";
public static final String FORMAT_COLOR = "color";
public static final String FORMAT_BOOLEAN = "boolean";
public static final String FORMAT_DIMENSION = "dimension";
public static final String FORMAT_FLOAT = "float";
public static final String FORMAT_INTEGER = "integer";
public static final String FORMAT_FRACTION = "fraction";
public static final String FORMAT_STRING = "string";
public static final String FORMAT_ENUM = "enum";
public static final String FORMAT_FLAG = "flag";
public static final Creator<Attr> CREATOR =
new Creator<>() {
@Override
public Attr createFromParcel(Parcel in) {
return new Attr(in);
}
@Override
public Attr[] newArray(int size) {
return new Attr[size];
}
};
private static final ILogger LOG = ILogger.newInstance("AttrInfo::Attr");
public INamespace namespace;
public String name;
public Set<String> possibleValues;
public int format;
public Attr(String name) {
this(name, INamespace.ANDROID);
}
public Attr(String name, INamespace namespace) {
this.name = name;
this.namespace = namespace;
this.possibleValues = new TreeSet<>();
}
protected Attr(@NonNull Parcel in) {
this.namespace = in.readParcelable(INamespace.class.getClassLoader());
this.name = in.readString();
this.format = in.readInt();
final var list = new ArrayList<String>();
in.readStringList(list);
this.possibleValues = new TreeSet<>(list);
}
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 FORMAT_REFERENCE:
return REFERENCE;
case FORMAT_COLOR:
return COLOR;
case FORMAT_BOOLEAN:
return BOOLEAN;
case FORMAT_DIMENSION:
return DIMENSION;
case FORMAT_FLOAT:
return FLOAT;
case FORMAT_INTEGER:
return INTEGER;
case FORMAT_FRACTION:
return FRACTION;
case FORMAT_STRING:
return STRING;
case FORMAT_ENUM:
return ENUM;
case FORMAT_FLAG:
return FLAG;
}
return UNKNOWN;
}
public static String createFormatText(int format) {
final var formats = new ArrayList<String>();
if ((format & REFERENCE) != 0) {
formats.add(FORMAT_REFERENCE);
}
if ((format & COLOR) != 0) {
formats.add(FORMAT_COLOR);
}
if ((format & BOOLEAN) != 0) {
formats.add(FORMAT_BOOLEAN);
}
if ((format & DIMENSION) != 0) {
formats.add(FORMAT_DIMENSION);
}
if ((format & FLOAT) != 0) {
formats.add(FORMAT_FLOAT);
}
if ((format & INTEGER) != 0) {
formats.add(FORMAT_INTEGER);
}
if ((format & FRACTION) != 0) {
formats.add(FORMAT_FRACTION);
}
if ((format & STRING) != 0) {
formats.add(FORMAT_STRING);
}
if ((format & ENUM) != 0) {
formats.add(FORMAT_ENUM);
}
if ((format & FLAG) != 0) {
formats.add(FORMAT_FLAG);
}
if (formats.isEmpty()) {
formats.add("unknown");
}
return TextUtils.join("|", formats);
}
public boolean hasPossibleValues() {
return possibleValues != null && possibleValues.size() > 0;
}
public boolean hasFormat(int format) {
return (this.format & format) != 0;
}
@Override
public int hashCode() {
return Objects.hash(namespace, name, possibleValues, format);
}
@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);
}
@NonNull
@Override
public String toString() {
return "Attr ["
+ " name: "
+ name
+ "\n"
+ " namespace: "
+ namespace
+ "\n"
+ " values: "
+ TextUtils.join(", ", this.possibleValues)
+ "\n"
+ " format: "
+ format
+ "\n"
+ "]";
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeParcelable(namespace, flags);
dest.writeString(name);
dest.writeInt(format);
dest.writeStringList(new ArrayList<>(this.possibleValues));
}
}
@@ -1,66 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.attrinfo.models;
import androidx.annotation.NonNull;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* A <code>&lt;declare-styleable&gt;</code>.
*
* @author Akash Yadav
*/
public class Styleable {
public final String name;
public final Set<Attr> attributes = new HashSet<>();
public Styleable(String name) {
if (name == null || name.trim().length() <= 0) {
throw new IllegalArgumentException("Invalid name specified for declared styleable");
}
this.name = name.trim();
}
@Override
public int hashCode() {
return Objects.hash(name, attributes);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Styleable)) {
return false;
}
Styleable styleable = (Styleable) o;
return Objects.equals(name, styleable.name) && Objects.equals(attributes, styleable.attributes);
}
@NonNull
@Override
public String toString() {
return "Styleable{" + "name='" + name + '\'' + ", attributes=" + attributes + '}';
}
}
@@ -1,67 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.sdk;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import com.itsaky.apiinfo.ApiInfo;
import com.itsaky.attrinfo.AttrInfo;
import com.itsaky.widgets.WidgetInfo;
import java.util.Objects;
/**
* Class that holds all of the 'sdk-info' APIs.
*
* @see ApiInfo
* @see AttrInfo
* @see WidgetInfo
* @author Akash Yadav
*/
public class SDKInfo {
private final AttrInfo attrInfo;
private final ApiInfo apiInfo;
private final WidgetInfo widgetInfo;
public SDKInfo(@NonNull final Context context) throws Exception {
this(context.getResources());
}
public SDKInfo(@NonNull final Resources resources) throws Exception {
Objects.requireNonNull(resources);
this.apiInfo = new ApiInfo(resources);
this.attrInfo = new AttrInfo(resources);
this.widgetInfo = new WidgetInfo(resources);
}
public AttrInfo getAttrInfo() {
return attrInfo;
}
public ApiInfo getApiInfo() {
return apiInfo;
}
public WidgetInfo getWidgetInfo() {
return widgetInfo;
}
}
@@ -1,103 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.widgets;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.widgets.models.Widget;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
public class WidgetInfo {
private final SortedMap<String, Widget> widgets;
private final SortedMap<String, String[]> params;
public WidgetInfo(@NonNull final Resources resources) throws IOException {
this.widgets = new TreeMap<>();
this.params = new TreeMap<>();
readWidgets(resources);
}
private void readWidgets(@NonNull final Resources resources) throws IOException {
final InputStream in = resources.openRawResource(com.itsaky.androidide.resources.R.raw.widgets);
if (in == null) {
return;
}
final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = reader.readLine()) != null) {
final var split = line.split("\\s");
final var name = split[0];
if (name == null || name.trim().isEmpty()) {
continue;
}
final var code = name.charAt(0);
final var entryName = name.substring(1);
final var isLayout = code == 'L'; // L -> Layout, W -> Widget, P -> LayoutParam
final var superclasses = new String[split.length - 1];
System.arraycopy(split, 1, superclasses, 0, superclasses.length);
if (code == 'P') {
final var className = entryName.substring(0, entryName.lastIndexOf("."));
this.params.put(className, superclasses);
} else {
final var widget = new Widget(entryName, isLayout);
widget.superclasses = superclasses;
widgets.put(entryName, widget);
}
}
}
public Widget getWidget(String name) {
return this.widgets.getOrDefault(name, null);
}
@Nullable
public Widget getWidgetBySimpleName(@NonNull final String name) {
for (final var entry : this.widgets.entrySet()) {
final var key = entry.getKey();
final var simple = key.substring(key.lastIndexOf(".") + 1);
if (simple.equals(name)) {
return entry.getValue();
}
}
return null;
}
public Collection<Widget> getWidgets() {
return this.widgets.values();
}
public String[] getLayoutParamSuperClasses(final String className) {
return this.params.get(className);
}
}
@@ -1,46 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.widgets.models;
import androidx.annotation.NonNull;
public class Widget implements Comparable<Widget> {
public String name;
public String simpleName;
public boolean isViewGroup;
public String[] superclasses = new String[0];
public Widget(@NonNull String name, boolean isViewGroup) {
this.name = name;
this.simpleName = name.substring(name.lastIndexOf(".") + 1);
this.isViewGroup = isViewGroup;
}
@Override
public int compareTo(Widget that) {
if (that != null) {
return this.simpleName.compareTo(that.simpleName);
}
return -1;
}
}
-1
View File
@@ -12,7 +12,6 @@ include ':lookup'
include ':sdk-info'
include ':shared'
include ':treeview'
include ':xml-inflater'
include ':gradle-plugin'
Binary file not shown.
-85
View File
@@ -1,85 +0,0 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
/bin/
/gen/
/out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
/build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
-44
View File
@@ -1,44 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
namespace "com.itsaky.inflater"
compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
defaultConfig {
minSdkVersion rootProject.ext.minSdk
targetSdkVersion rootProject.ext.targetSdk
}
compileOptions {
sourceCompatibility rootProject.ext.javaSourceVersion
targetCompatibility rootProject.ext.javaTargetVersion
}
kotlinOptions {
jvmTarget = "11"
}
buildTypes {
release {
minifyEnabled true
}
}
}
dependencies {
implementation fileTree (dir: 'libs', include: ['*.jar'])
implementation androidx.ktx
implementation androidx.annotation
implementation androidx.appcompat
implementation google.guava
implementation common.utilcode
implementation common.jsoup
implementation common.kotlin
implementation project (":common")
implementation project (":resources")
api project (':sdk-info')
}
-17
View File
@@ -1,17 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/bogdan/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
@@ -1 +0,0 @@
<manifest/>
@@ -1,117 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
/**
* Value type of {@link IAttribute}
*
* @author Akash Yadav
*/
public enum AttrType {
/** Refers to another resource */
REFERENCE("reference"),
/** Color value */
COLOR("color"),
/** Either {@code true} or {@code false} */
BOOLEAN("boolean"),
/** A dimension like 10dp, 14sp etc. */
DIMENSION("dimension"),
/** A floating point number. E.g.: 1.9, 123.45 */
FLOAT("float"),
/** An integer value */
INTEGER("integer"),
/** A string value */
STRING("string"),
/** A fraction */
FRACTION("fraction"),
/** A enum value. Unlike {@link #FLAG}, an attribute can have only one of this. */
ENUM("enum"),
/** A flag value. May contain on or more values */
FLAG("flag"),
/** An unknown value. The layout inflater does not consider this */
UNKNOWN("");
public static final String FORMAT_REF = REFERENCE.getFormat();
public static final String FORMAT_COLOR = COLOR.getFormat();
public static final String FORMAT_BOOL = BOOLEAN.getFormat();
public static final String FORMAT_DIMEN = DIMENSION.getFormat();
public static final String FORMAT_FLOAT = FLOAT.getFormat();
public static final String FORMAT_INT = INTEGER.getFormat();
public static final String FORMAT_STRING = STRING.getFormat();
public static final String FORMAT_FRACTION = FRACTION.getFormat();
public static final String FORMAT_ENUM = ENUM.getFormat();
public static final String FORMAT_FLAG = FLAG.getFormat();
private final String format;
private AttrType(String format) {
this.format = format;
}
/**
* Get the attribute type from format
*
* @param format Format, as declared in styleables.
* @return The Attribute type
*/
public static AttrType fromFormat(String format) {
if (FORMAT_REF.equals(format)) {
return AttrType.REFERENCE;
} else if (FORMAT_COLOR.equals(format)) {
return AttrType.COLOR;
} else if (FORMAT_BOOL.equals(format)) {
return AttrType.BOOLEAN;
} else if (FORMAT_DIMEN.equals(format)) {
return AttrType.DIMENSION;
} else if (FORMAT_FLOAT.equals(format)) {
return AttrType.FLOAT;
} else if (FORMAT_INT.equals(format)) {
return AttrType.INTEGER;
} else if (FORMAT_STRING.equals(format)) {
return AttrType.STRING;
} else if (FORMAT_FRACTION.equals(format)) {
return AttrType.FRACTION;
} else if (FORMAT_ENUM.equals(format)) {
return AttrType.ENUM;
} else if (FORMAT_FLAG.equals(format)) {
return AttrType.FLAG;
}
return AttrType.UNKNOWN;
}
public String getFormat() {
return this.format;
}
public boolean hasFixedValues() {
return this == AttrType.ENUM || this == AttrType.FLAG;
}
}
@@ -1,73 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
import java.util.HashSet;
import java.util.Set;
public abstract class BaseLayoutInflater extends ILayoutInflater {
private final Set<IInflateListener> inflateListeners = new HashSet<>();
protected boolean notify = true;
@Override
public void registerInflateListener(IInflateListener listener) {
inflateListeners.add(listener);
}
@Override
public void unregisterListener(IInflateListener listener) {
inflateListeners.remove(listener);
}
protected void preInflate() {
if (notify) {
for (IInflateListener listener : inflateListeners) {
listener.onBeginInflate();
}
}
}
protected void postApplyAttribute(IAttribute attr, IView view) {
if (notify) {
for (IInflateListener listener : inflateListeners) {
listener.onApplyAttribute(attr, view);
}
}
}
protected void postCreateView(IView view) {
if (notify) {
for (IInflateListener listener : inflateListeners) {
listener.onInflateView(view, view.getParent());
}
}
}
protected void postInflate(IView root) {
if (notify) {
for (IInflateListener listener : inflateListeners) {
listener.onFinishInflate(root);
}
}
}
protected Set<IInflateListener> getInflateListeners() {
return this.inflateListeners;
}
}
@@ -1,110 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.xml.INamespace;
import java.util.Comparator;
/**
* Attribute that can be applied to a {@link IView}
*
* @author Akash Yadav
*/
public interface IAttribute {
/**
* Returned by {@link #compareNull(Object, Object)} to indicate that the comparison must be done
* further.
*/
int DO_COMPARE = 2;
Comparator<IAttribute> 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 != null && 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 != null && firstNs.equals(INamespace.DECLARATOR)) {
return -1;
}
if (secondNs != null && secondNs.equals(INamespace.DECLARATOR)) {
return 1;
}
return 0;
};
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) {
return 1;
}
return DO_COMPARE;
}
@Nullable
INamespace getNamespace();
@NonNull
String getAttributeName();
@NonNull
String getValue();
/**
* Set the given string as a value to this attribute. This will set {@link #isApplied()} to return
* {@code true}.
*
* @param value The value to set.
*/
void apply(String value);
}
@@ -1,52 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
import android.view.View;
/** An adapter that handles attributes of a specific type of view */
public interface IAttributeAdapter {
/**
* Set the resource finder used by this class.
*
* @param resourceFinder The resource finder.
*/
void setResourceFinder(IResourceTable resourceFinder);
/**
* Can this adapter handle attributes of the provided view?
*
* @param view The view to which the attributes will be applied
* @return {@code true} if this adapter can handle attributes of the specified view
*/
boolean isApplicableTo(View view);
/**
* Apply the provided attribute to the view.
*
* <p>This will be called if and only if {@link #isApplicableTo(View)} returns {@code true}.
*
* @param attribute The attribute to apply
* @param view The view to apply attribute to
* @return {@code true} if the attribute was applied successfully {@code false} otherwise.
*/
boolean apply(IAttribute attribute, View view);
}
@@ -1,84 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
import android.view.View;
import java.util.HashMap;
import java.util.Map;
/**
* Holds references to view IDs. The {@link #newRound()} must be called before inflating a layout
* from the XML. This will make sure that we cleanup the existing IDs.
*
* @author Akash Yadav
*/
public class IDTable {
/**
* Map<String, Integer> containing the IDs. Entries are mapped by the name of the ID.
*
* <p>For example : if android:id="@+id/my_id" is provided for view, then an entry will be created
* in this map with the key : 'my_id' and a random integer as the value.
*
* <p>No entries are created for views which do not have the android:id attribute.
*/
private static final Map<String, Integer> ids = new HashMap<>();
/** Clears the existing ID map. */
public static void newRound() {
ids.clear();
}
/**
* Creates a new ID entry for the given name.
*
* @param idName The name of the ID.
* @return The assigned ID.
*/
public static int newId(String idName) {
{
// In some cases, @+id might be used as a reference
// to another ID. So we check if an ID with the same
// name is already present or not.
final int id = getId(idName);
if (id != View.NO_ID) {
return id;
}
}
final int id = View.generateViewId();
ids.put(idName, id);
return id;
}
/**
* Get the ID mapped by the given name.
*
* @param name The name of the ID.
* @return The mapped ID or {@link android.view.View#NO_ID}
*/
public static int getId(String name) {
final Integer id = ids.getOrDefault(name, null);
if (id == null) {
return View.NO_ID;
}
return id;
}
}
@@ -1,49 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
/**
* A fixed value which represents an enum of a flag
*
* @author Akash Yadav
*/
public interface IFixedValue {
/**
* Is this value a flag? An attribute which accepts a flag may be combined with other flags.
*
* @return {@code true} if this value is a flag. {@code false} otherwise.
*/
boolean isFlag();
/**
* The name of this enum/flag as represented in an XML file
*
* @return The name of this flag
*/
String getName();
/**
* Get the value of this enum/flag. This value is fixed and should not change.
*
* @return Value of this enum/flag.
*/
int getValue();
}
@@ -1,52 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
/**
* A listener which is invoked to notify about events in {@link ILayoutInflater}
*
* @author Akash Yadav
*/
public interface IInflateListener {
/** Called when the inflation process is about to start */
void onBeginInflate();
/**
* Called when a view has been inflated
*
* @param view The view that will be inflated
* @param parent The parent of this view
*/
void onInflateView(IView view, IViewGroup parent);
/**
* Called when the attribute has been applied to the specified view
*
* @param attr The applied attribute
* @param view The view to which attribute was applied
*/
void onApplyAttribute(IAttribute attr, IView view);
/**
* Called when the inflation process finished successfully
*
* @param The root rootView of the inflated layout
*/
void onFinishInflate(IView rootView);
}
@@ -1,185 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
import android.content.Context;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.itsaky.inflater.util.Preconditions;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A layout inflater which inflates layout from a raw XML file
*
* @author Akash Yadav
*/
public abstract class ILayoutInflater {
private static final Pattern XML_DECL = Pattern.compile("<\\?xml.*\\?>");
/** The file whose content is currently being inflated. */
private File currentlyInflatingFile;
/**
* Creates an instance of {@code ILayoutInflater}
*
* @param config The configuration of the LayoutInflater
* @return A new layout inflater instance
*/
@NonNull
public static ILayoutInflater newInstance(LayoutInflaterConfiguration config) {
return new XMLLayoutInflater(config);
}
/**
* Reset the {@link ContextProvider} of this inflater
*
* @param provider The new {@link ContextProvider}
*/
public abstract void resetContextProvider(ContextProvider provider);
/**
* Register the inflate listener to this inflater.
*
* <p>NOTE: Do not register too much listeners as they may affect the inflation time.
*
* @param listener The listener to register.
*/
public abstract void registerInflateListener(IInflateListener listener);
/**
* Unregister the inflate listener if it is registered.
*
* @param listener The listener to unregister
*/
public abstract void unregisterListener(IInflateListener listener);
/**
* Inflate the layout from the given file path
*
* @param path The file path to inflate layout from
* @return The inflated layout
* @throws InflateException when there was an error inflating the layout
*/
@NonNull
public IView inflatePath(String path, ViewGroup parent) throws InflateException {
Preconditions.assertNotBlank(path, "Layout file path is blank!");
return inflate(new File(path), parent);
}
/**
* Inflate the layout from the given file
*
* @param file The file to inflate layout from
* @return The inflated layout
* @throws InflateException when there was an error inflating the layout
*/
@NonNull
public IView inflate(File file, ViewGroup parent) throws InflateException {
Preconditions.assertNotnull(file, "Cannot inflate null file");
// How can we inflate a non existent file?
if (!file.exists()) {
throw new InflateException("File does not exist!");
}
if (!file.canRead()) {
throw new InflateException("Cannot read file. Permission denied!");
}
// Cannot inflate a non UTF-8 too. This will also be true if the file is empty.
if (!FileUtils.isUtf8(file)) {
throw new InflateException("File is not UTF-8 or is empty!");
}
requireResourceTable().setInflatingFile(file);
this.currentlyInflatingFile = file;
return inflate(FileIOUtils.readFile2String(file), parent);
}
/**
* Get the resource finder attached to this layout inflater.
*
* @return The attached resource finder.
*/
@NonNull
protected abstract IResourceTable requireResourceTable();
/**
* Inflate the layout from the provided XML layout code
*
* @param layout The XML layout code
* @return The inflated layout
* @throws InflateException when there was an error inflating the layout
*/
@NonNull
protected IView inflate(String layout, ViewGroup parent) throws InflateException {
Preconditions.assertNotBlank(layout, "Layout is blank!");
// Trim the layout code
layout = layout.trim();
final Matcher matcher = XML_DECL.matcher(layout);
if (matcher.find()) {
layout = layout.substring(matcher.end()).trim();
}
return doInflate(layout, parent);
}
/**
* Acatual implementation of the layout inflation
*
* @param layout The layout to inflate
* @return The inflated view
* @throws InflateException when there was an error inflating the layout
*/
@NonNull
protected abstract IView doInflate(String layout, ViewGroup parent) throws InflateException;
/**
* Get the currently inflating file.
*
* @return The file.
*/
@Nullable
protected File getFile() {
return currentlyInflatingFile;
}
/** Provides a reference to {@link Context}. This will be used to create original view. */
public interface ContextProvider {
/**
* Provide the context to this inflater.
*
* @return The context
*/
Context getContext();
}
}
@@ -1,112 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.inflater;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.util.Collection;
/** Finds values in resources of a project */
public interface IResourceTable {
/**
* Find the path of the drawable with the provided name
*
* <p>This method will be called if the value of attribute starts with '@drawable/'
*
* @param name The name of the drawable
* @return The drawable resource.
*/
File findDrawable(@NonNull String name);
/**
* Find the layout file with the specified name.
*
* @param name The name of the layout file
* @return The found layout resource file
*/
File findLayout(@NonNull String name);
/**
* Find a string resource. Must be a valid string or {@code null}
*
* @param name The name of the resource
* @return Value of the resource
*/
String findString(@NonNull String name);
/**
* Find a color resource. The returned value must be a valid color value or {@code -1}
*
* @param name The name of the resource
* @return Value of the resource
*/
String findColor(@NonNull String name);
/**
* Find an array resource. Must be a valid array or {@code null}
*
* @param name The name of the resource
* @return Value of the resource
*/
String[] findArray(@NonNull String name);
/**
* Find the dimension value with the given name.
*
* @param name The name of the dimension resource
* @return The String representation of the dimension. It could one of the following format : 1dp,
* 12sp, 123px, etc.
*/
String findDimension(@NonNull String name);
/**
* Find the boolean value with the given name.
*
* @param name The name of the boolean resource.
* @return The string value of the boolean resource.
*/
String findBoolean(@NonNull String name);
/**
* Find the integer value with the given name.
*
* @param name The name of the integer resource.
* @return The string value of the integer resource.
*/
String findInteger(@NonNull String name);
/**
* Set the file which is currently being inflated.
*
* @param file The file.
*/
void setInflatingFile(@NonNull File file);
/**
* Lists the resources with the given type.
*
* @param type The type of the resources to list. Might be <code>null</code>.
* @return A collection of resource names. If <code>type</code> is <code>null</code>, all the
* resources in the resource table must be listed.
*/
@NonNull
Collection<String> listResourceNames(@Nullable String type);
}
@@ -1,217 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.inflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.xml.INamespace;
import java.util.List;
/**
* Represents a view in the UI Designer
*
* @author Akash Yadav
*/
public interface IView {
/**
* The indentation length that will be used while generating layout code. You can modify this to
* change the number of spaces used for indentation.
*/
int DEFAULT_INDENTATION_LENGTH = 4;
/**
* Get this view as android {@link View}. This will be used to display this view in the designer
*
* @return {@link View} of this object. Maybe {@code null}
*/
View asView();
/**
* Add this attribute to this view
*
* @param attr The Attribute to add
*/
void addAttribute(IAttribute attr);
/**
* Remove this attribute
*
* @param attr The Attribute to remove
*/
void removeAttribute(IAttribute attr);
/**
* Remove attribute at index
*
* @param index Index of the attribute to remove
*/
void removeAttributeAt(int index);
/**
* Get the attributes of this view.
*
* @return The attributes.
*/
List<IAttribute> getAttributes();
/**
* Get the attributes of this view as an array.
*
* @return The attributes.
* @see #getAttributes()
*/
IAttribute[] getAttrArray();
/**
* Does this view contains an attribute with the given namespace and name?
*
* @param namespace The namespace to check.
* @param name The name of the attribute to check.
* @return {@code true} if this view contains the attribute, {@code false} otherwise.
*/
boolean hasAttribute(INamespace namespace, String name);
/**
* Get the attribute with the given namespace and attribute name. Or {@code null} if there is no
* attribute with the given namespace and name.s
*
* @param namespace The namespace of the attribute.
* @param name The name of the attribute.
* @return Found {@link IAttribute} or {@code null}.
*/
@Nullable
IAttribute getAttribute(INamespace namespace, String name);
/**
* Update the value of the given attribute.
*
* @param attribute The attribute that should be updated.
* @return {@code true} if the attribute was successfully updated, {@code false} otherwise.
*/
default boolean updateAttribute(@NonNull IAttribute attribute) {
return updateAttribute(
attribute.getNamespace(), attribute.getAttributeName(), attribute.getValue());
}
/**
* Find and update the given attribute with the given value.
*
* @param namespace The namespace of the attribute.
* @param name The name of the attribute.
* @param value The new value of the attribute.
* @return {@code true} if the attribute was successfully updated, {@code false} otherwise.
*/
boolean updateAttribute(INamespace namespace, String name, String value);
/**
* Register this attribute adapter
*
* @param adapter The adapter to register
*/
void registerAttributeAdapter(IAttributeAdapter adapter);
/**
* Register the given namespace to this view.
*
* @param namespace The name space to register.
*/
void registerNamespace(INamespace namespace);
/**
* Find the namespace registered by the given name. Returns <code>null</code> if there is none.
*
* @param name The name of the namespace to look for. Must not be <code>null</code>.
* @return The found namespace or <code>null</code>.
*/
@Nullable
INamespace findRegisteredNamespace(@NonNull String name);
/**
* Get the parent of this view
*
* @return The parent of this view or {@code null} if this is the root view.
*/
IViewGroup getParent();
/**
* 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);
/**
* 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?
*
* @return {@code true} if this view is a placeholder, {@code false} otherwise.
*/
boolean isPlaceholder();
/**
* Get the stored data.
*
* @return The stored data.
* @see #setExtraData(Object)
*/
@Nullable
Object getExtraData();
/**
* Store an object that is associated with this view.
*
* @param data The data to store.
*/
void setExtraData(Object data);
/**
* @see #generateCode(int)
*/
default String generateCode() {
return generateCode(0);
}
/**
* Get the layout resource code for this file.
*
* @param indentationLength The number of spaces to indent.
*/
String generateCode(int indentationLength);
/**
* Get the tag name which will be used for generating XML Code for this view.
*
* <p>Default implementation simply returns the simple name of this view's class.
*
* @return The tag name of this view.
*/
String getXmlTag();
}
@@ -1,145 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.inflater;
import android.view.View;
import java.util.List;
import java.util.function.Consumer;
/**
* Represents a {@link android.view.ViewGroup}
*
* @author Akash Yadav
*/
public interface IViewGroup extends IView {
/**
* Add this view to this group
*
* @param view The view to add
*/
void addView(IView view);
/**
* Add this view to this group
*
* @param view The view to add
* @param index Add this view to this index
*/
void addView(IView view, int index);
/**
* Remove the view at the specified index
*
* @param index The index of the view to remove
*/
void removeView(int index);
/**
* Remove the given if it exists in this group
*
* @param view The view to remove
*/
void removeView(IView view);
/** Remove all children in this group */
void removeAll();
/**
* Perform an action in every child in this group
*
* @param consumer The consumer which will consume the views
*/
void forEachChild(Consumer<IView> consumer);
/**
* Get the number of child views this layout contains
*
* @return The child count
*/
int getChildCount();
/**
* Get the list of children in this group
*
* @return The list of children
*/
List<IView> getChildren();
/**
* Get child at the provided index
*
* @param index The index of child
* @throws IndexOutOfBoundsException If there is no child at the given index
*/
IView getChildAt(int index);
/**
* Find the index of the given children in this group.
*
* @param view The view to find index of.
* @return The index of the child or -1 if this group does not contain the given child.
*/
int indexOfChild(IView view);
/**
* Find the index of the given children in this group.
*
* @param view The view to find index of.
* @return The index of the child or -1 if this group does not contain the given child.
*/
int indexOfChild(View view);
/**
* Register the given hierarchy change listener to this group.
*
* @param listener The listener to register.
*/
void registerHierarchyChangeListener(OnHierarchyChangeListener listener);
/**
* Unregister the given hierarchy change listener from this view group.
*
* @param listener The listener to unregister.
*/
void unregisterHierarchyChangeListener(OnHierarchyChangeListener listener);
/**
* A listener which can be used to get notification when a children is added or removed from an
* {@link IViewGroup}.
*
* @author Akash Yadav
*/
public static interface OnHierarchyChangeListener {
/**
* Called when a child view is added in the view group to which this listener is attached.
*
* @param view The view that was added.
*/
void onViewAdded(IView view);
/**
* Called when a child view is removed from the view group to which this listener is attached.
*
* @param view The view that was removed. Note that this view's parent will be null.
*/
void onViewRemoved(IView view);
}
}
@@ -1,36 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
/** Thrown when there was an error inflating the layout */
public class InflateException extends RuntimeException {
public InflateException(String message) {
super(message);
}
public InflateException(Throwable th) {
super(th);
}
public InflateException(String msg, Throwable th) {
super(msg, th);
}
}
@@ -1,87 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater;
import com.itsaky.attrinfo.AttrInfo;
import com.itsaky.widgets.WidgetInfo;
import java.io.File;
import java.util.Set;
/** Configuration for {@link ILayoutInflater} */
public class LayoutInflaterConfiguration {
final Set<File> resDirs;
final AttrInfo attrInfo;
final WidgetInfo widgetInfo;
final IResourceTable resFinder;
final ILayoutInflater.ContextProvider contextProvider;
public LayoutInflaterConfiguration(
Set<File> resDirs,
AttrInfo attrInfo,
WidgetInfo widgetInfo,
IResourceTable resourceProvider,
ILayoutInflater.ContextProvider contextProvider) {
this.resDirs = resDirs;
this.attrInfo = attrInfo;
this.widgetInfo = widgetInfo;
this.resFinder = resourceProvider;
this.contextProvider = contextProvider;
}
/** A class that builds a {@link LayoutInflaterConfiguration} */
public static class Builder {
private Set<File> resDirs;
private AttrInfo attrInfo;
private WidgetInfo widgetInfo;
private IResourceTable resourceProvider;
private ILayoutInflater.ContextProvider provider;
public Builder setResourceDirectories(Set<File> dirs) {
this.resDirs = dirs;
return this;
}
public Builder setAttrInfo(AttrInfo info) {
this.attrInfo = info;
return this;
}
public Builder setWidgetInfo(WidgetInfo info) {
this.widgetInfo = info;
return this;
}
public Builder setResourceFinder(IResourceTable provider) {
this.resourceProvider = provider;
return this;
}
public Builder setContextProvider(ILayoutInflater.ContextProvider provider) {
this.provider = provider;
return this;
}
public LayoutInflaterConfiguration create() {
return new LayoutInflaterConfiguration(
resDirs, attrInfo, widgetInfo, resourceProvider, provider);
}
}
}
@@ -1,41 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater;
/**
* Exception thrown by inflaters and drawable parsers to indicate that the a specific view or tag is
* not supported.
*
* @author Akash Yadav
*/
public class NotSupportedException extends UnsupportedOperationException {
public NotSupportedException() {}
public NotSupportedException(String message) {
super(message);
}
public NotSupportedException(String message, Throwable cause) {
super(message, cause);
}
public NotSupportedException(Throwable cause) {
super(cause);
}
}
@@ -1,448 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.inflater;
import static com.itsaky.inflater.util.Preconditions.assertNotBlank;
import static com.itsaky.inflater.util.Preconditions.assertNotnull;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.blankj.utilcode.util.ReflectUtils;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.attrinfo.AttrInfo;
import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter;
import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
import com.itsaky.inflater.impl.BaseView;
import com.itsaky.inflater.impl.ErrorUiView;
import com.itsaky.inflater.impl.IncludeLayout;
import com.itsaky.inflater.impl.UiAttribute;
import com.itsaky.inflater.impl.UiView;
import com.itsaky.inflater.impl.UiViewGroup;
import com.itsaky.inflater.util.Preconditions;
import com.itsaky.widgets.WidgetInfo;
import com.itsaky.widgets.models.Widget;
import com.itsaky.xml.INamespace;
import com.itsaky.xml.impl.UiNamespace;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
class XMLLayoutInflater extends BaseLayoutInflater {
public static final String ATTR_ADAPTER_SUFFIX = "AttrAdapter";
private static final ILogger LOG = ILogger.newInstance("XMLLayoutInflater");
private final Set<File> resDirs;
private final AttrInfo attrInfo;
private final WidgetInfo widgetInfo;
private final IResourceTable resFinder;
private ContextProvider contextProvider;
XMLLayoutInflater(@NonNull LayoutInflaterConfiguration config) {
this.resDirs = config.resDirs;
this.attrInfo = config.attrInfo;
this.widgetInfo = config.widgetInfo;
this.resFinder = config.resFinder;
this.contextProvider = config.contextProvider;
Preconditions.assertAllNotNull(
"LayoutInflater parameters cannot be null", resDirs, attrInfo, widgetInfo, resFinder);
}
@Override
public void resetContextProvider(ILayoutInflater.ContextProvider provider) {
this.contextProvider = provider;
}
@NonNull
@Override
protected IResourceTable requireResourceTable() {
return this.resFinder;
}
@Override
@NonNull
protected IView doInflate(String layout, ViewGroup parent) throws InflateException {
try {
IDTable.newRound();
// Notify
preInflate();
final Document doc = Jsoup.parse(layout, "", Parser.xmlParser());
if (doc.childrenSize() <= 0) {
throw new InflateException("No views added");
}
if (doc.childrenSize() > 1) {
throw new InflateException(
"More than one root element was found. An XML layout can have only one root"
+ " element.");
}
IView root = onCreateView(doc.child(0), parent, null);
if (root == null) {
root = onCreateErrorView(TextView.class.getName(), "No views added", parent);
}
// Notify
postInflate(root);
return root;
} catch (Throwable th) {
throw new InflateException("Unable to inflate layout", th);
}
}
protected IView onCreateView(Element tag, ViewGroup parentGroup, IViewGroup parent)
throws InflateException {
if (tag == null) {
return null;
}
final String name = tag.tagName().trim();
if (name.equals("include")) {
return createFromInclude(tag.attributes(), parent);
}
final IView root;
int style = tag.hasAttr("style") ? parseFrameworkStyle(tag.attr("style")) : -1;
if (!name.contains(".")) {
root = createFromSimpleName(name, parentGroup, style);
} else {
root = createFromQualifiedName(name, parentGroup, style);
}
if (root == null) {
return null;
}
registerAttributeAdaptersTo(root);
if (!tag.attributes().isEmpty()) {
addAttributesTo(root, tag.attributes(), parent);
}
if (root instanceof IViewGroup) {
addChildren(tag, (IViewGroup) root);
}
postCreateView(root);
return root;
}
protected void addChildren(@NonNull Element tag, IViewGroup parent) {
if (tag.childrenSize() > 0 && !parent.isPlaceholder()) {
for (Element child : tag.children()) {
final IView created = onCreateView(child, (ViewGroup) parent.asView(), parent);
if (created != null) {
parent.addView(created);
created.setParent(parent);
}
}
}
}
protected int parseFrameworkStyle(String value) throws InflateException {
try {
final String name = underscorize(value.substring(value.indexOf('/') + 1));
return (int) android.R.attr.class.getField(name).get(null);
} catch (Throwable th) {
LOG.error("Unable to parse framework style", th);
return -1;
}
}
protected void registerAttributeAdaptersTo(@NonNull IView root) {
root.registerAttributeAdapter(onCreateAttributeAdapter(root.asView()));
}
protected IAttributeAdapter onCreateAttributeAdapter(View view) {
final var displayMetrics = contextProvider.getContext().getResources().getDisplayMetrics();
try {
final String adapterName = view.getClass().getName().concat(ATTR_ADAPTER_SUFFIX);
final String name = "com.itsaky.inflater.adapters.".concat(adapterName);
final Class<? extends IAttributeAdapter> adapterClass =
getClass().getClassLoader().loadClass(name).asSubclass(IAttributeAdapter.class);
final var constructor =
adapterClass.getConstructor(IResourceTable.class, DisplayMetrics.class);
final var adapter = constructor.newInstance(resFinder, displayMetrics);
adapter.setResourceFinder(resFinder);
return adapter;
} catch (Throwable th) {
LOG.error(
BaseApplication.getBaseInstance()
.getString(
com.itsaky.androidide.resources.R.string.err_no_attr_adapter, view.getClass().getName()),
th);
}
// If we cannot find a suitable adapter, fall back to using common adapters
IAttributeAdapter adapter;
if (view instanceof ViewGroup) {
adapter = new ViewGroupAttrAdapter(resFinder, displayMetrics);
} else {
adapter = new ViewAttrAdapter(resFinder, displayMetrics);
}
return adapter;
}
protected void addAttributesTo(IView view, Attributes attributes, IViewGroup parent) {
assertNotnull(attributes, "Cannot apply null attributes!");
for (Attribute attr : attributes) {
final String key = attr.getKey();
final String[] split =
!key.contains(":") ? new String[] {"", key} : key.split(Pattern.quote(":"), 2);
final String namespace = split[0];
final String name = split[1];
final String value = attr.getValue();
INamespace ns;
if (namespace.equals("xmlns")) {
ns = INamespace.DECLARATOR;
view.registerNamespace(new UiNamespace(name, value));
} else {
// set the parent so that we could look for
// declared namespaces
view.setParent(parent);
ns = view.findRegisteredNamespace(namespace);
}
if (ns == null) {
ns = INamespace.invalid(namespace);
}
final IAttribute iAttr = asAttribute(ns, name, value);
view.addAttribute(iAttr);
postApplyAttribute(iAttr, view);
}
}
protected IAttribute asAttribute(INamespace namespace, String name, String value) {
return new UiAttribute(namespace, name, value);
}
protected IView create(@NonNull String name, ViewGroup parent, int style)
throws InflateException {
if (!name.contains(".")) {
return createFromSimpleName(name, parent, style);
} else {
return createFromQualifiedName(name, parent, style);
}
}
protected IView createFromInclude(Attributes attrs, IViewGroup parent) {
if (!attrs.hasKey("layout")) {
throw new InflateException("<include> tag doesn't have 'layout' attribute.");
}
final var layout = attrs.get("layout");
Preconditions.assertNotBlank(
layout,
"'layout' attribute for <include> tag has invalid value." + "layout='" + layout + "'");
if (layout.startsWith("@android:layout/")) {
return inflateAndroidLayout(layout.substring("@android:layout/".length()), attrs, parent);
}
final var name = layout.substring("@layout/".length());
final var file = resFinder.findLayout(name);
if (file == null) {
throw new InflateException("Layout file '" + name + "' not found");
}
return inflateLocalIncludeLayout(attrs, parent, name, file);
}
protected IView inflateAndroidLayout(String name, Attributes attrs, @NonNull IViewGroup parent) {
final var id = (int) ReflectUtils.reflect(android.R.layout.class).field(name).get();
final var view = LayoutInflater.from(contextProvider.getContext()).inflate(id, null);
final var layout = new IncludeLayout(new UiView(IncludeLayout.TAG, view, false));
applyLayoutParams(layout, (ViewGroup) parent.asView());
addAttributesTo(layout, attrs, parent);
return layout;
}
protected IView createFromSimpleName(String name, ViewGroup parent, int style)
throws InflateException {
final Widget widget = widgetInfo.getWidgetBySimpleName(name);
if (widget == null) {
LOG.error("Unable to inflate view. widget == null");
return onCreateErrorView(
name, getString(com.itsaky.androidide.resources.R.string.msg_cannot_create_view, name), parent);
}
return createFromQualifiedName(widget.name, parent, style);
}
protected IView createFromQualifiedName(String name, ViewGroup parent, int style)
throws InflateException {
assertNotBlank(name, "Invalid tag name: " + name);
// TODO Try to load classes directly from .class files if possible
try {
final var androidView = this.widgetInfo.getWidget(name);
if (androidView == null) {
// If this is not an Android view, do not bother to create one
throw new NotSupportedException();
}
final View created = createAndroidViewForName(name, style);
final BaseView view =
created instanceof ViewGroup
? new UiViewGroup(name, (ViewGroup) created)
: new UiView(name, created);
return applyLayoutParams(view, parent);
} catch (Throwable th) {
if (th instanceof NotSupportedException) {
return onCreateErrorView(name, getString(R.string.msg_view_not_supported, name), parent);
}
final var msg = getString(R.string.msg_cannot_create_view, name);
LOG.error(msg, th);
return onCreateErrorView(name, msg, parent);
}
}
@NonNull
protected View createAndroidViewForName(String name, int style)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
InstantiationException, java.lang.reflect.InvocationTargetException {
final Class<? extends View> loaded = Class.forName(name).asSubclass(View.class);
final Constructor<? extends View> constructor;
if (style == -1) {
constructor = loaded.getConstructor(Context.class);
return constructor.newInstance(contextProvider.getContext());
} else {
constructor = loaded.getConstructor(Context.class, AttributeSet.class, int.class);
return constructor.newInstance(contextProvider.getContext(), null, style);
}
}
protected IView applyLayoutParams(@NonNull IView view, ViewGroup parent) {
final ViewGroup.LayoutParams params = generateLayoutParams(parent);
view.asView().setLayoutParams(params);
return view;
}
protected IView onCreateErrorView(String name, String msg, final ViewGroup parent)
throws InflateException {
final var view =
ErrorUiView.create(
Objects.requireNonNull(contextProvider.getContext(), "Context is null."), name, msg);
return applyLayoutParams(view, parent);
}
@NonNull
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup parent) {
try {
Class<?> clazz = parent.getClass();
Method method = null;
while (true) {
try {
method = clazz.getDeclaredMethod("generateDefaultLayoutParams");
break;
} catch (Throwable e) {
/* ignored */
}
clazz = clazz.getSuperclass();
if (clazz == null) {
break;
}
}
if (method != null) {
method.setAccessible(true);
return (ViewGroup.LayoutParams) method.invoke(parent);
}
LOG.error("Unable to create default params for view parent:", parent);
return new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
} catch (Throwable th) {
throw new InflateException("Unable to create layout params for parent: " + parent, th);
}
}
@NonNull
private String underscorize(@NonNull String substring) {
return substring.replace(".", "_");
}
@NonNull
private IncludeLayout inflateLocalIncludeLayout(
Attributes attrs, IViewGroup parent, String name, File file) {
// DesignerActivity applies drag listeners in 'onInflateView()' call
// But, <include> layouts must not be edited here
// So, we avoid notifying the listeners
super.notify = false;
final var view = this.inflate(file, (ViewGroup) parent.asView());
Preconditions.assertNotnull(view, "Failed to inflate '" + name + "'");
final var includeLayout = new IncludeLayout(view);
applyLayoutParams(includeLayout, (ViewGroup) parent.asView());
addAttributesTo(includeLayout, attrs, parent);
// Enable notifications
super.notify = true;
return includeLayout;
}
private String getString(@StringRes int id, Object... format) throws InflateException {
assertNotnull(contextProvider.getContext(), "Context is null!");
return contextProvider.getContext().getString(id, format);
}
}
@@ -1,59 +0,0 @@
# Attribute Adapters
Attribute adapters handle attributes for a specific view.
## Implementing an Adapter
### Basic
To implment an adapter, you'll have to write a class extending one of the existing adapters.
Adapters extend other adapters in the same way as Android's view classes extend other views. For
example, ```android.widget.CompoundButton``` extends ```android.widget.Button```, in the same way
```com.itsaky.layoutinflater.adapters.android.widget.CompoundButtonAttrAdapter``` extends
```com.itsaky.layoutinflater.adapters.android.widget.ButtonAttrAdapter```. In this way, we don't
have to rewrite whole logic again to implement adapters for every other widget.
### Naming an adapter
If you look at the package name and the simple name of the adapter classes mentioned above, you'll
find that there is a specific way an adapter is named.
An adpater should be named in the following way :
```com.itsaky.layoutinflater.adapters.<package_name>.<simple_name>AttrAdapter```,
where, ```<package_name>``` is the package name of the widget you are implementing an adapter for
and ```<simple_name>``` is the simple name of the widget. The simple name is then suffixed
with ```AttrAdapter```.
Here is an example :
Assume we have to implement an adapter for ```android.widget.Button```. Here, ```<package_name>```
is ```android.widget``` and
```<simple_name>``` is ```Button```.
So, the adapter implentation class should
be ```com.itsaky.layoutinflater.adapters.android.widget.ButtonAttrAdapter```.
### Why such naming?
We use reflection to automatically create an adapter when inflating a view. For example,
if ```android.widget.Button``` instance is being created, we could simply create an adapter instance
using reflection by getting the package name and simple name of the widget.
See ```XMLLayoutInflater#onCreateAttributeAdapter(View)```.
### Precautions
- Make sure you correctly spell the file names. As you might already know, file names are case
sensitive.
- Some methods (or even classes) are not available for earlier versions of Android. The classes or
methods you're using must be compatible with API 24.
You could also check if you can use a method. Some methods are already implemented in
ViewAttrAdapter so you won't have to rewrite them. For example:
```
isApi26() // Android 8 (Oreo)
isApi28() // Android 9 (Pie)
isApi29() // Android 10 (Q)
isApi30() // Android 11 (R)
```
@@ -1,121 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.gesture;
import android.gesture.GestureOverlayView;
import android.util.DisplayMetrics;
import android.view.View;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.ReflectUtils;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.adapters.android.widget.FrameLayoutAttrAdapter;
/**
* Attribute adapter for {@link GestureOverlayView}
*
* @author Akash Yadav
*/
public class GestureOverlayViewAttrAdapter extends FrameLayoutAttrAdapter {
public GestureOverlayViewAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof GestureOverlayView;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final var gesture = (GestureOverlayView) view;
final var name = attribute.getAttributeName();
final var value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "eventsInterceptionEnabled":
gesture.setEventsInterceptionEnabled(parseBoolean(value));
break;
case "fadeDuration":
ReflectUtils.reflect(gesture).field("mFadeDuration", parseLong(value, 150));
break;
case "fadeEnabled":
gesture.setFadeEnabled(parseBoolean(value));
break;
case "fadeOffset":
gesture.setFadeOffset(parseLong(value, 420));
break;
case "gestureColor":
gesture.setGestureColor(parseColor(value, gesture.getContext()));
break;
case "gestureStrokeAngleThreshold":
gesture.setGestureStrokeAngleThreshold(parseFloat(value));
break;
case "gestureStrokeLengthThreshold":
gesture.setGestureStrokeLengthThreshold(parseFloat(value));
break;
case "gestureStrokeSquarenessThreshold":
gesture.setGestureStrokeSquarenessTreshold(parseFloat(value));
break;
case "gestureStrokeType":
gesture.setGestureStrokeType(parseGestureStrokeType(value));
break;
case "gestureStrokeWidth":
gesture.setGestureStrokeWidth(parseFloat(value));
break;
case "orientation":
gesture.setOrientation(parseOrientation(value));
break;
case "uncertainGestureColor":
gesture.setUncertainGestureColor(parseColor(value, gesture.getContext()));
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
private int parseOrientation(String value) {
if ("horizontal".equals(value)) {
return GestureOverlayView.ORIENTATION_HORIZONTAL;
}
return GestureOverlayView.ORIENTATION_VERTICAL;
}
protected int parseGestureStrokeType(String value) {
if ("multiple".equals(value)) {
return GestureOverlayView.GESTURE_STROKE_TYPE_MULTIPLE;
}
return GestureOverlayView.GESTURE_STROKE_TYPE_SINGLE;
}
}
@@ -1,595 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.inflater.adapters.android.view;
import android.content.Context;
import android.graphics.PorterDuff;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.NonNull;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IAttributeAdapter;
import com.itsaky.inflater.IDTable;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.util.CommonParseUtils;
import com.itsaky.xml.INamespace;
/**
* Handles attributes common to all android views. Also, LayoutParams related issues are also
* handled by this adapter.
*
* @author Akash Yadav
*/
public class ViewAttrAdapter extends CommonParseUtils implements IAttributeAdapter {
protected static final ILogger LOG = ILogger.newInstance("BaseViewAttrAdapter");
public ViewAttrAdapter(@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public void setResourceFinder(IResourceTable resourceFinder) {
this.resourceFinder = resourceFinder;
}
@Override
public boolean isApplicableTo(View view) {
return true; // Can be applied to any view
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final INamespace namespace = attribute.getNamespace();
final String name = attribute.getAttributeName();
final String value = attribute.getValue();
final ViewGroup.LayoutParams params = view.getLayoutParams();
final Context context = view.getContext();
if (!canHandleNamespace(namespace)) {
return false;
}
boolean handled = true;
switch (name) {
case "layout_height":
params.height = parseDimension(value, -2, displayMetrics);
break;
case "layout_width":
params.width = parseDimension(value, -2, displayMetrics);
break;
case "alpha":
view.setAlpha(parseFloat(value));
break;
case "background":
view.setBackground(parseDrawable(value, context));
break;
case "backgroundTint":
view.setBackgroundTintList(parseColorStateList(value, context));
break;
case "backgroundTintMode":
view.setBackgroundTintMode(parsePorterDuffMode(value));
break;
case "clipToOutline":
view.setClipToOutline(parseBoolean(value));
break;
case "contentDescription":
view.setContentDescription(parseString(value));
break;
case "contextClickable":
view.setContextClickable(parseBoolean(value));
break;
case "defaultFocusHighlightEnabled":
view.setDefaultFocusHighlightEnabled(parseBoolean(value));
break;
case "drawingCacheQuality":
view.setDrawingCacheQuality(parseDrawingCacheQuality(value));
break;
case "duplicateParentState":
view.setDuplicateParentStateEnabled(parseBoolean(value));
break;
case "elevation":
view.setElevation(parseDimension(value, 0, displayMetrics));
break;
case "fadeScrollbars":
view.setScrollbarFadingEnabled(parseBoolean(value));
break;
case "fadingEdgeLength":
view.setFadingEdgeLength(parseDimension(value, 0, displayMetrics));
break;
case "filterTouchesWhenObscured":
view.setFilterTouchesWhenObscured(parseBoolean(value));
break;
case "foreground":
view.setForeground(parseDrawable(value, context));
break;
case "foregroundGravity":
view.setForegroundGravity(parseGravity(value));
break;
case "foregroundTint":
view.setForegroundTintList(parseColorStateList(value, context));
break;
case "foregroundTintMode":
view.setForegroundTintMode(parsePorterDuffMode(value));
break;
case "id":
view.setId(parseId(value));
break;
case "minHeight":
view.setMinimumHeight(parseDimension(value, 0, displayMetrics));
break;
case "minWidth":
view.setMinimumWidth(parseDimension(value, 0, displayMetrics));
break;
case "padding":
final int padding = parseDimension(value, 0, displayMetrics);
view.setPaddingRelative(padding, padding, padding, padding);
break;
case "paddingLeft":
final int paddingLeft = parseDimension(value, 0, displayMetrics);
view.setPadding(
paddingLeft, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom());
break;
case "paddingTop":
final int paddingTop = parseDimension(value, 0, displayMetrics);
view.setPadding(
view.getPaddingLeft(), paddingTop, view.getPaddingRight(), view.getPaddingBottom());
break;
case "paddingRight":
final int paddingRight = parseDimension(value, 0, displayMetrics);
view.setPadding(
view.getPaddingLeft(), view.getPaddingTop(), paddingRight, view.getPaddingBottom());
break;
case "paddingBottom":
final int paddingBottom = parseDimension(value, 0, displayMetrics);
view.setPadding(
view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), paddingBottom);
break;
case "paddingStart":
final int paddingStart = parseDimension(value, 0, displayMetrics);
view.setPaddingRelative(
paddingStart, view.getPaddingTop(), view.getPaddingEnd(), view.getPaddingBottom());
break;
case "paddingEnd":
final int paddingEnd = parseDimension(value, 0, displayMetrics);
view.setPaddingRelative(
view.getPaddingStart(), view.getPaddingTop(), paddingEnd, view.getPaddingBottom());
break;
case "rotation":
view.setRotation(parseFloat(value));
break;
case "rotationX":
view.setRotationX(parseFloat(value));
break;
case "rotationY":
view.setRotationY(parseFloat(value));
break;
case "scaleX":
view.setScaleX(parseFloat(value));
break;
case "scaleY":
view.setScaleY(parseFloat(value));
break;
case "scrollX":
view.setScrollX((int) parseFloat(value));
break;
case "scrollY":
view.setScrollY((int) parseFloat(value));
break;
case "textAlignment":
view.setTextAlignment(parseTextAlignment(value));
break;
case "textDirection":
view.setTextDirection(parseTextDirection(value));
break;
case "tooltipText":
view.setTooltipText(parseString(value));
break;
case "transformPivotX":
view.setPivotX(parseFloat(value));
break;
case "transformPivotY":
view.setPivotY(parseFloat(value));
break;
case "translationX":
view.setTranslationX(parseFloat(value));
break;
case "translationY":
view.setTranslationY(parseFloat(value));
break;
case "translationZ":
view.setTranslationZ(parseFloat(value));
break;
case "visibility":
view.setVisibility(parseVisibility(value));
break;
default:
handled = false;
break;
}
// ----- Handle attributes related to parent ------
if (!handled && params instanceof LinearLayout.LayoutParams) {
handled =
handleLinearLayoutParams((LinearLayout.LayoutParams) params, name, value, displayMetrics);
}
if (!handled && params instanceof RelativeLayout.LayoutParams) {
handled = handleRelativeLayoutParams((RelativeLayout.LayoutParams) params, name, value);
}
if (!handled && params instanceof FrameLayout.LayoutParams) {
handled = handleFrameLayoutParams((FrameLayout.LayoutParams) params, name, value);
}
if (!handled && params instanceof ViewGroup.MarginLayoutParams) {
handled =
handleMarginParams((ViewGroup.MarginLayoutParams) params, name, value, displayMetrics);
}
view.setLayoutParams(params);
return handled;
}
protected int parseId(@NonNull String value) {
if (value.startsWith("@id/")) {
final String name = value.substring("@id/".length());
return IDTable.getId(name);
} else if (value.startsWith("@+id/")) {
final String name = value.substring("@+id/".length());
return IDTable.newId(name);
}
return View.NO_ID;
}
protected boolean handleRelativeLayoutParams(
RelativeLayout.LayoutParams params, @NonNull String name, String value) {
boolean handled = true;
switch (name) {
case "layout_above":
params.addRule(RelativeLayout.ABOVE, parseId(value));
break;
case "layout_alignBaseline":
params.addRule(RelativeLayout.ALIGN_BASELINE, parseId(value));
break;
case "layout_alignBottom":
params.addRule(RelativeLayout.ALIGN_BOTTOM, parseId(value));
break;
case "layout_alignEnd":
params.addRule(RelativeLayout.ALIGN_END, parseId(value));
break;
case "layout_alignLeft":
params.addRule(RelativeLayout.ALIGN_LEFT, parseId(value));
break;
case "layout_alignParentTop":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.ALIGN_PARENT_TOP, params);
break;
case "layout_alignParentBottom":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.ALIGN_PARENT_BOTTOM, params);
break;
case "layout_alignParentStart":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.ALIGN_PARENT_START, params);
break;
case "layout_alignParentEnd":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.ALIGN_PARENT_END, params);
break;
case "layout_alignParentLeft":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.ALIGN_PARENT_LEFT, params);
break;
case "layout_alignParentRight":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.ALIGN_PARENT_RIGHT, params);
break;
case "layout_alignRight":
params.addRule(RelativeLayout.ALIGN_RIGHT, parseId(value));
break;
case "layout_alignStart":
params.addRule(RelativeLayout.ALIGN_START, parseId(value));
break;
case "layout_alignTop":
params.addRule(RelativeLayout.ALIGN_TOP, parseId(value));
break;
case "layout_alignWithParentIfMissing":
params.alignWithParent = parseBoolean(value);
break;
case "layout_below":
params.addRule(RelativeLayout.BELOW, parseId(value));
break;
case "layout_centerHorizontal":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.CENTER_HORIZONTAL, params);
break;
case "layout_centerInParent":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.CENTER_IN_PARENT, params);
break;
case "layout_centerVertical":
setRelativeRuleIfTrue(parseBoolean(value), RelativeLayout.CENTER_VERTICAL, params);
break;
case "layout_toEndOf":
params.addRule(RelativeLayout.END_OF, parseId(value));
break;
case "layout_toStartOf":
params.addRule(RelativeLayout.START_OF, parseId(value));
break;
case "layout_toLeftOf":
params.addRule(RelativeLayout.LEFT_OF, parseId(value));
break;
case "layout_toRightOf":
params.addRule(RelativeLayout.RIGHT_OF, parseId(value));
break;
default:
handled = false;
}
return handled;
}
protected boolean handleLinearLayoutParams(
LinearLayout.LayoutParams params, @NonNull String name, String value, DisplayMetrics dm) {
boolean handled = true;
switch (name) {
case "layout_gravity":
params.gravity = parseGravity(value);
break;
case "layout_weight":
params.weight = parseDimension(value, 1, dm);
break;
default:
handled = false;
break;
}
return handled;
}
protected boolean handleFrameLayoutParams(
FrameLayout.LayoutParams params, @NonNull String name, String value) {
boolean handled = true;
if ("layout_gravity".equals(name)) {
params.gravity = parseGravity(value);
} else {
handled = false;
}
return handled;
}
protected boolean handleMarginParams(
ViewGroup.MarginLayoutParams params, @NonNull String name, String value, DisplayMetrics dm) {
boolean handled = true;
switch (name) {
case "layout_margin":
final int margin = parseDimension(value, 0, dm);
params.setMargins(margin, margin, margin, margin);
break;
case "layout_marginLeft":
params.leftMargin = parseDimension(value, 0, dm);
break;
case "layout_marginTop":
params.topMargin = parseDimension(value, 0, dm);
break;
case "layout_marginRight":
params.rightMargin = parseDimension(value, 0, dm);
break;
case "layout_marginBottom":
params.bottomMargin = parseDimension(value, 0, dm);
break;
case "layout_marginStart":
params.setMarginStart(parseDimension(value, 0, dm));
break;
case "layout_marginEnd":
params.setMarginEnd(parseDimension(value, 0, dm));
break;
default:
handled = false;
break;
}
return handled;
}
protected boolean canHandleNamespace(@NonNull IAttribute attr) {
return canHandleNamespace(attr.getNamespace());
}
protected boolean canHandleNamespace(INamespace namespace) {
return INamespace.ANDROID.equals(namespace);
}
protected int parseDrawingCacheQuality(@NonNull String value) {
switch (value) {
case "high":
return View.DRAWING_CACHE_QUALITY_HIGH;
case "low":
return View.DRAWING_CACHE_QUALITY_LOW;
case "auto":
default:
return View.DRAWING_CACHE_QUALITY_AUTO;
}
}
protected int parseVisibility(@NonNull String value) {
switch (value) {
case "gone":
return View.GONE;
case "invisible":
return View.INVISIBLE;
case "visible":
default:
return View.VISIBLE;
}
}
protected int parseTextAlignment(@NonNull String value) {
switch (value) {
case "center":
return View.TEXT_ALIGNMENT_CENTER;
case "gravity":
return View.TEXT_ALIGNMENT_GRAVITY;
case "textEnd":
return View.TEXT_ALIGNMENT_TEXT_END;
case "textStart":
return View.TEXT_ALIGNMENT_TEXT_START;
case "viewEnd":
return View.TEXT_ALIGNMENT_VIEW_END;
case "viewStart":
return View.TEXT_ALIGNMENT_VIEW_START;
case "inherit":
default:
return View.TEXT_ALIGNMENT_INHERIT;
}
}
protected int parseTextDirection(@NonNull String value) {
switch (value) {
case "anyRtl":
return View.TEXT_DIRECTION_ANY_RTL;
case "firstStrong":
return View.TEXT_DIRECTION_FIRST_STRONG;
case "firstStrongLtr":
return View.TEXT_DIRECTION_FIRST_STRONG_LTR;
case "firstStrongRtl":
return View.TEXT_DIRECTION_FIRST_STRONG_RTL;
case "locale":
return View.TEXT_DIRECTION_LOCALE;
case "ltr":
return View.TEXT_DIRECTION_LTR;
case "rtl":
return View.TEXT_DIRECTION_RTL;
case "inherit":
default:
return View.TEXT_DIRECTION_INHERIT;
}
}
protected PorterDuff.Mode parsePorterDuffMode(@NonNull String mode) {
switch (mode) {
case "add":
return PorterDuff.Mode.ADD;
case "multiply":
return PorterDuff.Mode.MULTIPLY;
case "screen":
return PorterDuff.Mode.SCREEN;
case "src_atop":
return PorterDuff.Mode.SRC_ATOP;
case "src_in":
return PorterDuff.Mode.SRC_IN;
case "src_over":
return PorterDuff.Mode.SRC_OVER;
default:
return PorterDuff.Mode.SRC;
}
}
protected float parseFloat(String value) {
try {
return Float.parseFloat(value);
} catch (Throwable th) {
return 1f;
}
}
protected float parseFloat(String value, float def) {
try {
return Float.parseFloat(value);
} catch (Throwable th) {
return def;
}
}
protected long parseLong(String value, long def) {
try {
return Long.parseLong(value);
} catch (Throwable th) {
return def;
}
}
protected int parseDimension(final String value, int defaultValue, final DisplayMetrics dm) {
if (value == null) {
return defaultValue;
}
char c = value.charAt(0);
if (Character.isDigit(c)) {
// A dimension value which starts with a digit. E.g.: 1dp, 12sp, 123px, etc.
StringBuilder dimensionVal = new StringBuilder();
int index = 0;
while (index < value.length() && Character.isDigit(c = value.charAt(index))) {
dimensionVal.append(c);
index++;
}
final int dimen = Integer.parseInt(dimensionVal.toString());
final String dimensionType = value.substring(index);
return (int) TypedValue.applyDimension(getUnitForDimensionType(dimensionType), dimen, dm);
} else if (c == '@') {
String name = value.substring("@dimen/".length());
return parseDimension(resourceFinder.findDimension(name), defaultValue, dm);
} else if (Character.isLetter(c)) {
// This could be one of the following :
// 1. match_parent
// 2. wrap_content
// 3. fill_parent
switch (value) {
case "match_parent":
case "fill_parent":
return ViewGroup.LayoutParams.MATCH_PARENT;
case "wrap_content":
default:
return ViewGroup.LayoutParams.WRAP_CONTENT;
}
}
return defaultValue;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
protected boolean isApi28() {
return Build.VERSION.SDK_INT >= 28;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
protected boolean isApi29() {
return Build.VERSION.SDK_INT >= 29;
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
protected boolean isApi30() {
return Build.VERSION.SDK_INT >= 30;
}
private void setRelativeRuleIfTrue(
boolean condition, int rule, RelativeLayout.LayoutParams params) {
if (condition) {
params.addRule(rule);
} else {
params.removeRule(rule);
}
}
}
@@ -1,111 +0,0 @@
/************************************************************************************
* 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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.inflater.adapters.android.view;
import android.animation.LayoutTransition;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
import org.jetbrains.annotations.Contract;
/**
* An attribute adapter implementation for ViewGroups.
*
* @author Akash Yadav
*/
public class ViewGroupAttrAdapter extends ViewAttrAdapter {
public ViewGroupAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof ViewGroup;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final ViewGroup group = (ViewGroup) view;
final String name = attribute.getAttributeName();
final String value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "animateLayoutChanges":
group.setLayoutTransition(new LayoutTransition());
break;
case "clipChildren":
group.setClipChildren(parseBoolean(value));
break;
case "clipToPadding":
group.setClipToPadding(parseBoolean(value));
break;
case "descendantFocusability":
group.setDescendantFocusability(parseDescendantsFocusability(value));
break;
case "layoutMode":
group.setLayoutMode(parseLayoutMode(value));
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
@Contract(pure = true)
private int parseLayoutMode(String value) {
switch (value) {
case "opticalBounds":
return ViewGroup.LAYOUT_MODE_OPTICAL_BOUNDS;
case "clipBounds":
default:
return ViewGroup.LAYOUT_MODE_CLIP_BOUNDS;
}
}
protected int parseDescendantsFocusability(@NonNull String value) {
switch (value) {
case "beforeDescendants":
return ViewGroup.FOCUS_BEFORE_DESCENDANTS;
case "blocksDescendants":
return ViewGroup.FOCUS_BLOCK_DESCENDANTS;
case "afterDescendants":
default:
return ViewGroup.FOCUS_AFTER_DESCENDANTS;
}
}
}
@@ -1,84 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget
import android.util.DisplayMetrics
import android.view.View
import android.widget.AbsListView
import com.itsaky.inflater.IAttribute
import com.itsaky.inflater.IResourceTable
/**
* Attribute adapter for the [AbsListView] widget.
*
* @author Akash Yadav
*/
open class AbsListViewAttrAdapter(resourceTable: IResourceTable, displayMetrics: DisplayMetrics) :
AdapterViewAttrAdapter(resourceTable, displayMetrics) {
override fun isApplicableTo(view: View?): Boolean {
return view is AbsListView
}
override fun apply(attribute: IAttribute, view: View): Boolean {
val list = view as AbsListView
val context = list.context
val value = attribute.value
if (!canHandleNamespace(attribute)) {
return false
}
var handled = true
when (attribute.attributeName) {
"cacheColorHint" -> list.cacheColorHint = parseColor(value, context)
"choiceMode" -> list.choiceMode = parseChoiceMode(value)
"drawSelectorOnTop" -> list.isDrawSelectorOnTop = parseBoolean(value)
"fastScrollEnabled" -> list.isFastScrollEnabled = parseBoolean(value)
"listSelector" -> list.selector = parseDrawable(value, context)
"smoothScrollbar" -> list.isSmoothScrollbarEnabled = parseBoolean(value)
"stackFromBottom" -> list.isStackFromBottom = parseBoolean(value)
"textFilterEnabled" -> list.isTextFilterEnabled = parseBoolean(value)
"transcriptMode" -> list.transcriptMode = parseTranscriptMode(value)
else -> handled = false
}
if (!handled) {
handled = super.apply(attribute, view)
}
return handled
}
protected open fun parseTranscriptMode(value: String): Int {
return when (value) {
"normal" -> AbsListView.TRANSCRIPT_MODE_NORMAL
"alwaysScroll" -> AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL
else -> AbsListView.TRANSCRIPT_MODE_DISABLED
}
}
protected open fun parseChoiceMode(value: String): Int {
return when (value) {
"multipleChoice" -> AbsListView.CHOICE_MODE_MULTIPLE
"multipleChoiceModal" -> AbsListView.CHOICE_MODE_MULTIPLE_MODAL
"singleChoice" -> AbsListView.CHOICE_MODE_SINGLE
else -> AbsListView.CHOICE_MODE_NONE
}
}
}
@@ -1,83 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.AbsSeekBar;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
/**
* Attribute adapter for handling attributes related to AbsSeekBar.
*
* @author Akash Yadav
*/
public abstract class AbsSeekBarAttrAdapter extends ProgressBarAttrAdapter {
public AbsSeekBarAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof AbsSeekBar;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final var seek = (AbsSeekBar) view;
final var namespace = attribute.getNamespace();
final var name = attribute.getAttributeName();
final var value = attribute.getValue();
if (!canHandleNamespace(namespace)) {
return false;
}
boolean handled = true;
switch (name) {
case "thumbTint":
seek.setThumbTintList(parseColorStateList(value, seek.getContext()));
break;
case "thumbTintMode":
seek.setThumbTintMode(parsePorterDuffMode(value));
break;
case "tickMarkTint":
seek.setTickMarkTintList(parseColorStateList(value, seek.getContext()));
break;
case "tickMarkTintMode":
seek.setTickMarkTintMode(parsePorterDuffMode(value));
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
}
@@ -1,73 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.AbsSpinner;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
public class AbsSpinnerAttrAdapter extends AdapterViewAttrAdapter {
public AbsSpinnerAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof AbsSpinner;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final var spinner = (AbsSpinner) view;
final var context = spinner.getContext();
final var namespace = attribute.getNamespace();
final var name = attribute.getAttributeName();
final var value = attribute.getValue();
boolean handled = true;
if (!canHandleNamespace(namespace)) {
return false;
}
switch (name) {
case "entries":
final var array = parseArray(value);
final var adapter = newSimpleAdapter(context, array);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
}
@@ -1,70 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
/**
* Adapter for handling attributes related to AdapterView.
*
* @author Akash Yadav
*/
public abstract class AdapterViewAttrAdapter extends ViewGroupAttrAdapter {
public AdapterViewAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof AdapterView;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
// No special attributes for AdapterView
return super.apply(attribute, view);
}
protected ArrayAdapter<String> newSimpleAdapter(Context ctx) {
return newSimpleAdapter(ctx, newAdapterItems(4));
}
protected ArrayAdapter<String> newSimpleAdapter(Context ctx, String[] items) {
return new ArrayAdapter<String>(ctx, android.R.layout.simple_list_item_1, items);
}
protected String[] newAdapterItems(int size) {
final var items = new String[size];
for (var i = 0; i < size; i++) {
items[i] = "Item " + i;
}
return items;
}
}
@@ -1,53 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.Button;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
/**
* Adapter for handling attributes related to Button.
*
* <p>However, {@code android.widget.Button} has no special attributes. This adpater simply inherits
* from {@link TextViewAttrAdapter}.
*
* @author Akash Yadav
*/
public class ButtonAttrAdapter extends TextViewAttrAdapter {
public ButtonAttrAdapter(@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof Button;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
// No special attributes for Button
return super.apply(attribute, view);
}
}
@@ -1,47 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.CheckBox;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
/** Attribute handler for handling attributes related to CheckBox */
public class CheckBoxAttrAdapter extends CompondButtonAttrAdapter {
public CheckBoxAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof CheckBox;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
// No special attributes for CheckBox
return super.apply(attribute, view);
}
}
@@ -1,79 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.CheckedTextView;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
/**
* Attribute handler for handling attributes related to CheckedTextView.
*
* @author Akash Yadav
*/
public class CheckedTextViewAttrAdapter extends TextViewAttrAdapter {
public CheckedTextViewAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof CheckedTextView;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final CheckedTextView text = (CheckedTextView) view;
final String name = attribute.getAttributeName();
final String value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "checkMarkTintMode":
text.setCheckMarkTintMode(parsePorterDuffMode(value));
break;
case "checkMarkTint":
text.setCheckMarkTintList(parseColorStateList(value, text.getContext()));
break;
case "checkMark":
// Ignored...
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
}
@@ -1,64 +0,0 @@
package com.itsaky.inflater.adapters.android.widget;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.CompoundButton;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
/**
* Attribute handler for handling attibutes to CompoundButton
*
* @author Akash Yadav
*/
public class CompondButtonAttrAdapter extends ButtonAttrAdapter {
public CompondButtonAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof CompoundButton;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final CompoundButton button = (CompoundButton) view;
final Context context = button.getContext();
final String name = attribute.getAttributeName();
final String value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "buttonTint":
button.setButtonTintList(parseColorStateList(value, context));
break;
case "button":
button.setButtonDrawable(parseDrawable(value, context));
break;
case "buttonTintMode":
button.setButtonTintMode(parsePorterDuffMode(value));
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
}
@@ -1,46 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
public class EditTextAttrAdapter extends TextViewAttrAdapter {
public EditTextAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof EditText;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
// No special attributes for EditText
return super.apply(attribute, view);
}
}
@@ -1,75 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
/**
* Attribute adapter for {@link android.widget.FrameLayout}
*
* @author Akash Yadav
*/
public class FrameLayoutAttrAdapter extends ViewGroupAttrAdapter {
public FrameLayoutAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof FrameLayout;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final var frame = (FrameLayout) view;
final var name = attribute.getAttributeName();
final var value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "foregroundGravity":
frame.setForegroundGravity(parseGravity(value));
break;
case "measureAllChildren":
frame.setMeasureAllChildren(parseBoolean(value));
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
}
@@ -1,105 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.GridLayout;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
/**
* @author Akash Yadav
*/
public class GridLayoutAttrAdapter extends ViewGroupAttrAdapter {
public GridLayoutAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof GridLayout;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final var grid = (GridLayout) view;
final var name = attribute.getAttributeName();
final var value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "alignmentMode":
grid.setAlignmentMode(parseAlignmentMode(value));
break;
case "columnCount":
grid.setColumnCount(parseInteger(value, Integer.MIN_VALUE));
break;
case "columnOrderPreserved":
grid.setColumnOrderPreserved(parseBoolean(value));
break;
case "orientation":
grid.setOrientation(parseOrientation(value));
break;
case "rowCount":
grid.setRowCount(parseInteger(value, Integer.MIN_VALUE));
break;
case "rowOrderPreserved":
grid.setRowOrderPreserved(parseBoolean(value));
break;
case "useDefaultMargins":
grid.setUseDefaultMargins(parseBoolean(value));
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
protected int parseOrientation(String value) {
if ("vertical".equals(value)) {
return GridLayout.VERTICAL;
}
return GridLayout.HORIZONTAL;
}
protected int parseAlignmentMode(String value) {
if ("alignBounds".equals(value)) {
return GridLayout.ALIGN_BOUNDS;
}
return GridLayout.ALIGN_MARGINS;
}
}
@@ -1,50 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.ImageButton;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
/**
* Attribute handler for handling attributes related to ImageButton.
*
* @author Akash Yadav
*/
public class ImageButtonAttrAdapter extends ImageViewAttrAdapter {
public ImageButtonAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof ImageButton;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
// No special attributes for ImageButton
return super.apply(attribute, view);
}
}
@@ -1,135 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter;
/**
* Attribute handler for handling attributes related to ImageButton.
*
* @author Akash Yadav
*/
public class ImageViewAttrAdapter extends ViewAttrAdapter {
public ImageViewAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof ImageView;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final ImageView image = (ImageView) view;
final Context context = image.getContext();
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
final String name = attribute.getAttributeName();
final String value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "adjustViewBounds":
image.setAdjustViewBounds(parseBoolean(value));
break;
case "baseline":
image.setBaseline(parseDimension(value, 0, dm));
break;
case "baselineAlignBottom":
image.setBaselineAlignBottom(parseBoolean(value));
break;
case "cropToPadding":
image.setCropToPadding(parseBoolean(value));
break;
case "maxHeight":
image.setMaxHeight(parseDimension(value, 0, dm));
break;
case "maxWidth":
image.setMaxWidth(parseDimension(value, 0, dm));
break;
case "scaleType":
image.setScaleType(parseScaleType(value));
break;
case "src":
image.setImageDrawable(parseImageResource(value, context));
break;
case "tint":
image.setImageTintList(parseColorStateList(value, context));
break;
case "tintMode":
image.setImageTintMode(parsePorterDuffMode(value));
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
private Drawable parseImageResource(String value, Context context) {
if (value.startsWith("@drawable/") || value.startsWith("@mipmap/")) {
return parseDrawable(value, context);
}
return ContextCompat.getDrawable(context, android.R.drawable.ic_delete);
}
private ImageView.ScaleType parseScaleType(String value) {
switch (value) {
case "center":
return ImageView.ScaleType.CENTER;
case "centerCrop":
return ImageView.ScaleType.CENTER_CROP;
case "centerInside":
return ImageView.ScaleType.CENTER_INSIDE;
case "fitEnd":
return ImageView.ScaleType.FIT_END;
case "fitStart":
return ImageView.ScaleType.FIT_START;
case "fitXY":
return ImageView.ScaleType.FIT_XY;
case "matrix":
return ImageView.ScaleType.MATRIX;
case "fitCenter":
default:
return ImageView.ScaleType.FIT_CENTER;
}
}
}
@@ -1,98 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter;
/**
* Adapter for handling attributes related to LinearLayout.
*
* @author Akash Yadav
*/
public class LinearLayoutAttrAdapter extends ViewGroupAttrAdapter {
public LinearLayoutAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof LinearLayout;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final LinearLayout linear = (LinearLayout) view;
final String name = attribute.getAttributeName();
final String value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "baselineAligned":
linear.setBaselineAligned(parseBoolean(value));
break;
case "baselineAlignedChildIndex":
linear.setBaselineAlignedChildIndex(parseInteger(value, linear.getChildCount()));
break;
case "gravity":
linear.setGravity(parseGravity(value));
break;
case "measureWithLargestChild":
linear.setMeasureWithLargestChildEnabled(parseBoolean(value));
break;
case "orientation":
linear.setOrientation(parseOrientation(value));
break;
case "weightSum":
linear.setWeightSum(parseFloat(value));
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
protected int parseOrientation(@NonNull String value) {
switch (value) {
case "vertical":
return LinearLayout.VERTICAL;
case "horizontal":
default:
return LinearLayout.HORIZONTAL;
}
}
}
@@ -1,65 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget
import android.util.DisplayMetrics
import android.view.View
import android.widget.ArrayAdapter
import android.widget.ListView
import com.blankj.utilcode.util.SizeUtils
import com.itsaky.inflater.IAttribute
import com.itsaky.inflater.IResourceTable
/** @author Akash Yadav */
class ListViewAttrAdapter(resourceTable: IResourceTable, displayMetrics: DisplayMetrics) :
AbsListViewAttrAdapter(resourceTable, displayMetrics) {
override fun isApplicableTo(view: View?): Boolean {
return view is ListView
}
override fun apply(attribute: IAttribute, view: View): Boolean {
val list = view as ListView
val value = attribute.value
val context = list.context
if (!canHandleNamespace(attribute)) {
return false
}
var handled = true
when (attribute.attributeName) {
"divider" -> list.divider = parseDrawable(value, context)
"dividerHeight" ->
list.dividerHeight = parseDimension(value, SizeUtils.dp2px(1f), displayMetrics)
"entries" -> {
val entries = parseArray(value)
list.adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, entries)
}
"footerDividersEnabled" -> list.setFooterDividersEnabled(parseBoolean(value))
"headerDividersEnabled" -> list.setHeaderDividersEnabled(parseBoolean(value))
}
if (!handled) {
handled = super.apply(attribute, view)
}
return handled
}
}
@@ -1,139 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.inflater.adapters.android.widget;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import com.itsaky.inflater.IAttribute;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter;
/**
* Attribute adapter for handling attributes related to ProgressBar.
*
* @author Akash Yadav
*/
public class ProgressBarAttrAdapter extends ViewAttrAdapter {
public ProgressBarAttrAdapter(
@NonNull IResourceTable resourceFinder, DisplayMetrics displayMetrics) {
super(resourceFinder, displayMetrics);
}
@Override
public boolean isApplicableTo(View view) {
return view instanceof ProgressBar;
}
@Override
public boolean apply(@NonNull IAttribute attribute, @NonNull View view) {
final ProgressBar pb = (ProgressBar) view;
final Context context = pb.getContext();
final DisplayMetrics dm = context.getResources().getDisplayMetrics();
final String name = attribute.getAttributeName();
final String value = attribute.getValue();
if (!canHandleNamespace(attribute)) {
return false;
}
boolean handled = true;
switch (name) {
case "indeterminate":
pb.setIndeterminate(parseBoolean(value));
break;
case "indeterminateDrawable":
pb.setIndeterminateDrawable(parseDrawable(value, context));
break;
case "indeterminateTint":
pb.setIndeterminateTintList(parseColorStateList(value, context));
break;
case "indeterminateTintMode":
pb.setIndeterminateTintMode(parsePorterDuffMode(value));
break;
case "max":
pb.setMax(parseInteger(value, 100));
break;
case "maxHeight":
if (isApi29()) {
pb.setMaxHeight(parseDimension(value, Integer.MAX_VALUE, dm));
}
break;
case "maxWidth":
if (isApi29()) {
pb.setMaxWidth(parseDimension(value, Integer.MAX_VALUE, dm));
}
break;
case "min":
pb.setMin(parseInteger(value, 0));
break;
case "minHeight":
if (isApi29()) {
pb.setMinHeight(parseDimension(value, 0, dm));
}
break;
case "minWidth":
if (isApi29()) {
pb.setMinWidth(parseDimension(value, 0, dm));
}
break;
case "progress":
pb.setProgress(parseInteger(value, 50));
break;
case "progressBackgroundTint":
pb.setProgressBackgroundTintList(parseColorStateList(value, context));
break;
case "progressBackgroundTintMode":
pb.setProgressBackgroundTintMode(parsePorterDuffMode(value));
break;
case "progressDrawable":
pb.setProgressDrawable(parseDrawable(value, context));
break;
case "progressTint":
pb.setProgressTintList(parseColorStateList(value, context));
break;
case "progressTintMode":
pb.setProgressTintMode(parsePorterDuffMode(value));
break;
case "secondaryProgress":
pb.setSecondaryProgress(parseInteger(value, 0));
break;
case "secondaryProgressTint":
pb.setSecondaryProgressTintList(parseColorStateList(value, context));
break;
case "secondaryProgressTintMode":
pb.setSecondaryProgressTintMode(parsePorterDuffMode(value));
break;
default:
handled = false;
break;
}
if (!handled) {
handled = super.apply(attribute, view);
}
return handled;
}
}

Some files were not shown because too many files have changed in this diff Show More