/[svn]/gigedit/trunk/src/gigedit/paramedit.cpp
ViewVC logotype

Diff of /gigedit/trunk/src/gigedit/paramedit.cpp

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 1460 by persson, Sat Oct 27 12:28:33 2007 UTC revision 3364 by schoenebeck, Tue Nov 14 18:07:25 2017 UTC
# Line 1  Line 1 
1  /*  /*
2   * Copyright (C) 2006, 2007 Andreas Persson   * Copyright (C) 2006-2017 Andreas Persson
3   *   *
4   * This program is free software; you can redistribute it and/or   * This program is free software; you can redistribute it and/or
5   * modify it under the terms of the GNU General Public License as   * modify it under the terms of the GNU General Public License as
# Line 17  Line 17 
17   * 02110-1301 USA.   * 02110-1301 USA.
18   */   */
19    
20    #include "global.h"
21  #include "paramedit.h"  #include "paramedit.h"
22    
23    #include "compat.h"
24    #include "Settings.h"
25    
26    #include <gtkmm/messagedialog.h>
27    
28  namespace {  namespace {
29      const char* const controlChangeTexts[] = {      struct CCText {
30          "none", "channelaftertouch", "velocity",          const char* const txt;
31          0,          bool isExtension; ///< True if this is a controller only supported by LinuxSampler, but not supperted by Gigasampler/GigaStudio.
32          "modwheel", // "Modulation Wheel or Lever",      };
33          "breath", // "Breath Controller",      static const CCText controlChangeTexts[] = {
34          0,          // 3 special ones (not being CCs)
35          "foot", // "Foot Controller",          { _("none") }, { _("channelaftertouch") }, { _("velocity") },
36          "portamentotime", // "Portamento Time",          {0}, // bank select MSB (hard coded in sampler, so discouraged to be used here, even though considerable)
37          0, 0, 0, 0, 0, 0,          { _("modwheel") }, // "Modulation Wheel or Lever",
38          "effect1", // "Effect Control 1",          { _("breath") }, // "Breath Controller",
39          "effect2", // "Effect Control 2",          { _("undefined"), true },
40          0, 0,          { _("foot") }, // "Foot Controller",
41          "genpurpose1", // "General Purpose Controller 1",          { _("portamentotime") }, // "Portamento Time",
42          "genpurpose2", // "General Purpose Controller 2",          { _("data entry MSB"), true },
43          "genpurpose3", // "General Purpose Controller 3",          { _("volume"), true },
44          "genpurpose4", // "General Purpose Controller 4",          { _("balance"), true },
45          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,          { _("undefined"), true },
46          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,          { _("pan"), true },
47          0, 0, 0, 0, 0, 0,          { _("expression"), true },
48          "sustainpedal", // "Damper Pedal on/off (Sustain)",          { _("effect1") }, // "Effect Control 1",
49          "portamento", // "Portamento On/Off",          { _("effect2") }, // "Effect Control 2",
50          "sostenuto", // "Sustenuto On/Off",          { _("undefined"), true },
51          "softpedal", // "Soft Pedal On/Off",          { _("undefined"), true },
52          0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,          { _("genpurpose1") }, // "General Purpose Controller 1",
53          "genpurpose5", // "General Purpose Controller 5",          { _("genpurpose2") }, // "General Purpose Controller 2",
54          "genpurpose6", // "General Purpose Controller 6",          { _("genpurpose3") }, // "General Purpose Controller 3",
55          "genpurpose7", // "General Purpose Controller 7",          { _("genpurpose4") }, // "General Purpose Controller 4",
56          "genpurpose8", // "General Purpose Controller 8",          { _("undefined"), true },
57          0, 0, 0, 0, 0, 0, 0,          { _("undefined"), true },
58          "effect1depth", // "Effects 1 Depth",          { _("undefined"), true },
59          "effect2depth", // "Effects 2 Depth",          { _("undefined"), true },
60          "effect3depth", // "Effects 3 Depth",          { _("undefined"), true },
61          "effect4depth", // "Effects 4 Depth",          { _("undefined"), true },
62          "effect5depth", // "Effects 5 Depth"          { _("undefined"), true },
63            { _("undefined"), true },
64            { _("undefined"), true },
65            { _("undefined"), true },
66            { _("undefined"), true },
67            { _("undefined"), true },
68            
69            // LSB variant of the various controllers above
70            // (so discouraged to be used here for now)
71            {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0},
72            {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0}, {0},
73            {0}, {0}, {0}, {0},
74            
75            { _("sustainpedal") }, // "Damper Pedal on/off (Sustain)",
76            { _("portamento") }, // "Portamento On/Off",
77            { _("sostenuto") }, // "Sustenuto On/Off",
78            { _("softpedal") }, // "Soft Pedal On/Off",
79            { _("legato"), true },
80            { _("hold2"), true },
81            { _("soundvariation"), true },
82            { _("timbre"), true },
83            { _("releasetime"), true },
84            { _("attacktime"), true },
85            { _("brightness"), true },
86            { _("decaytime"), true },
87            { _("vibratorate"), true },
88            { _("vibratodepth"), true },
89            { _("vibratodelay"), true },
90            { _("undefined"), true },
91            { _("genpurpose5") }, // "General Purpose Controller 5",
92            { _("genpurpose6") }, // "General Purpose Controller 6",
93            { _("genpurpose7") }, // "General Purpose Controller 7",
94            { _("genpurpose8") }, // "General Purpose Controller 8",
95            { _("portamentoctrl"), true },
96            { _("undefined"), true },
97            { _("undefined"), true },
98            { _("undefined"), true },
99            {0}, // high resolution velocity prefix (so discouraged to be used here)
100            { _("undefined"), true },
101            { _("undefined"), true },
102            { _("effect1depth") }, // "Effects 1 Depth",
103            { _("effect2depth") }, // "Effects 2 Depth",
104            { _("effect3depth") }, // "Effects 3 Depth",
105            { _("effect4depth") }, // "Effects 4 Depth",
106            { _("effect5depth") }, // "Effects 5 Depth"
107            { _("dataincrement"), true },
108            { _("datadecrement"), true },
109            {0}, // NRPN LSB (so discouraged to be used here)
110            {0}, // NRPN MSB (so discouraged to be used here)
111            {0}, // RPN LSB (so discouraged to be used here)
112            {0}, // RPN MSB (so discouraged to be used here)
113            { _("undefined"), true },
114            { _("undefined"), true },
115            { _("undefined"), true },
116            { _("undefined"), true },
117            { _("undefined"), true },
118            { _("undefined"), true },
119            { _("undefined"), true },
120            { _("undefined"), true },
121            { _("undefined"), true },
122            { _("undefined"), true },
123            { _("undefined"), true },
124            { _("undefined"), true },
125            { _("undefined"), true },
126            { _("undefined"), true },
127            { _("undefined"), true },
128            { _("undefined"), true },
129            { _("undefined"), true },
130            { _("undefined"), true } // CC 119
131            // (all other ones that follow [CC 120- CC 127] are hard coded channel
132            // mode messages, so those are discouraged to be used here)
133      };      };
134  }  }
135    
136    #define controlChangeTextsSize  (sizeof(controlChangeTexts) / sizeof(CCText))
137    
138  LabelWidget::LabelWidget(const char* labelText, Gtk::Widget& widget) :  LabelWidget::LabelWidget(const char* labelText, Gtk::Widget& widget) :
139      label(Glib::ustring(labelText) + ":"),      label(Glib::ustring(labelText) + ":"),
140      widget(widget)      widget(widget)
141  {  {
142      label.set_alignment(Gtk::ALIGN_LEFT);  #if HAS_GTKMM_ALIGNMENT
143        label.set_alignment(Gtk::ALIGN_START);
144    #else
145        label.set_halign(Gtk::Align::START);
146    #endif
147  }  }
148    
149  void LabelWidget::set_sensitive(bool sensitive)  void LabelWidget::set_sensitive(bool sensitive)
# Line 70  void LabelWidget::set_sensitive(bool sen Line 152  void LabelWidget::set_sensitive(bool sen
152      widget.set_sensitive(sensitive);      widget.set_sensitive(sensitive);
153  }  }
154    
155    ReadOnlyLabelWidget::ReadOnlyLabelWidget(const char* leftHandText)
156        : LabelWidget(leftHandText, text)
157    {
158    #if HAS_GTKMM_ALIGNMENT
159        text.set_alignment(Gtk::ALIGN_START, Gtk::ALIGN_START);
160    #else
161        label.set_halign(Gtk::Align::START);
162        label.set_valign(Gtk::Align::START);
163    #endif
164    }
165    
166    ReadOnlyLabelWidget::ReadOnlyLabelWidget(const char* leftHandText, const char* rightHandText)
167       : LabelWidget(leftHandText, text)
168    {
169    #if HAS_GTKMM_ALIGNMENT
170        text.set_alignment(Gtk::ALIGN_START, Gtk::ALIGN_START);
171    #else
172        text.set_halign(Gtk::Align::START);
173        text.set_valign(Gtk::Align::START);
174    #endif
175        text.set_text(rightHandText);
176    }
177    
178  NumEntry::NumEntry(const char* labelText, double lower, double upper,  NumEntry::NumEntry(const char* labelText, double lower, double upper,
179                     int decimals) :                     int decimals) :
180        LabelWidget(labelText, box),
181    #if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 90) || GTKMM_MAJOR_VERSION < 2
182      adjust(lower, lower, upper, 1, 10),      adjust(lower, lower, upper, 1, 10),
183    #else
184        adjust(Gtk::Adjustment::create(lower, lower, upper, 1, 10)),
185    #endif
186      scale(adjust),      scale(adjust),
187      spinbutton(adjust),      spinbutton(adjust)
     LabelWidget(labelText, box)  
188  {  {
189        scale.set_size_request(70);
190      spinbutton.set_digits(decimals);      spinbutton.set_digits(decimals);
191      spinbutton.set_value(0);      spinbutton.set_value(0);
192        spinbutton.set_numeric();
193      scale.set_draw_value(false);      scale.set_draw_value(false);
194      box.pack_start(spinbutton, Gtk::PACK_SHRINK);      box.pack_start(spinbutton, Gtk::PACK_SHRINK);
195      box.add(scale);      box.add(scale);
# Line 88  NumEntryGain::NumEntryGain(const char* l Line 199  NumEntryGain::NumEntryGain(const char* l
199                             double lower, double upper,                             double lower, double upper,
200                             int decimals, double coeff) :                             int decimals, double coeff) :
201      NumEntry(labelText, lower, upper, decimals),      NumEntry(labelText, lower, upper, decimals),
     coeff(coeff),  
202      value(0),      value(0),
203        coeff(coeff),
204      connected(true)      connected(true)
205  {  {
206      spinbutton.signal_value_changed().connect(      spinbutton.signal_value_changed().connect(
# Line 179  void NumEntryPermille::set_value(uint16_ Line 290  void NumEntryPermille::set_value(uint16_
290  NoteEntry::NoteEntry(const char* labelText) :  NoteEntry::NoteEntry(const char* labelText) :
291      NumEntryTemp<uint8_t>(labelText)      NumEntryTemp<uint8_t>(labelText)
292  {  {
293      spinbutton.signal_input().connect(      spin_button_show_notes(spinbutton);
         sigc::mem_fun(*this, &NoteEntry::on_input));  
     spinbutton.signal_output().connect(  
         sigc::mem_fun(*this, &NoteEntry::on_output));  
294  }  }
295    
296  const char* notes[] = {  namespace {
297      "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"      const char* notes[] = {
298  };          _("C"), _("C#"), _("D"), _("D#"), _("E"), _("F"),_("F#"),
299            _("G"), _("G#"), _("A"), _("A#"), _("B")
300        };
301    
302        int note_value(const Glib::ustring& note, double* value)
303        {
304            const char* str = note.c_str();
305    
306            int i;
307            for (i = 11 ; i >= 0 ; i--) {
308                if (strncasecmp(str, notes[i], strlen(notes[i])) == 0) break;
309            }
310            if (i >= 0) {
311                char* endptr;
312                long x = strtol(str + strlen(notes[i]), &endptr, 10);
313                if (endptr != str + strlen(notes[i])) {
314                    *value = std::max(0L, std::min(i + (x + 1) * 12, 127L));
315                    return true;
316                }
317            } else {
318                char* endptr;
319                long x = strtol(str, &endptr, 10);
320                if (endptr != str) {
321                    *value = std::max(0L, std::min(x, 127L));
322                    return true;
323                }
324            }
325    
326    #if HAS_GTKMM_CPP11_ENUMS
327            return Gtk::SpinButton::INPUT_ERROR;
328    #else
329            return Gtk::INPUT_ERROR;
330    #endif
331        }
332    }
333    
334    int note_value(const Glib::ustring& note)
335    {
336        double value = 0;
337        note_value(note, &value);
338        return value;
339    }
340    
341  // Convert the Entry text to a number  Glib::ustring note_str(int note)
 int NoteEntry::on_input(double* new_value)  
342  {  {
343      const char* str = spinbutton.get_text().c_str();      char buf[10];
344        sprintf(buf, "%s%d", notes[note % 12], note / 12 - 1);
345        return buf;
346    }
347    
348      int i;  namespace {
349      for (i = 11 ; i >= 0 ; i--) {      // Convert the Entry text to a number
350          if (strncmp(str, notes[i], strlen(notes[i])) == 0) break;  #if GTKMM_MAJOR_VERSION > 3 || (GTKMM_MAJOR_VERSION == 3 && (GTKMM_MINOR_VERSION > 91 || (GTKMM_MINOR_VERSION == 91 && GTKMM_MICRO_VERSION >= 2))) // GTKMM >= 3.91.2
351        int on_input(double& new_value, Gtk::SpinButton* spinbutton) {
352            return note_value(spinbutton->get_text(), &new_value);
353      }      }
354      if (i >= 0) {  #else
355          char* endptr;      int on_input(double* new_value, Gtk::SpinButton* spinbutton) {
356          long x = strtol(str + strlen(notes[i]), &endptr, 10);          return note_value(spinbutton->get_text(), new_value);
357          if (endptr != str + strlen(notes[i])) {      }
358              *new_value = i + (x + 1) * 12;  #endif
359              return true;  
360          }      // Convert the Adjustment position to text
361        bool on_output(Gtk::SpinButton* spinbutton) {
362            spinbutton->set_text(
363                note_str(spinbutton->get_adjustment()->get_value() + 0.5));
364            return true;
365      }      }
     return Gtk::INPUT_ERROR;  
366  }  }
367    
368  // Convert the Adjustment position to text  // Make a SpinButton show notes instead of numbers
369  bool NoteEntry::on_output()  void spin_button_show_notes(Gtk::SpinButton& spin_button)
370  {  {
371      int x = int(spinbutton.get_adjustment()->get_value() + 0.5);      spin_button.set_numeric(false);
372      char buf[10];      spin_button.set_width_chars(4);
373      sprintf(buf, "%s%d", notes[x % 12], x / 12 - 1);      spin_button.signal_input().connect(
374      spinbutton.set_text(buf);          sigc::bind(sigc::ptr_fun(&on_input), &spin_button));
375      return true;      spin_button.signal_output().connect(
376            sigc::bind(sigc::ptr_fun(&on_output), &spin_button));
377  }  }
378    
379  ChoiceEntryLeverageCtrl::ChoiceEntryLeverageCtrl(const char* labelText) :  ChoiceEntryLeverageCtrl::ChoiceEntryLeverageCtrl(const char* labelText) :
380      align(0, 0, 0, 0),  #if HAS_GTKMM_ALIGNMENT
381      LabelWidget(labelText, align)      LabelWidget(labelText, align),
382  {      align(0, 0, 0, 0)
383      for (int i = 0 ; i < 99 ; i++) {  #else
384          if (controlChangeTexts[i]) {      LabelWidget(labelText, combobox)
385              combobox.append_text(controlChangeTexts[i]);  #endif
386    {
387        for (int i = 0 ; i < controlChangeTextsSize ; i++) {
388            if (controlChangeTexts[i].txt) {
389                const int cc = i - 3;
390                Glib::ustring s = (i < 3)
391                    ? controlChangeTexts[i].txt
392                    : Glib::ustring::compose("CC%1: %2%3", cc, controlChangeTexts[i].txt, controlChangeTexts[i].isExtension ? " [EXT]" : "");
393    #if (GTKMM_MAJOR_VERSION == 2 && GTKMM_MINOR_VERSION < 24) || GTKMM_MAJOR_VERSION < 2
394                combobox.append_text(s);
395    #else
396                combobox.append(s);
397    #endif
398          }          }
399      }      }
400      combobox.signal_changed().connect(      combobox.signal_changed().connect(
401          sigc::mem_fun(*this, &ChoiceEntryLeverageCtrl::value_changed));          sigc::mem_fun(*this, &ChoiceEntryLeverageCtrl::value_changed));
402    #if HAS_GTKMM_ALIGNMENT
403      align.add(combobox);      align.add(combobox);
404    #else
405        combobox.set_halign(Gtk::Align::FILL);
406        combobox.set_valign(Gtk::Align::FILL);
407    #endif
408      value.type = gig::leverage_ctrl_t::type_none;      value.type = gig::leverage_ctrl_t::type_none;
409      value.controller_number = 0;      value.controller_number = 0;
410  }  }
# Line 255  void ChoiceEntryLeverageCtrl::value_chan Line 428  void ChoiceEntryLeverageCtrl::value_chan
428      default:      default:
429          value.type = gig::leverage_ctrl_t::type_controlchange;          value.type = gig::leverage_ctrl_t::type_controlchange;
430          int x = 3;          int x = 3;
431          for (int cc = 0 ; cc < 96 ; cc++) {          for (uint cc = 0 ; cc < controlChangeTextsSize - 3 ; cc++) {
432              if (controlChangeTexts[cc + 3]) {              if (controlChangeTexts[cc + 3].txt) {
433                  if (rowno == x) {                  if (rowno == x) {
434                      value.controller_number = cc;                      value.controller_number = cc;
435                        if (controlChangeTexts[cc + 3].isExtension &&
436                            Settings::singleton()->warnUserOnExtensions)
437                        {
438                            Glib::ustring txt = _("<b>Format Extension</b>\n\nAll controllers marked with \"<b>[EXT]</b>\" are an extension to the original gig sound format. They will only work with LinuxSampler, but they will <b>not work</b> with Gigasampler/GigaStudio!\n\n(You may disable this warning in the <i>Settings</i> menu.)");
439                            Gtk::MessageDialog msg(
440                                txt, true, Gtk::MESSAGE_WARNING
441                            );
442                            msg.run();
443                        }
444                      break;                      break;
445                  }                  }
446                  x++;                  x++;
# Line 271  void ChoiceEntryLeverageCtrl::value_chan Line 453  void ChoiceEntryLeverageCtrl::value_chan
453    
454  void ChoiceEntryLeverageCtrl::set_value(gig::leverage_ctrl_t value)  void ChoiceEntryLeverageCtrl::set_value(gig::leverage_ctrl_t value)
455  {  {
456      int x;      int comboIndex;
457      switch (value.type)      switch (value.type)
458      {      {
459      case gig::leverage_ctrl_t::type_none:      case gig::leverage_ctrl_t::type_none:
460          x = 0;          comboIndex = 0;
461          break;          break;
462      case gig::leverage_ctrl_t::type_channelaftertouch:      case gig::leverage_ctrl_t::type_channelaftertouch:
463          x = 1;          comboIndex = 1;
464          break;          break;
465      case gig::leverage_ctrl_t::type_velocity:      case gig::leverage_ctrl_t::type_velocity:
466          x = 2;          comboIndex = 2;
467          break;          break;
468      case gig::leverage_ctrl_t::type_controlchange:      case gig::leverage_ctrl_t::type_controlchange: {
469          x = -1;          comboIndex = -1;
470          for (int cc = 0 ; cc < 96 ; cc++) {          int x = 3;
471              if (controlChangeTexts[cc + 3]) {          for (uint cc = 0 ; cc < controlChangeTextsSize - 3 ; cc++) {
472                  x++;              if (controlChangeTexts[cc + 3].txt) {
473                  if (value.controller_number == cc) {                  if (value.controller_number == cc) {
474                      x += 3;                      comboIndex = x;
475                      break;                      break;
476                  }                  }
477                    x++;
478              }              }
479          }          }
480          break;          break;
481        }
482      default:      default:
483          x = -1;          comboIndex = -1;
484          break;          break;
485      }      }
486      combobox.set_active(x);      combobox.set_active(comboIndex);
487  }  }
488    
489    
# Line 314  BoolEntry::BoolEntry(const char* labelTe Line 498  BoolEntry::BoolEntry(const char* labelTe
498  StringEntry::StringEntry(const char* labelText) :  StringEntry::StringEntry(const char* labelText) :
499      LabelWidget(labelText, entry)      LabelWidget(labelText, entry)
500  {  {
501      entry.signal_changed().connect(      entry.signal_changed().connect(sig_changed.make_slot());
         sigc::mem_fun(*this, &StringEntry::value_changed));  
502  }  }
503    
504  void StringEntry::value_changed()  gig::String StringEntry::get_value() const
505  {  {
506      if (ptr) {      return gig_from_utf8(entry.get_text());
507          *ptr = entry.get_text();  }
508          sig_changed();  
509      }  void StringEntry::set_value(const gig::String& value) {
510        entry.set_text(gig_to_utf8(value));
511    }
512    
513    
514    StringEntryMultiLine::StringEntryMultiLine(const char* labelText) :
515        LabelWidget(labelText, frame)
516    {
517        text_buffer = text_view.get_buffer();
518        frame.set_shadow_type(Gtk::SHADOW_IN);
519        frame.add(text_view);
520        text_buffer->signal_changed().connect(sig_changed.make_slot());
521    }
522    
523    gig::String StringEntryMultiLine::get_value() const
524    {
525        Glib::ustring value = text_buffer->get_text();
526        for (int i = 0 ; (i = value.find("\x0a", i)) >= 0 ; i += 2)
527            value.replace(i, 1, "\x0d\x0a");
528        return gig_from_utf8(value);
529    }
530    
531    void StringEntryMultiLine::set_value(const gig::String& value)
532    {
533        Glib::ustring text = gig_to_utf8(value);
534        for (int i = 0 ; (i = text.find("\x0d\x0a", i, 2)) >= 0 ; i++)
535            text.replace(i, 2, "\x0a");
536        text_buffer->set_text(text);
537    }
538    
539    
540    Table::Table(int x, int y) :
541    #if USE_GTKMM_GRID
542        Gtk::Grid(),
543        cols(x),
544    #else
545        Gtk::Table(x, y),
546    #endif
547        rowno(0)
548    {
549    }
550    
551    void Table::add(BoolEntry& boolentry)
552    {
553    #if USE_GTKMM_GRID
554        attach(boolentry.widget, 0, rowno, 2);
555    #else
556        attach(boolentry.widget, 0, 2, rowno, rowno + 1,
557               Gtk::FILL, Gtk::SHRINK);
558    #endif
559        rowno++;
560    }
561    
562    void Table::add(BoolEntryPlus6& boolentry)
563    {
564    #if USE_GTKMM_GRID
565        attach(boolentry.widget, 0, rowno, 2);
566    #else
567        attach(boolentry.widget, 0, 2, rowno, rowno + 1,
568               Gtk::FILL, Gtk::SHRINK);
569    #endif
570        rowno++;
571  }  }
572    
573  void StringEntry::set_ptr(gig::String* ptr)  void Table::add(LabelWidget& prop)
574  {  {
575      this->ptr = 0;  #if USE_GTKMM_GRID
576      entry.set_text(*ptr);      attach(prop.label, 1, rowno);
577      this->ptr = ptr;      attach(prop.widget, 2, rowno);
578    #else
579        attach(prop.label, 1, 2, rowno, rowno + 1,
580               Gtk::FILL, Gtk::SHRINK);
581        attach(prop.widget, 2, 3, rowno, rowno + 1,
582               Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK);
583    #endif
584        rowno++;
585  }  }

Legend:
Removed from v.1460  
changed lines
  Added in v.3364

  ViewVC Help
Powered by ViewVC