4

i m writing one application in which i have created custom list field for displaying listview. my CustomListField contains one image and text in a row. i m gettiing field change listener on click of listfield row but i want to put fieldchange listener on image too.. can anyone tell me how can i do that.

here is my code.

public class CustomListField extends ListField implements ListFieldCallback {
    private Vector _listData;
    private int _MAX_ROW_HEIGHT = 60;

    public CustomListField(Vector data) {
        _listData = data;
        setSize(_listData.size());
        setSearchable(true);
        setCallback(this);
        setRowHeight(_MAX_ROW_HEIGHT);
    }

    protected void drawFocus(Graphics graphics, boolean on) {
        XYRect rect = new XYRect();
        graphics.setGlobalAlpha(150);
        graphics.setColor(Color.BLUE);
        getFocusRect(rect);
        drawHighlightRegion(graphics, HIGHLIGHT_FOCUS, true, rect.x, rect.y, rect.width, rect.height);
    }

    public int moveFocus(int amount, int status, int time) {
        this.invalidate(this.getSelectedIndex());
        return super.moveFocus(amount, status, time);

    }

    public void onFocus(int direction) {
        super.onFocus(direction);
    }

    protected void onUnFocus() {
        this.invalidate(this.getSelectedIndex());
    }

    public void refresh() {
        this.getManager().invalidate();
    }

    public void drawListRow(ListField listField, Graphics graphics, int index, int y, int w) {
        listField.setBackground(BackgroundFactory.createBitmapBackground(Bitmap.getBitmapResource("listing_bg.png")));
        ListRander listRander = (ListRander) _listData.elementAt(index);

        graphics.setGlobalAlpha(255);
        graphics.setFont(Font.getDefault().getFontFamily().getFont(Font.PLAIN, 24));

        final int margin = 5;
        final Bitmap thumb = listRander.getListThumb();
        final String listHeading = listRander.getListTitle();
        final Bitmap nevBar = listRander.getNavBar();

        // list border
        graphics.setColor(Color.GRAY);
        graphics.drawRect(0, y, w, _MAX_ROW_HEIGHT);

        // thumbnail border & thumbnail image
        graphics.setColor(Color.BLACK);
        // graphics.drawRoundRect(margin-2, y+margin-2,thumb.getWidth()+2, thumb.getHeight()+2, 5, 5);
        graphics.drawBitmap(margin, y + margin, thumb.getWidth(), thumb.getHeight(), thumb, 0, 0);

        // drawing texts
        // graphics.setFont(Font.BOLD);
        graphics.drawText(listHeading, margin + thumb.getWidth(), y + margin);
        graphics.setColor(Color.GRAY);
        // graphics.setFont(Font.smallFont); // graphics.drawText(listDesc, 2*margin+thumb.getWidth(), y+ margin+20); // //
        // graphics.drawText(listDesc2, 2*margin+thumb.getWidth(), y+ margin+32);

        // draw navigation button
        final int navBarPosY = y + (_MAX_ROW_HEIGHT / 2 - nevBar.getHeight() / 2);
        final int navBarPosX = Graphics.getScreenWidth() - nevBar.getWidth() + margin;
        graphics.drawBitmap(navBarPosX, navBarPosY, nevBar.getWidth(), nevBar.getHeight(), nevBar, 0, 0);
    }

    public Object get(ListField listField, int index) {
        String rowString = (String) _listData.elementAt(index);
        return rowString;
    }

    public int indexOfList(ListField listField, String prefix, int start) {
        for (Enumeration e = _listData.elements(); e.hasMoreElements();) {
            String rowString = (String) e.nextElement();
            if (rowString.startsWith(prefix)) {
                return _listData.indexOf(rowString);
            }
        }
        return 0;
    }

    public int getPreferredWidth(ListField listField) {
        return 3 * listField.getRowHeight();

    }

/*
    protected boolean trackwheelClick(int status, int time) {
        invalidate(getSelectedIndex());
        Dialog.alert(" U have selected :" + getSelectedIndex());
        return super.trackwheelClick(status, time);
    }    
*/
}

i want to put click listner on star image of listfield row

and following is output of abbove code. enter image description here

tonymontana
  • 4,523
  • 4
  • 26
  • 48
Nirav Bhandari
  • 4,200
  • 5
  • 29
  • 58
  • So , do you want different click-event for the 'Product Name' and the Icon Image ? – Tariq M Nasim Jul 17 '12 at 05:53
  • yes...excaty...when i click on star image,different event should be fired instead of default listfield click event – Nirav Bhandari Jul 17 '12 at 07:51
  • do u have any solutions? – Nirav Bhandari Jul 17 '12 at 08:08
  • I don't have any solution in my hand but I can think about it ... you can try any of Arhimed's suggestions, that looks good to me. – Tariq M Nasim Jul 17 '12 at 08:19
  • I actually have an app that does almost exactly this, so I already worked through all the issues with focus handling and change listeners. If you can wait until tomorrow, I can modify that code to match your display, and post it. – Nate Jul 17 '12 at 11:47
  • i'll wait..but ri8 now can u give me some hint regarding that.. – Nirav Bhandari Jul 17 '12 at 11:50
  • Yes, I certainly did make it a VerticalFieldManager. But, there's more involved than what you might think when starting on the task. There's issues with transferring focus, isolating clicks between the row, and the icon (star), and differences in drawing focus, compared to a ListField implementation. – Nate Jul 17 '12 at 11:52

2 Answers2

7

I did something very similar to this on a past project:

Background

As Arhimed said in his answer, and as you can read about on the BlackBerry forums here, you can't have full-fledged Field objects within the ListField. The content of ListField rows is just drawn directly in drawListRow() as text, and Bitmaps, etc. The contents aren't Field instances, and therefore, are not focusable.

So, what I did was to replace ListField with a subclass of Manager. Originally, I used a VerticalFieldManager, but I ran into problems with that. I've also been seeing a lot of issues on stack overflow, where people subclass VerticalFieldManager, customize just one small behaviour, and everything starts breaking. It seems to me that VerticalFieldManager works well if you accept its normal behaviour, and if you need something more, just extend Manager directly. Performing layout for vertically stacked rows is pretty easy.

I then made each row its own Manager, and implemented custom layout in sublayout() to place the row's Fields where I wanted them. I could then also make the row focusable, and then a bitmap/button on the row separately focusable (like your star). Clicking the row invokes one action, and clicking the star invokes another one.

I will note, however, that in my app, performance was not an issue, because I only had 10-20 rows. Also, I did have to modify my code to match your example, so consider this code only lightly tested. However, I did build it into an app, so it should perform fine as long as my assumptions, and your description were valid.

Implementation

First, it wasn't clear to me what your ListRander is (you didn't show that code). However, in my code, I need a data class to contain details about one row. It looked like that's how you used ListRander, so that's what I used:

public class ListRander {

   private String _title;
   private Bitmap _thumb;

   public ListRander(String title, Bitmap thumb) {
      _title = title;
      _thumb = thumb;
   }

   public String getTitle() {
      return _title;
   }
   public Bitmap getThumb() {
      return _thumb;
   }
}

Then, I replaced your CustomListField class with my own:

public class CustomListField extends Manager implements FocusChangeListener {

   private int _MAX_ROW_HEIGHT = 60; 
   private boolean _searchable = false;
   private Vector _listData; 
   private FieldChangeListener _fieldListener;

   public CustomListField(Vector data) {
      super(FOCUSABLE | VERTICAL_SCROLL | VERTICAL_SCROLLBAR);
      setSearchable(true);
      setEditable(false);
      setListData(data);
   }

   public void setChangeListener(FieldChangeListener listener) {
      // we need to save this listener, because we set it to listen to all new rows
      _fieldListener = listener;
      int numFields = getFieldCount();
      for (int f = 0; f < numFields; f++) {
         getField(f).setChangeListener(listener);
      }
      super.setChangeListener(listener);
   }

   public int getRowHeight() {
      return _MAX_ROW_HEIGHT;
   }

   public void setSearchable(boolean searchable) {
      _searchable = searchable;
   }

   public int getSelectedIndex() {
      return getFieldWithFocusIndex();  // TODO??
   }

   public Object get(int index) {
      return _listData.elementAt(index);
   }

   public int indexOfList(String prefix, int start) {
      if (start >= _listData.size() || !_searchable) {
         return -1;
      } else {
         int result = getSelectedIndex();  // the default result if we find no matches
         for (Enumeration e = _listData.elements(); e.hasMoreElements(); ) {
            String rowString = (String) e.nextElement(); 
            if (rowString.startsWith(prefix)) { 
               return _listData.indexOf(rowString); 
            } 
         }
         return result;
      }
   }

   protected boolean navigationClick(int status, int time) {
      CustomListRow focus = (CustomListRow) getFieldWithFocus();
      if (focus != null) {
         // see if the row wants to process this click
         if (!focus.navigationClick(status, time)) {
            // let our FieldChangeListener know that this row has been clicked
            fieldChangeNotify(getFieldWithFocusIndex());
         }
         return true;
      } else {
         return false;
      }
   }

   protected void sublayout(int width, int height) {
      int w = Math.min(width, getPreferredWidth());
      int h = Math.min(height, getPreferredHeight());
      int rowHeight = getRowHeight();
      int numRows = getFieldCount();

      setExtent(w, h);
      setVirtualExtent(w, rowHeight * numRows);

      for (int i = 0; i < numRows; i++) {
         Field f = getField(i);
         setPositionChild(f, 0, rowHeight * i);
         layoutChild(f, w, rowHeight);
      }
   }

   public int getPreferredWidth() {
      return Display.getWidth();
   }

   public int getPreferredHeight() {
      return Display.getHeight();
   }

   public void setListData(Vector listData) {
      _listData = listData;
      if (listData != null) {
         int listSize = listData.size();
         int numRows = getFieldCount();
         for (int s = 0; s < listSize; s++) {
            if (s < numRows) {
               // we can reuse existing CustomListRows
               CustomListRow row = (CustomListRow) getField(s);               
               row.setData((ListRander) listData.elementAt(s));
            } else {
               CustomListRow row = new CustomListRow((ListRander) listData.elementAt(s));
               row.setChangeListener(_fieldListener);
               row.setFocusListener(this);
               add(row);
            }
         }

         if (listSize < numRows) {
            // delete the excess rows
            deleteRange(listSize, numRows - listSize);
         }
      } else {
         deleteAll();
      }
      invalidate();
   }

   public void focusChanged(Field field, int eventType) {
      // we handle scrolling here, when focus changes between rows
      if (eventType == FOCUS_GAINED) {
         if (field.getTop() < getVerticalScroll()) {
            // field is off the top of the screen, so scroll up
            setVerticalScroll(field.getTop());
         } else if (field.getTop() >= getVerticalScroll() + getVisibleHeight()) {
            // field is off the bottom of the screen, so scroll down
            setVerticalScroll(field.getTop() - getVisibleHeight() + getRowHeight());
         }          
      }
   }
}

Finally, one row is represented by my CustomListRow class:

public class CustomListRow extends Manager implements FieldChangeListener {

   private static final int _MAX_ROW_HEIGHT = 60;   
   private ListRander _data;
   private BitmapField _thumb;
   private LabelField _title;
   private FocusableBitmapField _star;
   private static final Bitmap _starImg = Bitmap.getBitmapResource("star.png");
   private static final Bitmap _bgImg = Bitmap.getBitmapResource("listing_bg.png");
   private SeparatorField _separator;
   private int _fontColor = Color.BLACK;
   private boolean _highlighted = false;
   private int _width;

   // subclass exists to expose focus methods (make public)
   private class FocusableBitmapField extends BitmapField {
      public FocusableBitmapField() {
         super(_starImg, BitmapField.FOCUSABLE | BitmapField.EDITABLE);    
      }
      public void onFocus(int direction) {
         super.onFocus(direction);
      }
      public void onUnfocus() {
         super.onUnfocus();
      }
   }

   public CustomListRow(ListRander data) {
      super(Field.FOCUSABLE | Manager.NO_VERTICAL_SCROLL | Manager.NO_VERTICAL_SCROLLBAR);

      setBackground(BackgroundFactory.createBitmapBackground(_bgImg));
      _width = Display.getWidth();

      long labelStyle = (DrawStyle.LEFT | DrawStyle.TOP | DrawStyle.ELLIPSIS);
      _title = new LabelField("", labelStyle) {   // custom anonymous class to change font color
         protected void paint(Graphics g) {
            int c = g.getColor();
            g.setColor(_fontColor);
            super.paint(g);
            g.setColor(c);
         }
      };
      _title.setFont(Font.getDefault().getFontFamily().getFont(Font.PLAIN, 24));

      _thumb = new BitmapField();

      _star = new FocusableBitmapField();
      _star.setChangeListener(this);

      _separator = new SeparatorField() {  // custom anonymous class to change separator color
         protected void paint(Graphics g) {
            int c = g.getColor();
            g.setColor(Color.GRAY);
            super.paint(g);
            g.setColor(c);
         }
      };

      setData(data);

      add(_thumb);
      add(_title);
      add(_star);
      add(_separator);
   }

   public ListRander getData() {
      return _data;
   }

   public void setData(ListRander value) {
      if (value != _data) {
         _data = value;

         _title.setText(value.getTitle());
         _thumb.setBitmap(value.getThumb());
      }
   }

   private void onStarClicked() {
      Dialog.alert("Star has been clicked or tapped!");
   }

   private void onRowClicked() {
      Dialog.alert("Row has been clicked or tapped!");
   }

   public void fieldChanged(Field field, int context) {
      if (field == _star) {
         onStarClicked();
      }
   }  

   public boolean navigationClick(int status, int time) {
      if (_star.isFocus()) {
         onStarClicked();
         return true;
      } /* else {
            onRowClicked();
            return true;
        } */
      return false;  // we will not consume this event
   }

   protected void highlight(boolean onRow) {
      _fontColor = onRow ? Color.WHITE : Color.BLACK;  // change font color for contrast
      _highlighted = onRow;         
      invalidate();
   }

   protected void onFocus(int direction) {
      // called when focus first transfers to this row, from another Field
      if (direction == 1) {
         // coming from top to bottom, we highlight the row first, not the star  
         highlight(true);
      } else if (direction == -1) {
         // coming from bottom to top, we highlight the star button first, not the row
         _star.onFocus(direction);
         highlight(false);
      }
   }  

   protected void onUnfocus() {
      // remove highlighting of the row, if any
      highlight(false);
      super.onUnfocus();
   }

   protected int moveFocus(int amount, int status, int time) {
      // called when this row already has focus (either on row, or star button)
      if (amount > 0) {
         // moving top to bottom
         if (!_star.isFocus()) {
            // we were on the row, now move to the star button
            _star.onFocus(1);
            highlight(false);
            amount--;             // consume one unit of movement
         }
      } else {
         // moving from bottom to top
         if (_star.isFocus()) {
            // we were on the star button, now move back over to the row
            _star.onUnfocus();
            highlight(true);
            amount++;             // consume one unit of movement
         }
      }
      return amount;
   }

   protected boolean touchEvent(net.rim.device.api.ui.TouchEvent event) {
      // We take action when the user completes a click (a.k.a. unclick)
      int eventCode = event.getEvent();
      if ((eventCode == TouchEvent.UNCLICK) || (eventCode == TouchEvent.DOWN)) {
         // Get the touch location, within this Manager
         int x = event.getX(1);
         int y = event.getY(1);

         if ((x >= 0) && (y >= 0) && (x < _width) && (y < _MAX_ROW_HEIGHT)) {
            int field = getFieldAtLocation(x, y);
            if ((field >= 0) && (getField(field) == _star)) {
               // Let event propagate to (star) button field
               return super.touchEvent(event);
            } else {
               if (eventCode == TouchEvent.UNCLICK) {
                  // A completed click anywhere else in this row should popup details for this selection
                  fieldChangeNotify(1);
                  onRowClicked();
               } else {
                  // This is just a soft touch (TouchEvent.DOWN), without full click
                  setFocus();
               }
               // Consume the event
               return true;
            }
         }
      }
      // Event wasn't for us, let superclass handle in default manner
      return super.touchEvent(event);
   }

   protected void sublayout(int width, int height) {
      height = Math.min(getPreferredHeight(), height);
      setExtent(_width, height);

      final int margin = 5;

      int thumbWidth = _thumb.getPreferredWidth();
      layoutChild(_thumb, thumbWidth, _thumb.getPreferredHeight());
      setPositionChild(_thumb, margin, margin);

      int starWidth = _star.getPreferredWidth();
      int starHeight = _star.getPreferredHeight();
      layoutChild(_star, starWidth, starHeight);
      setPositionChild(_star, width - starWidth - margin, (height - starHeight) / 2);

      // this assumes you want margin between all fields, and edges
      layoutChild(_title, width - thumbWidth - starWidth - 4 * margin, _title.getPreferredHeight());
      setPositionChild(_title, margin + thumbWidth /* + margin */, margin);  // TODO?
   }    

   protected void paintBackground(Graphics g) {
      super.paintBackground(g);
      if (_highlighted) {
         // you can't override drawFocus() for a Manager, so we'll handle that here:
         int oldColor = g.getColor();
         int oldAlpha = g.getGlobalAlpha();
         XYRect rect = new XYRect();
         g.setGlobalAlpha(150); 
         g.setColor(Color.BLUE); 
         getFocusRect(rect); 
         drawHighlightRegion(g, HIGHLIGHT_FOCUS, true, rect.x, rect.y, rect.width, rect.height);
         g.setGlobalAlpha(oldAlpha);
         g.setColor(oldColor);
      }
   }

   public int getPreferredWidth() {
      return _width;
   }

   public int getPreferredHeight() {
      return _MAX_ROW_HEIGHT;
   }
}

Usage

This is how you might use the whole list field (maybe in a Screen class):

public class ListScreen extends MainScreen implements FieldChangeListener {
   public ListScreen() {
      try {
         Vector data = new Vector();
         Bitmap icon = Bitmap.getBitmapResource("list_icon.png");
         for (int i = 0; i < 15; i++) {
            ListRander lr = new ListRander("Product Name " + i, icon);
            data.addElement(lr);
         }
         CustomListField list = new CustomListField(data);
         add(list);
         list.setChangeListener(this);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }

   public void fieldChanged(Field field, int context) {
      if (field instanceof CustomListRow) {
         CustomListRow row = (CustomListRow) field;
         Dialog.alert(row.getData().getTitle() + " was selected!");
      }      
   }    
}

In my app, it made sense for the CustomListRow itself to handle the equivalent of your star click. However, for me, it did not make sense to have the row click handled that way. So, I let you set a FieldChangeListener on the CustomListField itself, which is called back when any row is selected. See the example above in my screen class. If you want to handle the row click inside the CustomListRow class, too, that's fine. I laid out a onRowClicked() method there. Search in the code for where that's commented out, and you can reactivate, an implement that method (onRowClicked()).

Issues

  • My app didn't require list searching. I laid out a sample implementation of that, like ListField has. But, I didn't test it. That's your job, if you need it. I just got you started with the CustomListField implementation (see indexOfList()).
  • I didn't see what your "nav bar" was for. A bar is usually a full-width item, like a status bar, or toolbar. I don't see anything like that in your screenshot. A nav item might be a little arrow at the right side of each row, to bring up details. But, I didn't see that in your screenshot either. So, I ignored that code. If you need a nav bar, you obviously know what it should be, and can add that to my code above.
  • I couldn't tell whether or not you just added the star as part of the row's background image, or if you had a separate image for that. I added a separate star.png to represent the star. I would assume that clicking the star fills it in, or highlights it, or something. But, you didn't describe that problem, so I assume you can handle that. If you need a custom field to represent the star, that can have selected and unselected images, just post that as a new question.
  • You had some code that appeared like it was trying to set the row width to 3x the row height, but that didn't match your screen shot. Most lists are full-screen width anyway. So, I remove that code. My CustomListRow class implements getPreferredWidth() and requests the full screen width. Change if you like.
Community
  • 1
  • 1
Nate
  • 30,589
  • 12
  • 76
  • 201
  • @Nirav, You're welcome. If you have any trouble with the code, let me know. I could probably zip up a "Hello World" app with the code above, that's runnable. As I said, *most* of the code was taken from an app of mine, so if you find problems, I'd love to know, and would be happy to look at them. – Nate Jul 18 '12 at 09:59
  • will let you know if any problem arise..ri8 now i am implementing your code..n m sure it will works..thanks man. – Nirav Bhandari Jul 18 '12 at 10:10
  • @Nate,I am also working on such type of custom list field.Using your code I am getting NullpoiterException while adding sublayout. Please describe the flow of your code.Thanks.. – Pawan Gupta Mar 12 '13 at 06:52
  • @Pawan, I think there already is as much description above as I can provide. **Where** exactly are you getting the `NullPointerException`? There are two `sublayout()` methods provided above. Which line are you getting it on? You should be able to determine which object is `null`, causing that exception. – Nate Mar 12 '13 at 13:22
  • @Pawan, also, if I had to guess (without your answer to my previous question), I would guess that you are simply not using one of my pieces of member data. In your other question, your description made it sound like you would not need the `_thumb` variable I use above. If not, make sure to delete that from the code, and delete everywhere that uses it. If you don't initialize `_thumb`, or anything else, and then use it in `sublayout()`, that will of course give you a `NullPointerException`. – Nate Mar 12 '13 at 21:56
  • @Nate,Thanks a lot for your reply.I am getting **NullpointerException** in **CustomListRow** class in sublayout method.I get it when all 15 elements used are added using sublayout means at the end of **sublayout()** method. I need *_thumb* in my app but I am unable to determine which object is *null* – Pawan Gupta Mar 13 '13 at 08:24
  • @Pawan, Well, that's where you need to step through that method in the debugger, and see what's null. It looks like that method calls methods on `_thumb`, `_star`, and `_title`, so if any of those are null, you'll get an exception. Are you sure you want to use `_thumb`? In this example, `_thumb` is only a thumbnail image. The one that's actually clickable (like a button) is the `_star` object. – Nate Mar 13 '13 at 08:31
  • @Nate, I am getting **NullpointerException** in **onFocus()** method of **customListRow** class. I am not getting why it is created here whereas I have put all images properly and initialized all objects. I am getting default focus as **null** .What it means... – Pawan Gupta Apr 16 '13 at 04:41
  • @Pawan, the only object used in `onFocus()` is `_star`, so I would say that `_star` is null. Either you did not add a `star.png` image to your project, or you have changed the code that I posted. I don't understand what you mean by *"I am getting default focus as null"*. – Nate Apr 16 '13 at 08:07
  • @Nate, Its working now.I just added **super.onFocus(direction);** in **onFocus()** method in case when **direction == 1** . – Pawan Gupta Apr 16 '13 at 08:50
  • @Nate,now I am getting another issue that when I come from top to bottom ,List row didn't get focus.Whereas when I go bottom to top it gets focus..Both the time I am getting focus on star. – Pawan Gupta Apr 16 '13 at 10:29
  • @Pawan, It's been a while since I wrote this code, but I can try running it tonight, to see if I can reproduce your problem. Your last comment makes sense to me, because you added the `super.onFocus()` call, which I intentionally left out of the original code. Adding that will change the focus behaviour. The previous comment is more confusing to me. If the null pointer exception really was happening in `onFocus()`, then adding `super.onFocus(direction)` would not fix that. Are you **100% sure** that's *where* the exception was being thrown? If so, how do you know? Did you step thru it? – Nate Apr 16 '13 at 21:22
  • @Nate,actually I didn't know that the NullpointerException was due to **super.onFocus()** but I just found that this was missing so I added it and run the code again and it solved my problem.It was just hit and trial to run code after adding super.onFocus() because I knew it can't create null pointer exception. Still I don't know what was causing the exception and why it solved by adding super.onFocus(direction) . About Exception:After run of **highlight()** called in **onFocus()** it was throwing null pointer exception and showing in **ListScreen** class. – Pawan Gupta Apr 17 '13 at 04:34
  • @Pawan, ok, well please don't tell me where the exception is if you're not sure. That makes it hard to help you. I'm looking over the app that this example came from, and trying to find out what's different. There are some differences. I will post an update as soon as I figure this out. – Nate Apr 19 '13 at 01:43
  • @Pawan, sorry I forgot about this. Are you still in need of a solution to the star field focus problem? – Nate Apr 30 '13 at 11:38
6

Unlike Android's ListView the BB's ListField is not designed to have a focusable/clickable fields inside of list items. So any attempt to workaround this will have some negative side effects.

A relatively easy/quick workaround would be to switch to VerticalFieldManager (check this other stack overflow question). But if the list is too long (more than several hundreds, I believe) you risk to "eat" too much memory.

If the app is designed for touch screens only, then you can try to stay with ListField + do some manual tracking of touch event coordinates. So when you detect a list field click (in a way you would normally do it) you can check whether the touch coordinates correspond to the star image area (at least on the X axis). I am not going to invent/provide an implementation, but just giving an idea.

Community
  • 1
  • 1
Vit Khudenko
  • 27,639
  • 10
  • 56
  • 86