Add radical selection to main app view

Julien LepillerSun Apr 26 21:55:46+0200 2020

5ff162a

Add radical selection to main app view

app/src/main/java/eu/lepiller/nani/MainActivity.java

2727
import se.fekete.furiganatextview.furiganaview.FuriganaTextView;
2828
2929
public class MainActivity extends AppCompatActivity implements OnTaskCompleted<SearchResult> {
30-
    private LinearLayout result_view;
30+
    private LinearLayout result_view, result_layout;
3131
    private Button search_button;
3232
    private TextView feedback_text;
3333
    private EditText search_form;
3434
    private static ArrayList<Result> savedResults;
35+
    private RadicalSelectorView radical_selector;
3536
3637
    @Override
3738
    protected void onCreate(Bundle savedInstanceState) {

4243
4344
        search_button = findViewById(R.id.search_button);
4445
        result_view = findViewById(R.id.results_view);
46+
        result_layout = findViewById(R.id.result_layout);
4547
        feedback_text = findViewById(R.id.feedback);
4648
        search_form = findViewById(R.id.search_form);
49+
        radical_selector = findViewById(R.id.radical_selector);
50+
51+
        Button radical_button = findViewById(R.id.radical_button);
52+
53+
        try {
54+
            radical_selector.setDictionary(DictionaryFactory.getRadicalDictionary(getApplicationContext()));
55+
        } catch (NoDictionaryException e) {
56+
            e.printStackTrace();
57+
        }
4758
4859
        search_button.setOnClickListener(new View.OnClickListener() {
4960
            @Override
5061
            public void onClick(View v) {
62+
                radical_selector.setVisibility(View.INVISIBLE);
63+
                result_layout.setVisibility(View.VISIBLE);
64+
5165
                String text = search_form.getText().toString();
5266
                if (text.isEmpty()) {
5367
                    Snackbar.make(findViewById(R.id.search_form), getString(R.string.no_search), Snackbar.LENGTH_LONG).show();

6377
            }
6478
        });
6579
80+
        radical_button.setOnClickListener(new View.OnClickListener() {
81+
            @Override
82+
            public void onClick(View v) {
83+
                try {
84+
                    DictionaryFactory.getRadicalDictionary(getApplicationContext());
85+
                    radical_selector.setVisibility(View.VISIBLE);
86+
                    result_layout.setVisibility(View.INVISIBLE);
87+
                } catch (NoDictionaryException e) {
88+
                    Snackbar.make(findViewById(R.id.search_form), getString(R.string.no_radical_dict), Snackbar.LENGTH_LONG).show();
89+
                }
90+
            }
91+
        });
92+
93+
        radical_selector.setOnCloseEventListener(new View.OnClickListener() {
94+
            @Override
95+
            public void onClick(View v) {
96+
                radical_selector.setVisibility(View.INVISIBLE);
97+
                result_layout.setVisibility(View.VISIBLE);
98+
            }
99+
        });
100+
101+
        radical_selector.setOnClickEventListener(new View.OnClickListener() {
102+
            @Override
103+
            public void onClick(View v) {
104+
                String k = ((Button)v).getText().toString();
105+
                search_form.append(k);
106+
            }
107+
        });
108+
66109
        if(savedResults != null) {
67110
            showResults(savedResults);
68111
        }

86129
            }
87130
88131
89-
            if(searchResult == null || searchResult.size() == 0) {
132+
            if(searchResult.size() == 0) {
90133
                MojiConverter converter = new MojiConverter();
91134
                try {
92135
                    searchResult = DictionaryFactory.search(converter.convertRomajiToHiragana(text));

app/src/main/java/eu/lepiller/nani/RadicalSelectorView.java

11
package eu.lepiller.nani;
22
3-
public class RadicalSelectorView {
3+
import android.content.Context;
4+
import android.util.AttributeSet;
5+
import android.util.Log;
6+
import android.view.LayoutInflater;
7+
import android.view.View;
8+
import android.widget.Button;
9+
import android.widget.LinearLayout;
10+
import android.widget.TableLayout;
11+
import android.widget.TableRow;
12+
import android.widget.TextView;
13+
import android.widget.ToggleButton;
14+
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.Set;
19+
20+
import eu.lepiller.nani.dictionary.IncompatibleFormatException;
21+
import eu.lepiller.nani.dictionary.RadicalDict;
22+
23+
public class RadicalSelectorView extends LinearLayout {
24+
    private Button close_button;
25+
    private LinearLayout kanji_row1, kanji_row2;
26+
    private TableLayout radical_table;
27+
    private List<String> selected = new ArrayList<>();
28+
    View.OnClickListener kanjiSelectionListener;
29+
    RadicalDict dictionary;
30+
31+
    public RadicalSelectorView(Context context) {
32+
        this(context, null, 0);
33+
    }
34+
35+
    public RadicalSelectorView(Context context, AttributeSet attrs) {
36+
        this(context, attrs, 0);
37+
    }
38+
39+
    public RadicalSelectorView(Context context, AttributeSet attrs, int defStyle) {
40+
        super(context, attrs, defStyle);
41+
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
42+
        inflater.inflate(R.layout.content_radicals, this);
43+
        this.setOrientation(VERTICAL);
44+
    }
45+
46+
    @Override
47+
    protected void onFinishInflate() {
48+
        super.onFinishInflate();
49+
50+
        close_button = findViewById(R.id.close_button);
51+
        kanji_row1 = findViewById(R.id.kanji_row1);
52+
        kanji_row2 = findViewById(R.id.kanji_row2);
53+
        radical_table = findViewById(R.id.radical_table);
54+
    }
55+
56+
    public void setDictionary(RadicalDict dict) {
57+
        dictionary = dict;
58+
59+
        try {
60+
            Map<Integer, List<String>> radicals = dict.getAllRadicals();
61+
            for(int stroke: radicals.keySet()) {
62+
                TableRow row = new TableRow(getContext());
63+
                TextView strokeView = new TextView(getContext());
64+
                strokeView.setText(Integer.toString(stroke));
65+
                row.addView(strokeView);
66+
67+
                List<String> lst = radicals.get(stroke);
68+
                for(final String radical: lst) {
69+
                    ToggleButton radicalButton = new ToggleButton(getContext());
70+
                    radicalButton.setTextOff(radical);
71+
                    radicalButton.setTextOn(radical);
72+
                    radicalButton.setText(radical);
73+
74+
                    radicalButton.setOnClickListener(new OnClickListener() {
75+
                        @Override
76+
                        public void onClick(View v) {
77+
                            boolean checked = ((ToggleButton) v).isChecked();
78+
                            if(checked) {
79+
                                selected.add(radical);
80+
                            } else {
81+
                                selected.remove(radical);
82+
                            }
83+
84+
                            updateSelection();
85+
                            updateRadicals();
86+
                        }
87+
                    });
88+
                    row.addView(radicalButton);
89+
                }
90+
                radical_table.addView(row);
91+
            }
92+
        } catch(IncompatibleFormatException e) {
93+
            e.printStackTrace();
94+
        }
95+
    }
96+
97+
    public void setOnCloseEventListener(View.OnClickListener listener) {
98+
        close_button.setOnClickListener(listener);
99+
    }
100+
101+
    public void setOnClickEventListener(View.OnClickListener listener) {
102+
        kanjiSelectionListener = listener;
103+
    }
104+
105+
    public void updateSelection() {
106+
        try {
107+
            Map<Integer, List<String>> kanji = dictionary.match(selected);
108+
            LinearLayout row = kanji_row1;
109+
            kanji_row1.removeAllViews();
110+
            kanji_row2.removeAllViews();
111+
112+
            if(kanji == null)
113+
                return;
114+
115+
            int total = 0;
116+
            int number = 0;
117+
            for (int stroke : kanji.keySet()) {
118+
                total += kanji.get(stroke).size();
119+
            }
120+
            for (int stroke : kanji.keySet()) {
121+
                TextView strokeView = new TextView(getContext());
122+
                strokeView.setText(Integer.toString(stroke));
123+
                row.addView(strokeView);
124+
125+
                for (String k : kanji.get(stroke)) {
126+
                    number++;
127+
                    Button kanji_button = new Button(getContext());
128+
                    kanji_button.setText(k);
129+
                    kanji_button.setOnClickListener(kanjiSelectionListener);
130+
                    row.addView(kanji_button);
131+
132+
                    if (number > total / 2)
133+
                        row = kanji_row2;
134+
                }
135+
            }
136+
        } catch (IncompatibleFormatException e) {
137+
            e.printStackTrace();
138+
        }
139+
    }
140+
141+
    private void updateRadicals() {
142+
        try {
143+
            Set<String> available = dictionary.availableRadicals(selected);
144+
            Log.v("SELECTOR", Integer.toString(radical_table.getChildCount()));
145+
            for(int row = 0; row < radical_table.getChildCount(); row++) {
146+
                TableRow r = (TableRow) radical_table.getChildAt(row);
147+
                for(int col = 0; col < r.getChildCount(); col++) {
148+
                    View child = r.getChildAt(col);
149+
                    if(child instanceof ToggleButton) {
150+
                        child.setEnabled(available == null || available.contains(((ToggleButton) child).getText().toString()));
151+
                    }
152+
                }
153+
            }
154+
        } catch(IncompatibleFormatException e) {
155+
            e.printStackTrace();
156+
        }
157+
    }
4158
}

app/src/main/java/eu/lepiller/nani/dictionary/DictionaryFactory.java

101101
102102
        return dictionaries;
103103
    }
104+
105+
    public static RadicalDict getRadicalDictionary(Context context) throws NoDictionaryException {
106+
        if(instance == null)
107+
            instance = new DictionaryFactory(context);
108+
109+
        for(Dictionary d: dictionaries) {
110+
            if(d.isDownloaded() && d instanceof RadicalDict) {
111+
                return (RadicalDict) d;
112+
            }
113+
        }
114+
115+
        throw new NoDictionaryException();
116+
    }
104117
}

app/src/main/java/eu/lepiller/nani/dictionary/RadicalDict.java

11
package eu.lepiller.nani.dictionary;
22
3+
import android.util.Log;
4+
35
import java.io.File;
46
import java.io.FileNotFoundException;
57
import java.io.IOException;

1719
public class RadicalDict extends FileDictionary {
1820
    private final Map<String, Integer> strokeCount = new HashMap<>();
1921
    private final Map<String, Integer> radicalStrokeCount = new HashMap<>();
20-
    private final Map<String, String[]> radicals = new HashMap<>();
22+
    private final Map<String, List<String>> radicals = new HashMap<>();
2123
2224
    final private static String TAG = "RADK";
2325

3335
        radicalStrokeCount.clear();
3436
        radicals.clear();
3537
    }
36-
    private long fillStrokeCount(RandomAccessFile file) throws IOException{
38+
    private void fillStrokeCount(RandomAccessFile file) throws IOException{
3739
        int size = file.readShort();
3840
        for(int i=0; i<size; i++) {
39-
            String radical = getString(file);
41+
            String kanji = getString(file);
4042
            int stroke = file.readByte();
41-
42-
            strokeCount.put(radical, stroke);
43+
            strokeCount.put(kanji, stroke);
4344
        }
44-
        return file.getFilePointer();
4545
    }
4646
47-
    private long fillRadicalStrokeCount(RandomAccessFile file) throws IOException {
47+
    private void fillRadicalStrokeCount(RandomAccessFile file) throws IOException {
4848
        int size = file.readShort();
4949
        for(int i=0; i<size; i++) {
5050
            String radical = getString(file);

5252
5353
            radicalStrokeCount.put(radical, stroke);
5454
        }
55-
        return file.getFilePointer();
5655
    }
5756
58-
    private long fillRadicals(RandomAccessFile file) throws IOException {
57+
    private void fillRadicals(RandomAccessFile file) throws IOException {
5958
        int size = file.readShort();
6059
        for(int i=0; i<size; i++) {
6160
            String radical = getString(file);
6261
            String kanji = getString(file);
62+
            List<String> lst = new ArrayList<>();
63+
            for(String s: kanji.split("")) {
64+
                if(s.compareTo("") != 0) {
65+
                    lst.add(s);
66+
                }
67+
            }
6368
64-
            radicals.put(radical, kanji.split(""));
69+
            radicals.put(radical, lst);
6570
        }
66-
        return file.getFilePointer();
6771
    }
6872
6973
    private void fillTables() throws IncompatibleFormatException {

9498
        super(name, description, fullDescription, cacheDir, url);
9599
    }
96100
97-
    private boolean contains(String[] lst, String s) {
101+
    private boolean contains(List<String> lst, String s) {
98102
        if(lst==null)
99103
            return false;
100104
        for(String p: lst) {

112116
        fillTables();
113117
        Map<Integer, List<String>> result = null;
114118
        for(String r: rad) {
115-
            String[] possibilities = radicals.get(r);
119+
            List<String> possibilities = radicals.get(r);
116120
            if(possibilities == null)
117121
                continue;
118122
            if(result == null) {

122126
                    int stroke = 0;
123127
                    if(strokes != null)
124128
                        stroke = strokes;
129+
                    Log.d(TAG, Integer.toString(stroke));
125130
                    List <String> lst = result.get(stroke);
126131
                    if(lst == null) {
127132
                        lst = new ArrayList<>();

131136
                }
132137
            } else {
133138
                for(List<String> kanjiList: result.values()) {
134-
                    for(String kanji: kanjiList) {
139+
                    List<String> lst = new ArrayList<>(kanjiList);
140+
                    for(String kanji: lst) {
135141
                        if(! contains(possibilities, kanji))
136142
                            kanjiList.remove(kanji);
137143
                    }
138144
                }
139145
            }
140146
        }
147+
148+
        if(result != null) {
149+
            Set<Integer> strokes = new HashSet<>(result.keySet());
150+
            for (int stroke : strokes) {
151+
                List<String> kanji = result.get(stroke);
152+
                if (kanji == null || kanji.size() == 0) {
153+
                    result.remove(stroke);
154+
                }
155+
            }
156+
        }
157+
141158
        return result;
142159
    }
143160

151168
152169
        Set<String> result = new HashSet<>();
153170
        Map<Integer, List<String>> kanjiList = match(rad);
154-
        for(Map.Entry<String, String[]> e: radicals.entrySet()) {
171+
        for(Map.Entry<String, List<String>> e: radicals.entrySet()) {
155172
            String radical = e.getKey();
156-
            String[] possibilities = e.getValue();
173+
            List<String> possibilities = e.getValue();
157174
            boolean possible = false;
158175
            for(List<String> kanjiGroup: kanjiList.values()) {
159176
                if(possible)

app/src/main/res/layout/content_main.xml

2525
            android:layout_marginTop="32dp"
2626
            android:layout_weight="1"
2727
            android:ems="10"
28-
            android:importantForAutofill="no"
2928
            android:hint="@string/search_hint"
29+
            android:importantForAutofill="no"
3030
            android:inputType="text" />
3131
3232
        <Button

4242
4343
    </LinearLayout>
4444
45-
    <TextView
46-
        android:id="@+id/feedback"
45+
    <FrameLayout
4746
        android:layout_width="match_parent"
48-
        android:layout_height="wrap_content" />
47+
        android:layout_height="match_parent">
4948
50-
    <ScrollView
51-
        android:layout_weight="1"
52-
        android:layout_width="match_parent"
53-
        android:layout_height="0dp">
5449
        <LinearLayout
55-
            android:orientation="vertical"
56-
            android:id="@+id/results_view"
50+
            android:id="@+id/result_layout"
5751
            android:layout_width="match_parent"
58-
            android:layout_height="wrap_content"
59-
            android:layout_marginTop="16dp"
60-
            android:layout_marginBottom="8dp" />
61-
    </ScrollView>
52+
            android:layout_height="match_parent"
53+
            android:orientation="vertical">
54+
55+
            <TextView
56+
                android:id="@+id/feedback"
57+
                android:layout_width="match_parent"
58+
                android:layout_height="wrap_content" />
59+
60+
            <ScrollView
61+
                android:layout_width="match_parent"
62+
                android:layout_height="0dp"
63+
                android:layout_weight="1">
64+
65+
                <LinearLayout
66+
                    android:id="@+id/results_view"
67+
                    android:layout_width="match_parent"
68+
                    android:layout_height="wrap_content"
69+
                    android:layout_marginTop="16dp"
70+
                    android:layout_marginBottom="8dp"
71+
                    android:orientation="vertical" />
72+
            </ScrollView>
73+
74+
            <LinearLayout
75+
                android:id="@+id/bottom_layout"
76+
                android:layout_width="match_parent"
77+
                android:layout_height="wrap_content"
78+
                android:orientation="horizontal">
79+
80+
                <Space
81+
                    android:layout_width="0dp"
82+
                    android:layout_height="wrap_content"
83+
                    android:layout_weight="1" />
84+
85+
                <Button
86+
                    android:id="@+id/radical_button"
87+
                    android:layout_width="wrap_content"
88+
                    android:layout_height="wrap_content"
89+
                    android:text="???" />
90+
            </LinearLayout>
91+
92+
        </LinearLayout>
93+
94+
        <eu.lepiller.nani.RadicalSelectorView
95+
            android:id="@+id/radical_selector"
96+
            android:layout_width="match_parent"
97+
            android:layout_height="match_parent"
98+
            android:visibility="invisible" />
99+
100+
    </FrameLayout>
62101
63102
</LinearLayout>
63102=
64103=
\ No newline at end of file

app/src/main/res/layout/content_radicals.xml

44
    android:layout_height="match_parent"
55
    android:orientation="vertical">
66
7-
    <ScrollView
7+
    <HorizontalScrollView
88
        android:layout_width="match_parent"
99
        android:layout_height="wrap_content">
1010
11-
        <TableLayout
12-
            android:layout_width="match_parent"
13-
            android:layout_height="wrap_content"
14-
            android:orientation="vertical" >
15-
16-
            <TableRow
17-
                android:layout_width="match_parent"
18-
                android:layout_height="match_parent" >
11+
        <LinearLayout
12+
            android:layout_width="wrap_content"
13+
            android:layout_height="match_parent"
14+
            android:orientation="vertical">
15+
            <LinearLayout
16+
                android:id="@+id/kanji_row1"
17+
                android:layout_width="wrap_content"
18+
                android:layout_height="match_parent"
19+
                android:orientation="horizontal"></LinearLayout>
1920
20-
                <TextView
21-
                    android:id="@+id/textView16"
22-
                    android:layout_width="wrap_content"
23-
                    android:layout_height="wrap_content"
24-
                    android:text="TextView" />
25-
            </TableRow>
26-
27-
            <TableRow
28-
                android:layout_width="match_parent"
29-
                android:layout_height="match_parent" >
30-
31-
                <TextView
32-
                    android:id="@+id/textView17"
33-
                    android:layout_width="wrap_content"
34-
                    android:layout_height="wrap_content"
35-
                    android:text="TextView" />
36-
            </TableRow>
37-
        </TableLayout>
38-
    </ScrollView>
21+
            <LinearLayout
22+
                android:id="@+id/kanji_row2"
23+
                android:layout_width="wrap_content"
24+
                android:layout_height="match_parent"
25+
                android:orientation="horizontal"></LinearLayout>
26+
        </LinearLayout>
27+
    </HorizontalScrollView>
3928
4029
    <ScrollView
4130
        android:layout_width="match_parent"
42-
        android:layout_height="match_parent">
31+
        android:layout_height="wrap_content"
32+
        android:layout_weight="1">
4333
44-
        <TableLayout
34+
        <HorizontalScrollView
4535
            android:layout_width="match_parent"
46-
            android:layout_height="wrap_content"
47-
            android:orientation="vertical" >
36+
            android:layout_height="wrap_content">
4837
49-
            <TableRow
50-
                android:layout_width="match_parent"
51-
                android:layout_height="match_parent" >
38+
            <TableLayout
39+
                android:id="@+id/radical_table"
40+
                android:layout_width="wrap_content"
41+
                android:layout_height="wrap_content"
42+
                android:orientation="vertical">
5243
53-
                <TextView
54-
                    android:id="@+id/textView18"
55-
                    android:layout_width="wrap_content"
56-
                    android:layout_height="wrap_content"
57-
                    android:text="1" />
44+
            </TableLayout>
5845
59-
                <ToggleButton
60-
                    android:id="@+id/toggleButton"
61-
                    android:layout_width="45dp"
62-
                    android:layout_height="wrap_content"
63-
                    android:checked="false"
64-
                    android:enabled="true"
65-
                    android:text="??????"
66-
                    android:textOff="???"
67-
                    android:textOn="??????" />
68-
            </TableRow>
46+
        </HorizontalScrollView>
47+
    </ScrollView>
6948
70-
            <TableRow
71-
                android:layout_width="match_parent"
72-
                android:layout_height="match_parent" >
49+
    <LinearLayout
50+
        android:layout_width="match_parent"
51+
        android:layout_height="wrap_content">
7352
74-
            </TableRow>
75-
        </TableLayout>
76-
    </ScrollView>
53+
        <Space
54+
            android:layout_width="0dp"
55+
            android:layout_height="wrap_content"
56+
            android:layout_weight="1" />
57+
58+
        <Button
59+
            android:id="@+id/close_button"
60+
            android:layout_width="wrap_content"
61+
            android:layout_height="wrap_content"
62+
            android:text="@string/close" />
63+
    </LinearLayout>
7764
</merge>
7764=
7865=
\ No newline at end of file

app/src/main/res/values/strings.xml

1010
    <string name="no_search">No search term</string>
1111
    <string name="no_dic">You haven\'t downloaded any dictionary usable with this search function. Please select a dictionary.</string>
1212
    <string name="incompatible_format">Dictionary %s is not compatible with this version of the application. Either update the dictionary or the app.</string>
13+
    <string name="no_radical_dict">Dictionary for radical selection is not downloaded yet. Please select one from the dictionary list.</string>
1314
1415
    <string name="download">Download</string>
1516
    <string name="remove">Remove</string>
1617
    <string name="icon_description">Icon</string>
1718
    <string name="kanji_description">Writing</string>
19+
    <string name="close">close</string>
1820
1921
    <string name="dictionary_size">Actual size: %dMB</string>
2022
    <string name="feedback_no_result">No result</string>