Á͘ ÿŒ ÿ‚Ý…‹Ÿ‹ ÿÁ̓ ̓͂— ÔƒÁÍ ÅŠ‚‹

Listing 2 (/src/com/holub/ui/Bag.java): The Bag Wrapper


   1:  package com.holub.ui;
   2:  
   3:  import java.awt.*;
   4:  import java.awt.event.*;
   5:  import java.util.*;
   6:  
   7:  import javax.swing.*;
   8:  import javax.swing.event.*;
   9:  import javax.swing.border.*;
  10:  
  11:  import com.holub.ui.AncestorAdapter;
  12:  import com.holub.tools.Multicaster;
  13:  
  14:  /**
  15:   *  The Bag is a Collection that supports a notion of a user interface.
  16:   *  The Bag can produce a "visual proxy" that will take the form
  17:   *  of a JList, JComboBox, or JButton (which displays a dialog when
  18:   *  pushed), depending on the amount of screen real estate that's
  19:   *  available to it. The proxy decides how to display itself without
  20:   *  any outside intervention. Moreover, the "visual proxy" automatically
  21:   *  reflects model-level changes such as adding or removing items from the list.
  22:   *  For example, when you add or remove an item from a Bag, any proxies
  23:   *  that have exposed UIs will update their UIs to indicate the change.
  24:   *  
  25:   *  <p>Unlike a Collection, a Bag also has a notion of the "top" element.
  26:   *  (the one that you encounter first when you reach into the bag).
  27:   *  The top element is typically modified by the user selecting
  28:   *  an element in a UI created by a visual proxy. You can find out
  29:   *  when that element changes by registering an ActionListener. You
  30:   *  can also designate an existing bag element as the "top" element
  31:   *  manually by calling {@link Bag#designate_top}.
  32:   *  
  33:   *  <p>As with standard collections, the Bag is not particularly thread
  34:   *  safe. It does take some trouble to make sure that the list of
  35:   *  <code>ActionListener's</code> is handled in a thread-safe way, but
  36:   *  that's it. If you want thread safety, you should wrap a <code>Bag</code>
  37:   *  in a thread-safety wrapper of your own devising. You can use the
  38:   *  methods of the <code>Collection</code> class for this purpose if
  39:   *  you like, but only if you do not call any methods of <code>Bag</code>
  40:   *  that are not defined in the <code>Collection</code> interface.
  41:   *
  42:   *  Though the <code>Bag</code> is not thread safe with respect to
  43:   *  the outside world, it is thread safe with respect to Swing. That is,
  44:   *  it's okay for the <code>Bag</code> to be modified, even if a proxy
  45:   *  is exposing a UI.
  46:   *  
  47:   *  <p>Modifying the wrapped collection directly, rather than by calling a
  48:   *  Bag method, is dangerous. If you need to modify an element, remove it,
  49:   *  change it, then put it back.
  50:   *  
  51:   **/
  52:  
  53:  public class Bag implements User_interface, Collection
  54:  {
  55:      // A bug in the compiler (version 1.2.2 and all prior versions)
  56:      // erroneously prints the error message "xxx may not have been
  57:      // initialized," when the following are made final.  This bug
  58:      // is somehow related to the presence of inner classes, but I'd
  59:      // rather have the inner classes than the immutable fields.
  60:  
  61:      private /*final*/ Collection contents;
  62:      private /*final*/ Comparator sort_strategy;
  63:      private /*final*/ String     name;
  64:      private /*final*/ Vector     proxies = new Vector();
  65:      private           Object     top     = null;
  66:  
  67:      /** Create a bag that uses the indicated Collection to represent
  68:       *  the indicated attribute.
  69:       */
  70:  
  71:      public Bag( Collection contents, String name  )
  72:      {   this.contents       = contents;
  73:          this.name           = name;
  74:          this.sort_strategy  = null;
  75:      }
  76:  
  77:      /** Create a <code>Bag</code> that wraps the <code>Collection</code>
  78:       *  passed in as an argument. When the items in the collection are
  79:       *  displayed, they are first extracted to an array, which is then
  80:       *  sorted using the supplied <code>Comparator</code>. (There's
  81:       *  no point in doing this if the underlying Collection is
  82:       *  already sorted, but it's handy for <code>HashSet</code> objects
  83:       *  and <code>LinkedList</code>.)
  84:       */
  85:  
  86:      public Bag( Collection contents, String name, Comparator sort_strategy )
  87:      {   this.contents       = contents;
  88:          this.name           = name;
  89:          this.sort_strategy  = sort_strategy;
  90:      }
  91:  
  92:      /*==================================================================*/
  93:      /*              Pass-through (to Collection) methods.               */
  94:      /*==================================================================*/
  95:  
  96:      public boolean add   (Object o)       { boolean result = contents.add(o);
  97:                                              changed(1);
  98:                                              return result;
  99:                                            }
100:      public boolean addAll(Collection c)   { boolean result = contents.addAll(c);    
101:                                              changed(1);
102:                                              return result;
103:                                            }
104:      public boolean remove(Object o)       { boolean result = contents.remove(o);    
105:                                              changed(-1);
106:                                              return result;
107:                                            }
108:      public void    clear()                { contents.clear();  
109:                                              changed(-1);
110:                                            }
111:      public boolean removeAll(Collection c){ boolean result = contents.removeAll(c);
112:                                              changed(-1);
113:                                              return result;
114:                                            }
115:      public boolean retainAll(Collection c){ boolean result = contents.retainAll(c);
116:                                              changed(-1);
117:                                              return result;
118:                                            }
119:  
120:      public int      size        ()              {return contents.size();        }
121:      public boolean  isEmpty     ()              {return contents.isEmpty();     }
122:      public boolean  contains    (Object o)      {return contents.contains(o);   }
123:      public Iterator iterator    ()              {return contents.iterator();    }
124:      public Object[] toArray     ()              {return contents.toArray();     }
125:      public Object[] toArray     (Object a[])    {return contents.toArray(a);    }
126:      public boolean  containsAll (Collection c)  {return contents.containsAll(c);}
127:      public boolean  equals      (Object o)      {return contents.equals(o);     }
128:      public int      hashCode    ()              {return contents.hashCode();    }
129:  
130:      /*==================================================================*/
131:      /*                  "Top" item management                           */
132:      /*==================================================================*/
133:  
134:      /** Return the current "top" element.
135:       */
136:  
137:      public Object top()
138:      {   return top;
139:      }
140:  
141:      private ActionListener subscription_list = null;
142:  
143:      /** Add a listener that's notified when the designated "top" element
144:       *  changes, either through a call to {@link #designate_top}
145:       *  or by a user-mandated change made via a visual proxy.
146:       */
147:  
148:      public final void addActionListener( ActionListener subscriber )
149:      {   subscription_list =
150:              AWTEventMulticaster.add( subscription_list, subscriber );
151:      }
152:  
153:      /** Remove a listener added by {@link #addActionListener}.
154:       */
155:  
156:      public final void removeActionListener( ActionListener subscriber )
157:      {   subscription_list =
158:              AWTEventMulticaster.remove( subscription_list, subscriber );
159:      }
160:  
161:      /** Designate a new "top" element and notify any listeners that
162:       *  the top item has changed.. Typically, this will be done
163:       *  by the user picking an element in a proxy, but you can
164:       *  do it manually by calling this method.
165:       *  @param top the new "top" element. This element must be
166:       *          an element of the encapsulated Collection, either added
167:       *          directly or added via the <code>Bag</code> version
168:       *          of {@link add()}.
169:       *  @throws IllegalArgumentException if the argument doesn't
170:       *          identify an item already in the bag.
171:       */
172:  
173:      public void designate_top(final Object top)
174:      {
175:          if( !contents.contains( top ) )
176:              throw new IllegalArgumentException(
177:                          "Designated top item not in Bag (" + top + ")" );
178:          this.top = top;
179:  
180:          if( subscription_list != null )
181:              subscription_list.actionPerformed
182:              (   new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
183:                                                          top.toString())
184:              );
185:  
186:          Object[] copy;
187:          synchronized(proxies){ copy = proxies.toArray(); }
188:  
189:          for( int i = 0; i < copy.length; ++i )
190:          {   final Proxy proxy = (Proxy)copy[i];
191:              SwingUtilities.invokeLater
192:              (   new Runnable()
193:                  {   public void run()
194:                      {   proxy.new_selection(top);
195:                          proxy.repaint();
196:                      }
197:                  }
198:              );
199:          }
200:      }
201:  
202:      /*==================================================================*/
203:      /*                        Visual proxy                              */
204:      /*==================================================================*/
205:  
206:      private void changed( final int direction )
207:      {
208:          Object[] copy;
209:          synchronized(proxies){ copy = proxies.toArray(); }
210:  
211:          for( int i = 0; i < copy.length; ++i )
212:          {   final Proxy proxy = (Proxy)copy[i];
213:              SwingUtilities.invokeLater
214:              (   new Runnable()
215:                  {   public void run()
216:                      {   proxy.changed( direction );
217:                          proxy.repaint();
218:                      }
219:                  }
220:              );
221:          }
222:      }
223:  
224:      /** Manufacture a "visual proxy" for the current <code>Bag</code>,
225:       *  suitable for inclusion in a "form". Currently, only one
226:       *  proxy type (<code>"chooser"<code>) is supported.
227:       *  The <code>Bag</code>
228:       *  displays itself as a JList, JComboBox, or JButton, depending
229:       *  on the amount of screen real estate available to it. In the
230:       *  case of a button, the button label is the <code>attribute</code>
231:       *  argument. Note that you can add an Ancestor listener to the
232:       *  returned proxy to find out when the window that contains
233:       *  the proxy shuts down. The most-recently selected item
234:       *  can be fetched, at that point, by calling {@link #selected_item()}
235:       *
236:       *  @see Form
237:       *  @see User_interface
238:       */
239:  
240:      public JComponent visual_proxy( String attribute_name, boolean is_read_only )
241:      {  
242:          if( !is_read_only )
243:              return null;
244:      
245:          // Set things up so that the proxy will be in the "proxies"
246:          // list whenever it's visible. It's essential that it be removed
247:          // from the list when invisible; otherwise, the reference in
248:          // the list will keep the <code>Proxy</code> object from being garbage
249:          // collected after the containing window shuts down.
250:  
251:          final Proxy proxy = new Proxy( name );
252:          proxy.addAncestorListener
253:          (   new AncestorAdapter()
254:              {   public void ancestorRemoved(AncestorEvent event)    
255:                  {   synchronized( proxies )
256:                      {   proxies.remove( proxy );
257:                      }
258:                  }
259:                  public void ancestorAdded(AncestorEvent event)
260:                  {   synchronized( proxies )
261:                      {   proxies.add( proxy );
262:                      }
263:                  }
264:              }
265:          );
266:          return proxy;
267:      }
268:  
269:      /** The actual visual-proxy class is a JPanel that contains either
270:       *  a button, combo box, or list box, depending on its size. An
271:       *  instance of this class is returned by the <code>visual_proxy()</code>
272:       *  request.
273:       */
274:  
275:      public class Proxy extends JPanel
276:      {
277:          private int         selected_size = 10;
278:          private String      attribute_name;
279:          private JComboBox   drop_down;
280:          private JList       list;
281:          private JButton     button;
282:          private JComponent  current_ui;
283:  
284:          private Model    state  = new Model();
285:          private Renderer artist = new Renderer();
286:  
287:          /** A private constructor, creates a proxy for the current
288:           *  Bag. Since this constructor is private, you may not issue
289:           *  a <code>new Bag("xxx")</code> request. Get a proxy by
290:           *  calling the Bag's {@link Bag#visual_proxy} method.
291:           *
292:           *  @see Bag#visual_proxy.
293:           */
294:  
295:          private Proxy( final String attribute_name )
296:          {
297:              drop_down   = new JComboBox  ( state );
298:              list        = new JList      ( state );
299:              button      = new JButton    ( attribute_name );
300:  
301:              drop_down.setRenderer(artist);
302:              drop_down.setSelectedIndex( 0 );
303:  
304:              // Set up the list. It turns out that the model, though
305:              // sufficient for controlling the appearance of the drop-
306:              // down, is not sufficient to control the list, so set
307:              // up a listener that sets modifies the state of the
308:              // current Bag in response to a selection.
309:  
310:              list.setCellRenderer (artist);
311:              list.addListSelectionListener
312:              (   new ListSelectionListener()
313:                  {   public void valueChanged( ListSelectionEvent event )
314:                      {   if( !event.getValueIsAdjusting() )
315:                              designate_top(list.getSelectedValue());
316:                      }
317:                  }
318:              );
319:  
320:              // In the case of the button, set up a listener to throw up
321:              // the selection box (a small frame containing the JList)
322:              // when the button is pressed. Also arrange for the box
323:              // to pop up over the button, rather than in the upper-left
324:              // corner of the screen. The button is disabled as long as
325:              // the selection box is displayed.
326:  
327:              button.addActionListener
328:              (   new ActionListener()
329:                  {   public void actionPerformed( ActionEvent e )
330:                      {   Window popup = new Popup(attribute_name);
331:                          button.setEnabled(false);
332:                          popup.addWindowListener
333:                          (   new WindowAdapter()
334:                              {   public void windowClosing(WindowEvent e)
335:                                  {   button.setEnabled(true);
336:                                  }
337:                              }
338:                          );
339:                          popup.setLocation( button.getLocationOnScreen() );
340:                          popup.show();
341:                      }
342:                  }
343:              );
344:  
345:              // Arrange for the UI to change it's appearance, if necessary,
346:              // when the size changes.
347:  
348:              this.addComponentListener
349:              (   new ComponentAdapter()
350:                  {   public void componentResized(ComponentEvent e)
351:                      {   install_ui();
352:                      }
353:                  }
354:              );
355:  
356:              this.attribute_name = attribute_name;
357:              setLayout( new BorderLayout() );
358:          }
359:  
360:          /** Examines the size of the <code>Proxy</code> object and
361:           *  installs the button, drop-down, or list as appropriate.
362:           *  does nothing if the correct widget is already displayed.
363:           */
364:  
365:          private final void install_ui()
366:          {
367:              Rectangle bounds         = getBounds();
368:              Dimension drop_down_size = drop_down.getPreferredSize();
369:  
370:              boolean use_list   = bounds.height > (drop_down_size.height *3 )
371:                                && bounds.width  > (drop_down_size.width  +10);
372:              boolean use_button = bounds.width  < drop_down_size.width
373:                                || bounds.height < drop_down_size.height;
374:              boolean use_drop   = !use_list && !use_button;
375:  
376:              if( use_list    && current_ui == list       )   return;
377:              if( use_button  && current_ui == button     )   return;
378:              if( use_drop    && current_ui == drop_down  )   return;
379:  
380:              if( current_ui != null )
381:                  current_ui.setVisible( false );
382:              this.removeAll();
383:  
384:              if( use_button )
385:                  this.add( current_ui = button, BorderLayout.CENTER );
386:              else if( use_list )
387:                  this.add( new JScrollPane(current_ui = list), BorderLayout.CENTER );
388:              else
389:              {   if( current_ui == null )
390:                      drop_down.setSelectedIndex(0);
391:                  this.add( current_ui = drop_down, BorderLayout.NORTH );
392:              }
393:  
394:              current_ui.setVisible( true );
395:              
396:              // Make the newly added component visible, note that
397:              // repaint(), invalidate(), and doLayout() do not work
398:              // for this purpose (probably a bug).
399:  
400:              this.setVisible( false );
401:              this.setVisible( true );
402:          }
403:  
404:          /** Called when the state of the underlying <code>Collection</code>
405:           *  is changed by calling <code>add()</code>, <code>remove()</code>,
406:           *  etc.
407:           *  @param direction    <o if the collection has gotten smaller,<br>
408:           *                      >0 if it's gotten larger.<br>
409:           *                      =0 if it hasn't changed size, but needs
410:           *                          to be refreshed.
411:           */
412:  
413:          public void changed(int direction)
414:          {   int selected_index = list.getSelectedIndex();
415:  
416:              if( selected_index < 0 )
417:                  selected_index = 0;
418:  
419:              if( selected_index >= contents.size() )
420:                  selected_index = contents.size()-1;
421:  
422:              state.changed(direction);
423:  
424:              list.setSelectedIndex       (selected_index);
425:              list.ensureIndexIsVisible   (selected_index);
426:  
427:              drop_down.setSelectedIndex  (selected_index);
428:          }
429:  
430:          /** Called when the user picks an item from the current UI.
431:           *  Makes sure that the drop-down and the list stay in synch
432:           *  with each other.
433:           */
434:  
435:          public void new_selection( Object selected )
436:          {   if( list.getSelectedValue()  != selected )
437:              {   list.setSelectedValue    ( selected, true );
438:                  list.ensureIndexIsVisible( list.getSelectedIndex() );
439:              }
440:              if( drop_down.getSelectedItem() != selected )
441:              {   drop_down.setSelectedItem( selected );
442:              }
443:          }
444:  
445:          /** Overrides the base-class method, so must be public. Do not
446:           *  call this method. Installs the ui when the proxy is
447:           *  displayed the first time.
448:           */
449:  
450:          public void addNotify()
451:          {   super.addNotify();
452:              install_ui();
453:          }
454:  
455:          /*******************************************************************
456:           * The "model" class, holds the state of the font-name combo box.
457:           * The list of possible fonts is stored here, as is the currently-selected
458:           * font.
459:           */
460:  
461:          private final class Model   extends     AbstractListModel
462:                                      implements  ComboBoxModel
463:          {
464:              public Object getSelectedItem(         ){ return top();           }
465:              public void   setSelectedItem(Object o ){ designate_top(o);       }
466:  
467:              public int    getSize        (         ){ return contents.size(); }
468:              public Object getElementAt   (int index)
469:              {   Object[] items = contents.toArray();
470:                  if( sort_strategy != null )
471:                  {   Arrays.sort( items, sort_strategy );
472:                  }
473:                  return (0 <= index && index < items.length) ? items[index]: null;
474:              }
475:  
476:              /** Call this method if the  collection changes in some way.
477:               *  @param direction should be a negative number if the
478:               *              collection got smaller. A positive number
479:               *              if it got larger, 0 if it didn't change
480:               *              size, but the UI needs redrawing anyway.
481:               */
482:  
483:              public void changed( int direction )
484:              {  
485:                  if( direction < 0 )
486:                      fireIntervalAdded  (this, 0, contents.size()-1);
487:                  else if( direction > 0 )
488:                      fireIntervalRemoved(this, 0, contents.size()-1);
489:                  else
490:                      fireContentsChanged(this, 0, contents.size()-1);
491:              }
492:          }
493:  
494:          /*******************************************************************
495:           * The JList and JComboBox use the Renderer to draw its cells.
496:           */
497:  
498:          private final class Renderer implements ListCellRenderer  
499:          {  
500:              private JPanel selected_wrapper = new JPanel();
501:  
502:              public Renderer()
503:              {   selected_wrapper.setLayout( new BorderLayout() );
504:                  selected_wrapper.setBorder(
505:                              BorderFactory.createLineBorder(Color.red) );
506:              }
507:  
508:              // Return a Component whose paint method is used to draw
509:              // the cell. Note that that's the only thing that the
510:              // Component is used for. The component itself it not put
511:              // into the cell.
512:  
513:              public Component getListCellRendererComponent(
514:                                          JList list, Object value, int index,
515:                                          boolean isSelected, boolean cellHasFocus)
516:              {
517:                  JComponent item;
518:  
519:                  if(value == null )
520:                      value = "????";
521:  
522:                  if( value instanceof User_interface )
523:                  {   item = ((User_interface)value).visual_proxy(null,true);
524:                  }
525:                  else
526:                  {   item = new JLabel( value.toString() )
527:                      {   public Dimension getPreferredSize()
528:                          {   Dimension d = super.getPreferredSize();
529:                              d.height += 8;
530:                              d.width  += Math.min( d.width+8, 100 );
531:                              return d;
532:                          }
533:                      };
534:                  }
535:  
536:                  if( isSelected )
537:                  {  
538:                      selected_wrapper.removeAll();
539:                      selected_wrapper.add( item, BorderLayout.CENTER );
540:                      item = selected_wrapper;
541:                  }
542:  
543:                  return item;
544:              }
545:          }
546:  
547:          /***************************************************************
548:           * The "selection frame" that pop's up when the button UI is
549:           * pressed. The (non-modal) frame contains a panel with an
550:           * etched border, which in turn, holds the same JList object
551:           * that appears when the Proxy has enough room to display it.
552:           */
553:  
554:          private final class Popup extends JFrame
555:          {  
556:              public Popup( String attribute_name )
557:              {   JPanel interior = new JPanel();
558:                  interior.setBorder
559:                          (   BorderFactory.createTitledBorder
560:                              (   new EtchedBorder(Color.white,Color.gray),
561:                                  attribute_name
562:                              )
563:                          );
564:  
565:                  interior.setLayout( new BorderLayout() );
566:                  interior.add( new JScrollPane(list), BorderLayout.CENTER );
567:  
568:                  list.setVisible( true );
569:                  setContentPane( interior );
570:                  pack();
571:              }
572:          }
573:      }
574:  
575:      /*==================================================================*/
576:      /*                              TEST                                */
577:      /*==================================================================*/
578:  
579:      /** This small test class demonstrates how to use a Bag. It creates a
580:       *  <code>Bag</code>, puts a few items in it, displays two UIs, then
581:       *  allows you to add elements to the collection. You can resize the
582:       *  windows to watch the display change from on appearance to another,
583:       *  and when you type lines into the console window, the new lines
584:       *  appear in both of the UI windows. Also notice that the proxies
585:       *  stay in synch with each other with respect to selection as
586:       *  well (If you don't want this last behavior, wrap the same
587:       *  <code>Collection</code> object into two distinct <code>Bag</code>
588:       *  objects.
589:       */
590:  
591:      public static class Test
592:      {
593:          public void create_ui( Collection aggregate )
594:          {
595:              JFrame frame = new JFrame();
596:  
597:              // Get the visual proxy and shove it into the Frame
598:  
599:              User_interface displayable = (User_interface)aggregate;
600:              frame.getContentPane().add(
601:                          displayable.visual_proxy("Attribute", true) );
602:  
603:              // Set up a window-closing handler and pop the frame up.
604:  
605:              frame.addWindowListener
606:                  (   new WindowAdapter()
607:                      {   public void windowClosing( WindowEvent e )
608:                          {   System.exit(0);
609:                          }
610:                      }
611:                  );
612:              frame.pack();
613:              frame.show();
614:          }
615:  
616:          public static void main( String[] args ) throws Exception
617:          {  
618:              Collection aggregate = new Bag( new LinkedList(), "outer" );
619:              aggregate.add("A");
620:              aggregate.add("B");
621:              aggregate.add("C");
622:              aggregate.add("D");
623:  
624:              // You need to treat it as a Bag (as compared to a generic
625:              // Collection) to install an ActionListener, thus the cast.
626:              // It has to be final because it's referenced by the inner-
627:              // class object. Note that the listener will report all the
628:              // selections associated with displaying the initial UI.
629:              // There will be two such notifications (one for the list
630:              // and one for the drop-down) for each of the three proxies.
631:              // If you don't want this behavior, install the listener
632:              // after the visual proxy has been displayed.
633:  
634:              final Bag the_bag = (Bag) aggregate;
635:              the_bag.addActionListener
636:              (   new ActionListener()
637:                  {   public void actionPerformed( ActionEvent e )
638:                      {   System.out.println( "Selected " + the_bag.top() );
639:                      }
640:                  }
641:              );
642:  
643:              create_ui( aggregate );
644:              create_ui( aggregate );
645:              create_ui( aggregate );
646:  
647:              // Transfer all lines typed on the console to the collection.
648:  
649:              String s;
650:              while( (s = com.holub.tools.Std.in().readLine()) != null )
651:              {   com.holub.tools.Std.out().println( "->" + s );
652:                  aggregate.add( s );
653:              }
654:          }
655:      }
656:      //END_TEST
657:  }
          



Copyright 2001-2002, PC WORLD Iran/International Data Group Inc./ . All rights reserved.


ÿ‡œ‹ÍÕœ
ÞŸÁ ËÍß› ÍË ÁÍ Ëߊ ÍÕœ
:ËŸŸß˜ƒ ‚› àƒ Ã™Ô›

ÑÎÍÁ ‚ƒ ÿ™ŸŠ -
ÑÎÍÁ ‚ƒ -
ÑÎÍÁÿƒ -

ÿœ šßÔ ÿ™ŸŠ -
Ë‚œ› -
à‚…ß— -

ÿŠ… ÿ™ŸŠ -
Ãߙԛ -
Å‹Ÿœ ÿŠ… ÿ•‚— àÎÁËœÁ àƒ -


:Ý‚œ

:ÚŸœßÍ…—™Á Å‹„

: Å—ÍŒ Ý‚œ

܃– ÿ‚Å›‹–
Link 1
Link 2
Link 3
Link 4



Advertisements and other stuffs