fix: use wrap layout for options in search dialog, add size limit option
This commit is contained in:
@@ -423,6 +423,15 @@ public class Utils {
|
||||
return collection.iterator().next();
|
||||
}
|
||||
|
||||
public static <T> boolean isSetContainsAny(Set<T> inputSet, Set<T> searchKeys) {
|
||||
for (T t : inputSet) {
|
||||
if (searchKeys.contains(t)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static <T> T first(List<T> list) {
|
||||
if (list.isEmpty()) {
|
||||
|
||||
@@ -15,9 +15,11 @@ import jadx.gui.utils.NLS;
|
||||
|
||||
public class SearchSettings {
|
||||
private final String searchString;
|
||||
private final boolean useRegex;
|
||||
private final boolean ignoreCase;
|
||||
private final String searchPkgStr;
|
||||
private boolean useRegex;
|
||||
private boolean ignoreCase;
|
||||
private String searchPkgStr;
|
||||
private String resFilterStr;
|
||||
private int resSizeLimit; // in MB
|
||||
|
||||
private JClass activeCls;
|
||||
private JResource activeResource;
|
||||
@@ -25,15 +27,11 @@ public class SearchSettings {
|
||||
private ISearchMethod searchMethod;
|
||||
private JavaPackage searchPackage;
|
||||
|
||||
public SearchSettings(String searchString, boolean ignoreCase, boolean useRegex, String searchPkgStr) {
|
||||
public SearchSettings(String searchString) {
|
||||
this.searchString = searchString;
|
||||
this.useRegex = useRegex;
|
||||
this.ignoreCase = ignoreCase;
|
||||
this.searchPkgStr = searchPkgStr;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String prepare(MainWindow mainWindow) {
|
||||
public @Nullable String prepare(MainWindow mainWindow) {
|
||||
if (useRegex) {
|
||||
try {
|
||||
int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0;
|
||||
@@ -62,10 +60,18 @@ public class SearchSettings {
|
||||
return this.useRegex;
|
||||
}
|
||||
|
||||
public void setUseRegex(boolean useRegex) {
|
||||
this.useRegex = useRegex;
|
||||
}
|
||||
|
||||
public boolean isIgnoreCase() {
|
||||
return this.ignoreCase;
|
||||
}
|
||||
|
||||
public void setIgnoreCase(boolean ignoreCase) {
|
||||
this.ignoreCase = ignoreCase;
|
||||
}
|
||||
|
||||
public JavaPackage getSearchPackage() {
|
||||
return this.searchPackage;
|
||||
}
|
||||
@@ -74,6 +80,10 @@ public class SearchSettings {
|
||||
return cls.getJavaPackage().isDescendantOf(searchPackage);
|
||||
}
|
||||
|
||||
public void setSearchPkgStr(String searchPkgStr) {
|
||||
this.searchPkgStr = searchPkgStr;
|
||||
}
|
||||
|
||||
public String getSearchString() {
|
||||
return this.searchString;
|
||||
}
|
||||
@@ -101,4 +111,20 @@ public class SearchSettings {
|
||||
public ISearchMethod getSearchMethod() {
|
||||
return searchMethod;
|
||||
}
|
||||
|
||||
public String getResFilterStr() {
|
||||
return resFilterStr;
|
||||
}
|
||||
|
||||
public void setResFilterStr(String resFilterStr) {
|
||||
this.resFilterStr = resFilterStr;
|
||||
}
|
||||
|
||||
public int getResSizeLimit() {
|
||||
return resSizeLimit;
|
||||
}
|
||||
|
||||
public void setResSizeLimit(int resSizeLimit) {
|
||||
this.resSizeLimit = resSizeLimit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
|
||||
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings, SearchDialog searchDialog) {
|
||||
this.searchSettings = searchSettings;
|
||||
this.sizeLimit = mw.getSettings().getSrhResourceSkipSize() * 1048576;
|
||||
this.extSet = buildAllowedFilesExtensions(mw.getSettings().getSrhResourceFileExt());
|
||||
this.extSet = buildAllowedFilesExtensions(searchSettings.getResFilterStr());
|
||||
this.sizeLimit = searchSettings.getResSizeLimit() * 1024 * 1024;
|
||||
this.searchDialog = searchDialog;
|
||||
JResource activeResource = searchSettings.getActiveResource();
|
||||
if (activeResource != null) {
|
||||
@@ -168,8 +168,13 @@ public class ResourceSearchProvider implements ISearchProvider {
|
||||
}
|
||||
|
||||
private Set<String> buildAllowedFilesExtensions(String srhResourceFileExt) {
|
||||
String str = srhResourceFileExt.trim();
|
||||
if (str.isEmpty() || str.equals("*")) {
|
||||
anyExt = true;
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> set = new HashSet<>();
|
||||
for (String extStr : srhResourceFileExt.split("[|.]")) {
|
||||
for (String extStr : str.split("[|.]")) {
|
||||
String ext = extStr.trim();
|
||||
if (!ext.isEmpty()) {
|
||||
anyExt = ext.equals("*");
|
||||
|
||||
@@ -249,6 +249,22 @@ public class JadxProject {
|
||||
changed();
|
||||
}
|
||||
|
||||
public void setSearchResourcesFilter(String searchResourcesFilter) {
|
||||
data.setSearchResourcesFilter(searchResourcesFilter);
|
||||
}
|
||||
|
||||
public String getSearchResourcesFilter() {
|
||||
return data.getSearchResourcesFilter();
|
||||
}
|
||||
|
||||
public void setSearchResourcesSizeLimit(int searchResourcesSizeLimit) {
|
||||
data.setSearchResourcesSizeLimit(searchResourcesSizeLimit);
|
||||
}
|
||||
|
||||
public int getSearchResourcesSizeLimit() {
|
||||
return data.getSearchResourcesSizeLimit();
|
||||
}
|
||||
|
||||
private void changed() {
|
||||
JadxSettings settings = mainWindow.getSettings();
|
||||
if (settings != null && settings.getSaveOption() == JadxSettings.SAVEOPTION.ALWAYS) {
|
||||
|
||||
@@ -112,7 +112,6 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
private int mainWindowExtendedState = JFrame.NORMAL;
|
||||
private boolean codeAreaLineWrap = false;
|
||||
private int srhResourceSkipSize = 1000;
|
||||
private String srhResourceFileExt = ".xml|.html|.js|.json|.txt";
|
||||
private int searchResultsPerPage = 50;
|
||||
private boolean useAutoSearch = true;
|
||||
private boolean keepCommonDialogOpen = false;
|
||||
@@ -645,14 +644,6 @@ public class JadxSettings extends JadxCLIArgs {
|
||||
srhResourceSkipSize = size;
|
||||
}
|
||||
|
||||
public String getSrhResourceFileExt() {
|
||||
return srhResourceFileExt;
|
||||
}
|
||||
|
||||
public void setSrhResourceFileExt(String all) {
|
||||
srhResourceFileExt = all.trim();
|
||||
}
|
||||
|
||||
public int getSearchResultsPerPage() {
|
||||
return searchResultsPerPage;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ public class ProjectData {
|
||||
private @Nullable String cacheDir; // don't use relative path adapter
|
||||
private boolean enableLiveReload = false;
|
||||
private List<String> searchHistory = new ArrayList<>();
|
||||
private String searchResourcesFilter = "*";
|
||||
private int searchResourcesSizeLimit = 0; // in MB
|
||||
|
||||
protected Map<String, String> pluginOptions = new HashMap<>();
|
||||
|
||||
public List<Path> getFiles() {
|
||||
@@ -101,6 +104,22 @@ public class ProjectData {
|
||||
this.searchHistory = searchHistory;
|
||||
}
|
||||
|
||||
public String getSearchResourcesFilter() {
|
||||
return searchResourcesFilter;
|
||||
}
|
||||
|
||||
public void setSearchResourcesFilter(String searchResourcesFilter) {
|
||||
this.searchResourcesFilter = searchResourcesFilter;
|
||||
}
|
||||
|
||||
public int getSearchResourcesSizeLimit() {
|
||||
return searchResourcesSizeLimit;
|
||||
}
|
||||
|
||||
public void setSearchResourcesSizeLimit(int searchResourcesSizeLimit) {
|
||||
this.searchResourcesSizeLimit = searchResourcesSizeLimit;
|
||||
}
|
||||
|
||||
public Map<String, String> getPluginOptions() {
|
||||
return pluginOptions;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.ScrollPaneConstants;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
@@ -76,7 +75,6 @@ import jadx.gui.utils.LangLocale;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.ui.ActionHandler;
|
||||
import jadx.gui.utils.ui.DocumentUpdateListener;
|
||||
|
||||
public class JadxSettingsWindow extends JDialog {
|
||||
private static final long serialVersionUID = -1804570470377354148L;
|
||||
@@ -138,7 +136,6 @@ public class JadxSettingsWindow extends JDialog {
|
||||
groups.add(new CacheSettingsGroup(this));
|
||||
groups.add(makeAppearanceGroup());
|
||||
groups.add(new ShortcutsSettingsGroup(this, settings));
|
||||
groups.add(makeSearchResGroup());
|
||||
groups.add(makeProjectGroup());
|
||||
groups.add(new PluginSettings(mainWindow, settings).build());
|
||||
groups.add(makeOtherGroup());
|
||||
@@ -638,6 +635,10 @@ public class JadxSettingsWindow extends JDialog {
|
||||
jumpOnDoubleClick.setSelected(settings.isJumpOnDoubleClick());
|
||||
jumpOnDoubleClick.addItemListener(e -> settings.setJumpOnDoubleClick(e.getStateChange() == ItemEvent.SELECTED));
|
||||
|
||||
JSpinner resultsPerPage = new JSpinner(
|
||||
new SpinnerNumberModel(settings.getSearchResultsPerPage(), 0, Integer.MAX_VALUE, 1));
|
||||
resultsPerPage.addChangeListener(ev -> settings.setSearchResultsPerPage((Integer) resultsPerPage.getValue()));
|
||||
|
||||
JCheckBox useAltFileDialog = new JCheckBox();
|
||||
useAltFileDialog.setSelected(settings.isUseAlternativeFileDialog());
|
||||
useAltFileDialog.addItemListener(e -> settings.setUseAlternativeFileDialog(e.getStateChange() == ItemEvent.SELECTED));
|
||||
@@ -683,39 +684,17 @@ public class JadxSettingsWindow extends JDialog {
|
||||
SettingsGroup group = new SettingsGroup(NLS.str("preferences.other"));
|
||||
group.addRow(NLS.str("preferences.lineNumbersMode"), lineNumbersMode);
|
||||
group.addRow(NLS.str("preferences.jumpOnDoubleClick"), jumpOnDoubleClick);
|
||||
group.addRow(NLS.str("preferences.disable_tooltip_on_hover"), disableTooltipOnHover);
|
||||
group.addRow(NLS.str("preferences.search_results_per_page"), resultsPerPage);
|
||||
group.addRow(NLS.str("preferences.useAlternativeFileDialog"), useAltFileDialog);
|
||||
group.addRow(NLS.str("preferences.cfg"), cfg);
|
||||
group.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
|
||||
group.addRow(NLS.str("preferences.xposed_codegen_language"), xposedCodegenLanguage);
|
||||
group.addRow(NLS.str("preferences.check_for_updates"), update);
|
||||
group.addRow(NLS.str("preferences.update_channel"), updateChannel);
|
||||
group.addRow(NLS.str("preferences.disable_tooltip_on_hover"), disableTooltipOnHover);
|
||||
return group;
|
||||
}
|
||||
|
||||
private SettingsGroup makeSearchResGroup() {
|
||||
JSpinner resultsPerPage = new JSpinner(
|
||||
new SpinnerNumberModel(settings.getSearchResultsPerPage(), 0, Integer.MAX_VALUE, 1));
|
||||
resultsPerPage.addChangeListener(ev -> settings.setSearchResultsPerPage((Integer) resultsPerPage.getValue()));
|
||||
|
||||
JSpinner sizeLimit = new JSpinner(
|
||||
new SpinnerNumberModel(settings.getSrhResourceSkipSize(), 0, Integer.MAX_VALUE, 1));
|
||||
sizeLimit.addChangeListener(ev -> settings.setSrhResourceSkipSize((Integer) sizeLimit.getValue()));
|
||||
|
||||
JTextField fileExtField = new JTextField();
|
||||
fileExtField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> {
|
||||
String ext = fileExtField.getText();
|
||||
settings.setSrhResourceFileExt(ext);
|
||||
}));
|
||||
fileExtField.setText(settings.getSrhResourceFileExt());
|
||||
|
||||
SettingsGroup searchGroup = new SettingsGroup(NLS.str("preferences.search_group_title"));
|
||||
searchGroup.addRow(NLS.str("preferences.search_results_per_page"), resultsPerPage);
|
||||
searchGroup.addRow(NLS.str("preferences.res_skip_file"), sizeLimit);
|
||||
searchGroup.addRow(NLS.str("preferences.res_file_ext"), fileExtField);
|
||||
return searchGroup;
|
||||
}
|
||||
|
||||
private void closeGroups(boolean save) {
|
||||
for (ISettingsGroup group : groups) {
|
||||
group.close(save);
|
||||
|
||||
@@ -3,7 +3,7 @@ package jadx.gui.ui.dialog;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridLayout;
|
||||
import java.awt.Insets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
@@ -26,9 +26,12 @@ import javax.swing.JLabel;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.JToggleButton;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.WindowConstants;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import javax.swing.event.ChangeListener;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -46,8 +49,10 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import jadx.api.JavaClass;
|
||||
import jadx.api.JavaPackage;
|
||||
import jadx.core.dex.nodes.PackageNode;
|
||||
import jadx.core.utils.ListUtils;
|
||||
import jadx.core.utils.StringUtils;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.gui.jobs.ITaskInfo;
|
||||
import jadx.gui.jobs.ITaskProgress;
|
||||
import jadx.gui.search.SearchSettings;
|
||||
@@ -63,14 +68,16 @@ import jadx.gui.treemodel.JClass;
|
||||
import jadx.gui.treemodel.JNode;
|
||||
import jadx.gui.treemodel.JResource;
|
||||
import jadx.gui.ui.MainWindow;
|
||||
import jadx.gui.utils.CacheObject;
|
||||
import jadx.gui.utils.Icons;
|
||||
import jadx.gui.utils.JumpPosition;
|
||||
import jadx.gui.utils.NLS;
|
||||
import jadx.gui.utils.SimpleListener;
|
||||
import jadx.gui.utils.TextStandardActions;
|
||||
import jadx.gui.utils.UiUtils;
|
||||
import jadx.gui.utils.cache.ValueCache;
|
||||
import jadx.gui.utils.layout.WrapLayout;
|
||||
import jadx.gui.utils.rx.RxUtils;
|
||||
import jadx.gui.utils.ui.DocumentUpdateListener;
|
||||
|
||||
import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.ACTIVE_TAB;
|
||||
import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CLASS;
|
||||
@@ -92,7 +99,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
}
|
||||
|
||||
public static void searchInActiveTab(MainWindow window, SearchPreset preset) {
|
||||
SearchDialog searchDialog = new SearchDialog(window, preset, EnumSet.of(SearchOptions.ACTIVE_TAB));
|
||||
SearchDialog searchDialog = new SearchDialog(window, preset, EnumSet.of(ACTIVE_TAB));
|
||||
show(searchDialog, window);
|
||||
}
|
||||
|
||||
@@ -138,9 +145,12 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
|
||||
private final transient SearchPreset searchPreset;
|
||||
private final transient Set<SearchOptions> options;
|
||||
private final transient SimpleListener<Set<SearchOptions>> optionsListener = new SimpleListener<>();
|
||||
|
||||
private transient JTextField searchField;
|
||||
private transient JTextField packageField;
|
||||
private transient JTextField resExtField;
|
||||
private transient JSpinner resSizeLimit;
|
||||
|
||||
private transient @Nullable SearchTask searchTask;
|
||||
private transient JButton loadAllButton;
|
||||
@@ -159,7 +169,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
private final List<JNode> pendingResults = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Use single thread to do all background work, so additional synchronisation not needed
|
||||
* Use single thread to do all background work, so additional synchronization not needed
|
||||
*/
|
||||
private final Executor searchBackgroundExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
@@ -209,12 +219,12 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
break;
|
||||
|
||||
case CLASS:
|
||||
searchOptions.add(SearchOptions.CLASS);
|
||||
searchOptions.add(CLASS);
|
||||
break;
|
||||
|
||||
case COMMENT:
|
||||
searchOptions.add(SearchOptions.COMMENT);
|
||||
searchOptions.remove(SearchOptions.ACTIVE_TAB);
|
||||
searchOptions.add(COMMENT);
|
||||
searchOptions.remove(ACTIVE_TAB);
|
||||
break;
|
||||
}
|
||||
return searchOptions;
|
||||
@@ -263,36 +273,29 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
}
|
||||
});
|
||||
|
||||
JPanel searchLinePanel = new JPanel();
|
||||
searchLinePanel.setLayout(new BoxLayout(searchLinePanel, BoxLayout.LINE_AXIS));
|
||||
searchLinePanel.add(searchField);
|
||||
searchLinePanel.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchLinePanel.add(searchBtn);
|
||||
searchLinePanel.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchLinePanel.add(makeOptionsToggleButton(NLS.str("search_dialog.ignorecase"), Icons.ICON_MATCH, Icons.ICON_MATCH_SELECTED,
|
||||
JPanel searchButtons = new JPanel();
|
||||
searchButtons.setLayout(new BoxLayout(searchButtons, BoxLayout.LINE_AXIS));
|
||||
searchButtons.add(searchBtn);
|
||||
searchButtons.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchButtons.add(makeOptionsToggleButton(NLS.str("search_dialog.ignorecase"), Icons.ICON_MATCH, Icons.ICON_MATCH_SELECTED,
|
||||
SearchOptions.IGNORE_CASE));
|
||||
searchLinePanel.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchLinePanel.add(makeOptionsToggleButton(NLS.str("search_dialog.regex"), Icons.ICON_REGEX, Icons.ICON_REGEX_SELECTED,
|
||||
searchButtons.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchButtons.add(makeOptionsToggleButton(NLS.str("search_dialog.regex"), Icons.ICON_REGEX, Icons.ICON_REGEX_SELECTED,
|
||||
SearchOptions.USE_REGEX));
|
||||
searchLinePanel.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchLinePanel.add(makeOptionsToggleButton(NLS.str("search_dialog.active_tab"), Icons.ICON_ACTIVE_TAB,
|
||||
searchButtons.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchButtons.add(makeOptionsToggleButton(NLS.str("search_dialog.active_tab"), Icons.ICON_ACTIVE_TAB,
|
||||
Icons.ICON_ACTIVE_TAB_SELECTED, SearchOptions.ACTIVE_TAB));
|
||||
searchLinePanel.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchLinePanel.add(autoSearchCB);
|
||||
|
||||
searchLinePanel.setAlignmentX(LEFT_ALIGNMENT);
|
||||
|
||||
JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name"));
|
||||
findLabel.setAlignmentX(LEFT_ALIGNMENT);
|
||||
searchButtons.add(Box.createRigidArea(new Dimension(5, 0)));
|
||||
searchButtons.add(autoSearchCB);
|
||||
|
||||
JPanel searchFieldPanel = new JPanel();
|
||||
searchFieldPanel.setLayout(new BoxLayout(searchFieldPanel, BoxLayout.PAGE_AXIS));
|
||||
searchFieldPanel.setAlignmentX(LEFT_ALIGNMENT);
|
||||
searchFieldPanel.add(findLabel);
|
||||
searchFieldPanel.add(Box.createRigidArea(new Dimension(0, 5)));
|
||||
searchFieldPanel.add(searchLinePanel);
|
||||
searchFieldPanel.setLayout(new BorderLayout(5, 5));
|
||||
searchFieldPanel.add(new JLabel(NLS.str("search_dialog.open_by_name")), BorderLayout.LINE_START);
|
||||
searchFieldPanel.add(searchField, BorderLayout.CENTER);
|
||||
searchFieldPanel.add(searchButtons, BorderLayout.LINE_END);
|
||||
|
||||
JPanel searchInPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JPanel searchInPanel = new JPanel();
|
||||
searchInPanel.setLayout(new BoxLayout(searchInPanel, BoxLayout.LINE_AXIS));
|
||||
searchInPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in")));
|
||||
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.class"), SearchOptions.CLASS));
|
||||
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.method"), SearchOptions.METHOD));
|
||||
@@ -301,36 +304,50 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.resource"), SearchOptions.RESOURCE));
|
||||
searchInPanel.add(makeOptionsCheckBox(NLS.str("search_dialog.comments"), SearchOptions.COMMENT));
|
||||
|
||||
packageField = new JTextField();
|
||||
packageField.setMaximumSize(new Dimension(Integer.MAX_VALUE, searchField.getPreferredSize().height));
|
||||
packageField = new JTextField(Math.min(100, getMaxPkgLen()));
|
||||
TextStandardActions.attach(packageField);
|
||||
packageField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
|
||||
packageField.setToolTipText(NLS.str("search_dialog.limit_package"));
|
||||
|
||||
JPanel searchPackageOptions = new JPanel();
|
||||
searchPackageOptions.setLayout(new BoxLayout(searchPackageOptions, BoxLayout.LINE_AXIS));
|
||||
searchPackageOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.limit_package")));
|
||||
searchPackageOptions.add(packageField);
|
||||
JPanel searchPackagePanel = new JPanel(new BorderLayout());
|
||||
searchPackagePanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.limit_package")));
|
||||
searchPackagePanel.add(packageField, BorderLayout.CENTER);
|
||||
Dimension minPanelSize = calcMinSizeForTitledBorder(searchPackagePanel);
|
||||
searchPackagePanel
|
||||
.setPreferredSize(new Dimension(Math.max(packageField.getPreferredSize().width, minPanelSize.width), minPanelSize.height));
|
||||
|
||||
JTextField fileExtField = new JTextField();
|
||||
TextStandardActions.attach(fileExtField);
|
||||
fileExtField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
|
||||
fileExtField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> {
|
||||
String ext = fileExtField.getText();
|
||||
mainWindow.getSettings().setSrhResourceFileExt(ext);
|
||||
}));
|
||||
fileExtField.setText(mainWindow.getSettings().getSrhResourceFileExt());
|
||||
fileExtField.setMaximumSize(new Dimension(Integer.MAX_VALUE, searchField.getPreferredSize().height));
|
||||
resExtField = new JTextField();
|
||||
TextStandardActions.attach(resExtField);
|
||||
resExtField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
|
||||
resExtField.setToolTipText(NLS.str("preferences.res_file_ext"));
|
||||
resExtField.setText(mainWindow.getProject().getSearchResourcesFilter());
|
||||
|
||||
JPanel searchExtFileOptions = new JPanel();
|
||||
searchExtFileOptions.setLayout(new BoxLayout(searchExtFileOptions, BoxLayout.LINE_AXIS));
|
||||
searchExtFileOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.res_file_ext")));
|
||||
searchExtFileOptions.add(fileExtField);
|
||||
JPanel resExtFilePanel = new JPanel(new BorderLayout());
|
||||
resExtFilePanel.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.res_file_ext")));
|
||||
resExtFilePanel.add(resExtField, BorderLayout.CENTER);
|
||||
resExtFilePanel.setPreferredSize(calcMinSizeForTitledBorder(resExtFilePanel));
|
||||
|
||||
JPanel optionsPanel = new JPanel(new GridLayout(2, 2, 5, 5));
|
||||
optionsPanel.setAlignmentX(LEFT_ALIGNMENT);
|
||||
resSizeLimit = new JSpinner(new SpinnerNumberModel(mainWindow.getProject().getSearchResourcesSizeLimit(), 0, Integer.MAX_VALUE, 1));
|
||||
resSizeLimit.setToolTipText(NLS.str("preferences.res_skip_file"));
|
||||
|
||||
JPanel sizeLimitPanel = new JPanel(new BorderLayout());
|
||||
sizeLimitPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.res_skip_file")));
|
||||
sizeLimitPanel.add(resSizeLimit, BorderLayout.CENTER);
|
||||
sizeLimitPanel.setPreferredSize(calcMinSizeForTitledBorder(sizeLimitPanel));
|
||||
|
||||
JPanel optionsPanel = new JPanel(new WrapLayout(FlowLayout.LEFT));
|
||||
optionsPanel.add(searchInPanel);
|
||||
optionsPanel.add(searchPackageOptions);
|
||||
optionsPanel.add(searchExtFileOptions);
|
||||
optionsPanel.add(searchPackagePanel);
|
||||
optionsPanel.add(resExtFilePanel);
|
||||
optionsPanel.add(sizeLimitPanel);
|
||||
|
||||
optionsListener.addListener(searchOptions -> {
|
||||
boolean codeSearch = Utils.isSetContainsAny(searchOptions, EnumSet.of(CODE, CLASS, METHOD, FIELD, COMMENT));
|
||||
searchPackagePanel.setVisible(codeSearch);
|
||||
boolean resSearch = searchOptions.contains(RESOURCE);
|
||||
resExtFilePanel.setVisible(resSearch);
|
||||
sizeLimitPanel.setVisible(resSearch);
|
||||
});
|
||||
|
||||
JPanel searchPane = new JPanel();
|
||||
searchPane.setLayout(new BoxLayout(searchPane, BoxLayout.PAGE_AXIS));
|
||||
@@ -354,6 +371,34 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
}
|
||||
|
||||
private int getMaxPkgLen() {
|
||||
CacheObject cacheObject = mainWindow.getCacheObject();
|
||||
int maxPkgLength = cacheObject.getMaxPkgLength();
|
||||
if (maxPkgLength != 0) {
|
||||
return maxPkgLength;
|
||||
}
|
||||
int max = 1;
|
||||
for (PackageNode pkg : mainWindow.getWrapper().getRootNode().getPackages()) {
|
||||
int len = pkg.getPkgInfo().getFullName().length();
|
||||
if (len > max) {
|
||||
max = len;
|
||||
}
|
||||
}
|
||||
cacheObject.setMaxPkgLength(max);
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround to calculate minimum size for a panel with titled border
|
||||
*/
|
||||
private Dimension calcMinSizeForTitledBorder(JPanel panel) {
|
||||
TitledBorder border = (TitledBorder) panel.getBorder();
|
||||
Insets borderInsets = border.getBorderInsets(panel);
|
||||
int insets = 2 * (borderInsets.left + borderInsets.right);
|
||||
double titleWidth = panel.getFontMetrics(border.getTitleFont()).stringWidth(border.getTitle());
|
||||
return new Dimension((int) titleWidth + insets, panel.getPreferredSize().height);
|
||||
}
|
||||
|
||||
private void addSearchHistoryButton() {
|
||||
JButton searchHistoryButton = new JButton(new FlatSearchWithHistoryIcon(true));
|
||||
searchHistoryButton.setToolTipText(NLS.str("search_dialog.search_history"));
|
||||
@@ -441,17 +486,26 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
RxUtils.textFieldEnterPress(searchField),
|
||||
RxUtils.textFieldChanges(packageField),
|
||||
RxUtils.textFieldEnterPress(packageField),
|
||||
RxUtils.textFieldChanges(resExtField),
|
||||
RxUtils.textFieldEnterPress(resExtField),
|
||||
RxUtils.spinnerChanges(resSizeLimit),
|
||||
RxUtils.spinnerEnterPress(resSizeLimit),
|
||||
searchEmitter.getFlowable()));
|
||||
} else {
|
||||
searchEvents = Flowable.merge(
|
||||
searchEvents = Flowable.merge(List.of(
|
||||
RxUtils.textFieldEnterPress(searchField),
|
||||
RxUtils.textFieldEnterPress(packageField),
|
||||
searchEmitter.getFlowable());
|
||||
RxUtils.textFieldEnterPress(resExtField),
|
||||
RxUtils.spinnerEnterPress(resSizeLimit),
|
||||
searchEmitter.getFlowable()));
|
||||
}
|
||||
searchDisposable = searchEvents
|
||||
.debounce(50, TimeUnit.MILLISECONDS)
|
||||
.observeOn(Schedulers.from(searchBackgroundExecutor))
|
||||
.subscribe(t -> this.search(searchField.getText()));
|
||||
|
||||
// set initial values
|
||||
optionsListener.sendUpdate(options);
|
||||
}
|
||||
|
||||
private void search(String text) {
|
||||
@@ -477,14 +531,17 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
return null;
|
||||
}
|
||||
// allow empty text for search in comments
|
||||
if (text.isEmpty() && !options.contains(SearchOptions.COMMENT)) {
|
||||
if (text.isEmpty() && !options.contains(COMMENT)) {
|
||||
return null;
|
||||
}
|
||||
LOG.debug("Building search for '{}', options: {}", text, options);
|
||||
boolean ignoreCase = options.contains(IGNORE_CASE);
|
||||
boolean useRegex = options.contains(USE_REGEX);
|
||||
String searchPackageText = packageField.getText();
|
||||
SearchSettings searchSettings = new SearchSettings(text, !ignoreCase, useRegex, searchPackageText);
|
||||
SearchSettings searchSettings = new SearchSettings(text);
|
||||
searchSettings.setIgnoreCase(options.contains(IGNORE_CASE));
|
||||
searchSettings.setUseRegex(options.contains(USE_REGEX));
|
||||
searchSettings.setSearchPkgStr(packageField.getText().trim());
|
||||
searchSettings.setResFilterStr(resExtField.getText().trim());
|
||||
searchSettings.setResSizeLimit((Integer) resSizeLimit.getValue());
|
||||
|
||||
String error = searchSettings.prepare(mainWindow);
|
||||
UiUtils.highlightAsErrorField(searchField, !StringUtils.isEmpty(error));
|
||||
if (!StringUtils.isEmpty(error)) {
|
||||
@@ -496,6 +553,9 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
if (!buildSearch(newSearchTask, text, searchSettings)) {
|
||||
return null;
|
||||
}
|
||||
// save search settings
|
||||
mainWindow.getProject().setSearchResourcesFilter(searchSettings.getResFilterStr());
|
||||
mainWindow.getProject().setSearchResourcesSizeLimit(searchSettings.getResSizeLimit());
|
||||
return newSearchTask;
|
||||
}
|
||||
|
||||
@@ -529,7 +589,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
.filter(searchSettings::isInSearchPkg)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
if (text.isEmpty() && options.contains(SearchOptions.COMMENT)) {
|
||||
if (text.isEmpty() && options.contains(COMMENT)) {
|
||||
// allow empty text for comment search
|
||||
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings, searchClasses));
|
||||
return true;
|
||||
@@ -698,7 +758,6 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
|
||||
private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) {
|
||||
final JCheckBox chBox = new JCheckBox(name);
|
||||
chBox.setAlignmentX(LEFT_ALIGNMENT);
|
||||
chBox.setSelected(options.contains(opt));
|
||||
chBox.addItemListener(e -> {
|
||||
if (chBox.isSelected()) {
|
||||
@@ -706,6 +765,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
} else {
|
||||
options.remove(opt);
|
||||
}
|
||||
optionsListener.sendUpdate(options);
|
||||
searchEmitter.emitSearch();
|
||||
});
|
||||
return chBox;
|
||||
@@ -723,6 +783,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
} else {
|
||||
options.remove(opt);
|
||||
}
|
||||
optionsListener.sendUpdate(options);
|
||||
searchEmitter.emitSearch();
|
||||
});
|
||||
return toggleButton;
|
||||
@@ -744,7 +805,7 @@ public class SearchDialog extends CommonSearchDialog {
|
||||
private void registerActiveTabListener() {
|
||||
removeActiveTabListener();
|
||||
activeTabListener = e -> {
|
||||
if (options.contains(SearchOptions.ACTIVE_TAB)) {
|
||||
if (options.contains(ACTIVE_TAB)) {
|
||||
LOG.debug("active tab change event received");
|
||||
searchEmitter.emitSearch();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public class CacheObject {
|
||||
private String lastSearch;
|
||||
private Map<SearchDialog.SearchPreset, Set<SearchDialog.SearchOptions>> lastSearchOptions;
|
||||
private String lastSearchPackage;
|
||||
private int maxPkgLength;
|
||||
|
||||
private volatile boolean fullDecompilationFinished;
|
||||
|
||||
@@ -54,6 +55,14 @@ public class CacheObject {
|
||||
this.lastSearchPackage = lastSearchPackage;
|
||||
}
|
||||
|
||||
public int getMaxPkgLength() {
|
||||
return maxPkgLength;
|
||||
}
|
||||
|
||||
public void setMaxPkgLength(int maxPkgLength) {
|
||||
this.maxPkgLength = maxPkgLength;
|
||||
}
|
||||
|
||||
public JNodeCache getNodeCache() {
|
||||
return jNodeCache;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package jadx.gui.utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class SimpleListener<T> {
|
||||
private final List<Consumer<T>> listeners = new ArrayList<>();
|
||||
|
||||
public void sendUpdate(T data) {
|
||||
for (Consumer<T> listener : listeners) {
|
||||
listener.accept(data);
|
||||
}
|
||||
}
|
||||
|
||||
public void addListener(Consumer<T> listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public boolean removeListener(Consumer<T> listener) {
|
||||
return listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import java.awt.Insets;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import org.intellij.lang.annotations.MagicConstant;
|
||||
|
||||
/**
|
||||
* FlowLayout subclass that fully supports wrapping of components.
|
||||
*/
|
||||
@@ -34,7 +36,7 @@ public class WrapLayout extends FlowLayout {
|
||||
*
|
||||
* @param align the alignment value
|
||||
*/
|
||||
public WrapLayout(int align) {
|
||||
public WrapLayout(@MagicConstant(valuesFromClass = FlowLayout.class) int align) {
|
||||
super(align);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,18 @@ package jadx.gui.utils.rx;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import javax.swing.event.DocumentListener;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.FlowableEmitter;
|
||||
import io.reactivex.rxjava3.core.FlowableOnSubscribe;
|
||||
|
||||
import jadx.gui.utils.ui.DocumentUpdateListener;
|
||||
@@ -26,18 +32,40 @@ public class RxUtils {
|
||||
|
||||
public static Flowable<String> textFieldEnterPress(final JTextField textField) {
|
||||
FlowableOnSubscribe<String> source = emitter -> {
|
||||
KeyListener keyListener = new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent ev) {
|
||||
if (ev.getKeyCode() == KeyEvent.VK_ENTER) {
|
||||
emitter.onNext(textField.getText());
|
||||
}
|
||||
}
|
||||
};
|
||||
KeyListener keyListener = enterKeyListener(emitter, textField::getText);
|
||||
textField.addKeyListener(keyListener);
|
||||
emitter.setDisposable(new CustomDisposable(() -> textField.removeKeyListener(keyListener)));
|
||||
};
|
||||
return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged();
|
||||
}
|
||||
|
||||
public static Flowable<String> spinnerChanges(final JSpinner spinner) {
|
||||
FlowableOnSubscribe<String> source = emitter -> {
|
||||
ChangeListener changeListener = e -> emitter.onNext(String.valueOf(spinner.getValue()));
|
||||
spinner.addChangeListener(changeListener);
|
||||
emitter.setDisposable(new CustomDisposable(() -> spinner.removeChangeListener(changeListener)));
|
||||
};
|
||||
return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged();
|
||||
}
|
||||
|
||||
public static Flowable<String> spinnerEnterPress(final JSpinner spinner) {
|
||||
FlowableOnSubscribe<String> source = emitter -> {
|
||||
KeyListener keyListener = enterKeyListener(emitter, () -> String.valueOf(spinner.getValue()));
|
||||
spinner.addKeyListener(keyListener);
|
||||
emitter.setDisposable(new CustomDisposable(() -> spinner.removeKeyListener(keyListener)));
|
||||
};
|
||||
return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged();
|
||||
}
|
||||
|
||||
private static @NotNull KeyListener enterKeyListener(FlowableEmitter<String> emitter, Supplier<String> supplier) {
|
||||
return new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent ev) {
|
||||
if (ev.getKeyCode() == KeyEvent.VK_ENTER) {
|
||||
emitter.onNext(supplier.get());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.rename_valid=Um sie gültig zu machen
|
||||
preferences.rename_printable=Um druckbar zu machen
|
||||
preferences.rename_use_source_name_as_class_name_alias=Quelldateiname als Klassennamen-Alias verwenden
|
||||
preferences.rename_source_name_repeat_limit=Verwendung des Quellnamens erlauben, wenn dieser seltener vorkommt als
|
||||
preferences.search_group_title=Ressourcen durchsuchen
|
||||
preferences.search_results_per_page=Ergebnisse pro Seite (0 - keine Begrenzung)
|
||||
preferences.res_file_ext=Ressourcendatei-Erweiterungen ('xml|html', * für alle)
|
||||
preferences.res_skip_file=Ressourcendateien überspringen, wenn sie größer sind (MB) (0 - ausschalten)
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.rename_valid=To make them valid
|
||||
preferences.rename_printable=To make printable
|
||||
preferences.rename_use_source_name_as_class_name_alias=Use source file name as class name alias
|
||||
preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number
|
||||
preferences.search_group_title=Search
|
||||
preferences.search_results_per_page=Results per page (0 - no limit)
|
||||
preferences.res_file_ext=Resource files extensions ('xml|html', * for all)
|
||||
preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable)
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.reset_title=Reestablecer preferencias
|
||||
#preferences.rename_printable=To make printable
|
||||
preferences.rename_use_source_name_as_class_name_alias=Usar el nombre del source como alias para la clase
|
||||
#preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number
|
||||
#preferences.search_group_title=Search
|
||||
#preferences.search_results_per_page=Results per page (0 - no limit)
|
||||
#preferences.res_file_ext=Resource files extensions ('xml|html', * for all)
|
||||
#preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable)
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.rename_valid=Untuk membuatnya valid
|
||||
preferences.rename_printable=Untuk membuatnya dapat dicetak
|
||||
preferences.rename_use_source_name_as_class_name_alias=Gunakan nama berkas sumber sebagai alias nama kelas
|
||||
#preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number
|
||||
preferences.search_group_title=Pencarian
|
||||
preferences.search_results_per_page=Hasil per halaman (0 - tanpa batas)
|
||||
preferences.res_file_ext=Ekstensi berkas sumber daya ('xml|html', * untuk semua)
|
||||
preferences.res_skip_file=Lewati berkas sumber daya jika lebih besar (MB) (0 - nonaktifkan)
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.rename_valid=유효한 식별자로 바꾸기
|
||||
preferences.rename_printable=출력 가능하게 바꾸기
|
||||
preferences.rename_use_source_name_as_class_name_alias=소스 파일 이름을 클래스 이름 별칭으로 사용
|
||||
#preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number
|
||||
preferences.search_group_title=리소스 검색
|
||||
#preferences.search_results_per_page=Results per page (0 - no limit)
|
||||
preferences.res_file_ext=파일 확장자 (예: .xml|.html) (* 은 전체를 의미)
|
||||
preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB)
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.rename_valid=Deixá-las válidas
|
||||
preferences.rename_printable=Deixá-las imprimíveis (printable)
|
||||
preferences.rename_use_source_name_as_class_name_alias=Utilizar nome do arquivo como apelido da classe
|
||||
#preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number
|
||||
preferences.search_group_title=Buscar recursos
|
||||
#preferences.search_results_per_page=Results per page (0 - no limit)
|
||||
preferences.res_file_ext=Extensões de arquivos (ex: .xml|.html), * significa todas
|
||||
preferences.res_skip_file=Pular arquivos excedidos
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.rename_valid=И сделать их верными
|
||||
preferences.rename_printable=И сделать их доступными для печати
|
||||
preferences.rename_use_source_name_as_class_name_alias=Иcпользовать атрибут SOURCE
|
||||
#preferences.rename_source_name_repeat_limit=Allow using source name if it appears less than a limit number
|
||||
preferences.search_group_title=Поиск
|
||||
preferences.search_results_per_page=Результатов на страницу (0 - без лимита)
|
||||
preferences.res_file_ext=Расширения файлов ресурсов ('xml|html', * для всех)
|
||||
preferences.res_skip_file=Пропускать ресурсы больше чем (в МБ)
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.rename_valid=标识符应该符合标准规范
|
||||
preferences.rename_printable=标识符必须要能正常显示
|
||||
preferences.rename_use_source_name_as_class_name_alias=使用资源名作为类的别名
|
||||
preferences.rename_source_name_repeat_limit=如果源名称少于限制数,则允许使用源名称
|
||||
preferences.search_group_title=搜索资源
|
||||
preferences.search_results_per_page=每页结果数(0 - 无限制)
|
||||
preferences.res_file_ext=文件扩展名(比如 .xml|.html),* 表示所有
|
||||
preferences.res_skip_file=跳过文件大小(MB)
|
||||
|
||||
@@ -297,7 +297,6 @@ preferences.rename_valid=以使其有效
|
||||
preferences.rename_printable=以使其可列印
|
||||
preferences.rename_use_source_name_as_class_name_alias=將原始檔案名稱作為類別別名
|
||||
preferences.rename_source_name_repeat_limit=若出現次數少於限制數便允許使用來源名稱
|
||||
preferences.search_group_title=搜尋資源
|
||||
preferences.search_results_per_page=每頁的搜尋結果數 (0 - 無限制)
|
||||
preferences.res_file_ext=副檔名 (e.g. .xml|.html), * 表示全部
|
||||
preferences.res_skip_file=略過大於此值的檔案 (MB)
|
||||
|
||||
Reference in New Issue
Block a user