View Issue Details

IDProjectCategoryView StatusLast Update
0002482ardourfeaturespublic2012-05-23 16:45
Reportercolinf Assigned Topaul  
PrioritynormalSeveritytweakReproducibilityalways
Status closedResolutionfixed 
Product VersionSVN/2.0-ongoing 
Target Version3.0-beta1 
Summary0002482: LADSPA plugin windows are sometimes confusingly arranged.
DescriptionMany LADSPA plugins have parameters that could be considered in groups, e.g. the bands of multi-band parametric equalisers.

Especially when a plugin has many parameters, it's sometimes confusing when parameters which belong together are split across columns, and there's no visual cue as to which parameters are related.

It's possible to make a guess as to which parameters should be considered as groups based on their names, in order to (a) try to avoid splitting groups between columns and (b) make some visual distinction of which controls are grouped.
Additional InformationI've hacked together a patch which does this, based on whether it finds a numeric sub-string of the same value as, or a first or last word in common with the previous parameter.

It is indeed pretty hacky & horrible, in both concept and implementation, but actually works surprisingly well on pretty much all the LADSPA plugins I've tried it with.

If anyone thinks the idea is worthwhile, I can maybe have a go at implementing it a bit more nicely.
TagsNo tags attached.

Activities

2008-12-05 14:19

 

generic-plugin-sections.patch (5,119 bytes)   
Index: gtk2_ardour/generic_pluginui.cc
===================================================================
--- gtk2_ardour/generic_pluginui.cc	(revision 4281)
+++ gtk2_ardour/generic_pluginui.cc	(working copy)
@@ -121,9 +121,119 @@
 	}
 }
 
+
+static int numeric_value(string label, int pos) {
+int value = -1;
+
+	stringstream s;
+	s << label.substr(pos);
+	s >> value;
+	
+	return value;
+
+}
+
+static void split_label(string label, string &first, string &last, int &num) {
+static const char *digits = "0123456789";
+static const char *whitespace = " \t\r\n";
+ 
+
+	unsigned int first_digit_pos = label.find_first_of(digits);
+	
+
+	if (first_digit_pos != string::npos) {
+		// found some digits
+		num = numeric_value(label, first_digit_pos);
+
+		if (first_digit_pos > 0) {
+			// some text preceding number
+			first = label.substr(0, first_digit_pos);
+		
+		} else {
+			first = "";
+		
+		}
+		
+		unsigned int last_digit_pos = label.find_last_of(digits);
+		if (last_digit_pos < label.length()) {
+			// some text following number
+			last = label.substr(last_digit_pos + 1);
+		
+		} else {
+			last = "";
+		}	
+	} else {
+		// no digits found
+		num = -1;
+		unsigned int first_whitespace_pos = label.find_first_of(whitespace);
+		if (first_whitespace_pos != string::npos) {
+			// found some whitespace
+			if (first_whitespace_pos > 0) {
+				// some text before whitespace
+				first = label.substr(0, first_whitespace_pos);
+			} else {
+				// whitespace at start of label: what to do?
+				first = "";
+			}
+			
+			unsigned int last_whitespace_pos = label.find_last_of(whitespace);
+			if (last_whitespace_pos < label.length()) {
+				// some text following whitespace
+				last = label.substr(last_whitespace_pos + 1);
+			} else {
+				// whitespace at end of label: what to do?
+				last = "";	
+			}			
+		} else {
+			// no whitespace
+			first = label;
+			last = "";
+		}
+	
+	}
+}
+
+
+static bool possible_split(string label, string previous) {
+/* See how similar label is to previous.
+ *
+ * If they are sufficiently different, (according to a hand-wavy definition of
+ * "sufficiently"), return true to indicate that we might wish to start a new 
+ * column, or display some visual demarcation.
+ *
+ * Otherwise, return false to indicate that this label and the previous one
+ * should probably be grouped together.
+ */
+
+ 
+	int section_number, previous_section_number;
+
+	string first_word, last_word,
+	       previous_first_word, previous_last_word;
+
+	if (previous == "") 
+		return false;
+
+	split_label(label, first_word, last_word, section_number);
+	split_label(previous, previous_first_word, previous_last_word, previous_section_number);
+
+	// cerr << "label: " << label << ", num: " << section_number << endl;
+
+	if (section_number == previous_section_number) {
+		return false;
+	} else {
+		if (first_word == previous_first_word &&
+		    last_word  == previous_last_word ) {
+			return false;
+		}
+	}
+	return true;
+
+}
+
+
 void
 GenericPluginUI::build ()
-
 {
 	guint32 i = 0;
 	guint32 x = 0;
@@ -173,8 +283,10 @@
 	frame->set_label (_("Controls"));
 	frame->add (*box);
 	hpacker.pack_start(*frame, true, true);
-
+	
 	/* find all ports. build control elements for all appropriate control ports */
+	
+	string previous_label = "";
 
 	for (i = 0; i < plugin->parameter_count(); ++i) {
 
@@ -188,24 +300,7 @@
 
 			ControlUI* cui;
 	
-			/* if we are scrollable, just use one long column */
 
-			if (!is_scrollable) {
-				if (x++ > 20){
-					frame = manage (new Frame);
-					frame->set_name ("BaseFrame");
-					box = manage (new VBox);
-					
-					box->set_border_width (5);
-					box->set_spacing (1);
-
-					frame->add (*box);
-					hpacker.pack_start(*frame,true,true);
-
-					x = 1;
-				}
-			}
-
 			if ((cui = build_control_ui (i, plugin->get_nth_control (i))) == 0) {
 				error << string_compose(_("Plugin Editor: could not build control element for port %1"), i) << endmsg;
 				continue;
@@ -213,10 +308,34 @@
 				
 			if (cui->control || cui->clickbox || cui->combo) {
 
+				/* if we are scrollable, just use one long column */
+				if (!is_scrollable) {
+					x++;
+					if (possible_split(cui->label.get_text(), previous_label) || x > 20) {
+						if (x > 16) {
+							frame = manage (new Frame);
+							frame->set_name ("BaseFrame");
+							frame->set_label (_("Controls"));
+							box = manage (new VBox);
+							
+							box->set_border_width (5);
+							box->set_spacing (1);
+
+							frame->add (*box);
+							hpacker.pack_start(*frame,true,true);
+
+							x = 0;
+						} else {
+							Label *split = new Label(" ");
+							box->pack_start(*split, false, false, 0);
+						}
+					}
+				}
+
 				box->pack_start (*cui, false, false);
-
+				previous_label = cui->label.get_text();
 			} else if (cui->button) {
-
+			
 				if (button_row == button_rows) {
 					button_row = 0;
 					if (++button_col == button_cols) {
@@ -227,6 +346,7 @@
 
 				button_table.attach (*cui, button_col, button_col + 1, button_row, button_row+1, 
 						     FILL|EXPAND, FILL);
+
 				button_row++;
 
 			} else if (cui->display) {
generic-plugin-sections.patch (5,119 bytes)   

seablade

2008-12-07 01:52

manager   ~0005504

Well I certainly agree that grouping certain parameters is useful, I tend to think of this more as a limitation of LADSPA than Ardour myself, and it will be limited in how useful it can be by the default LADSPA GUI I suppose. But I love to be proven wrong as well;)

      Seablade

2009-04-12 22:57

 

4-band-parametric-before.png (36,631 bytes)   
4-band-parametric-before.png (36,631 bytes)   

2009-04-12 22:58

 

4-band-parametric-after.png (37,755 bytes)   
4-band-parametric-after.png (37,755 bytes)   

2009-04-12 22:58

 

tap-equalizer-bw-before.png (66,030 bytes)   
tap-equalizer-bw-before.png (66,030 bytes)   

2009-04-12 22:59

 

tap-equalizer-bw-after.png (52,942 bytes)   
tap-equalizer-bw-after.png (52,942 bytes)   

colinf

2009-04-12 23:07

updater   ~0005893

Well, I finally made some before-and-after screenshots.

I certainly find the 'after' layouts easier to use, and I think it goes a little way towards addressing the criticism that LADSPA plugin UIs look 'plain', but without adding unnecessary bling.

I might send a separate patch for the unnecessary bling later, if I get round to it :-)

2009-11-10 18:44

 

colinf

2009-11-10 18:45

updater   ~0007099

OK, unnecessary bling, as promised.

I know that the future is made of LV2 plugins, and very nice they look too, but just for a laugh in an idle moment the other day I rounded up a load of textural xpms I found about the place, and hacked generic_plugin_ui.cc & my ardour2_ui.rc theme file to sort-of-randomly assign them to the bg_pixmap of LADSPA plugin editor windows, according to a rudimentary "hash" of the 'creator' property of the plugin.

Attached a screenshot: ardour-plugins-aargh-my-eyes.png.

I can't decide whether this is a completely ridiculous idea or not. Thoughts, anyone?

Also, seriously, any interest in the concept of the original patch here?

thorgal

2009-11-11 14:16

reporter   ~0007101

.... mmmm, I don't know about the pink marble background ... :lol:
but the idea is fun :)

2010-04-22 13:40

 

generic-plugin-sections-v2.patch (7,109 bytes)   
Index: gtk2_ardour/generic_pluginui.cc
===================================================================
--- gtk2_ardour/generic_pluginui.cc	(revision 6960)
+++ gtk2_ardour/generic_pluginui.cc	(working copy)
@@ -121,6 +121,61 @@
 	}
 }
 
+// Some functions useful for calculation the 'similarity' of two plugin control
+// labels. Maybe there are standard library functions that do these things, but
+// I don't know what they are...
+
+static int get_number(string label) {
+static const char *digits = "0123456789";
+int value = -1;
+
+	unsigned int first_digit_pos = label.find_first_of(digits);
+	if (first_digit_pos != string::npos) {
+		// found some digits: there's a number in there somewhere
+		stringstream s;
+		s << label.substr(first_digit_pos);
+		s >> value;
+	}
+	return value;
+}
+
+
+static int match_or_digit(char c1, char c2) {
+	return c1 == c2 || (isdigit(c1) && isdigit(c2));
+}	
+
+static int matching_chars_at_head(const string s1, const string s2) {
+int n = 0;
+int length;
+
+	length = min(s1.length(), s2.length());
+	while (n < length) {
+		if (!match_or_digit(s1[n], s2[n]))
+			break;
+		n++;
+	} 
+	return n;
+}
+
+static int matching_chars_at_tail(const string s1, const string s2) {
+int n = 0;
+int s1pos, s2pos;
+
+	s1pos = s1.length();
+	s2pos = s2.length();
+	while (s1pos-- > 0 && s2pos-- > 0) {
+		if (!match_or_digit(s1[s1pos], s2[s2pos])	)
+			break;
+		n++;
+	} 
+	return n;
+}
+
+// #include <iomanip>
+
+static const guint32 min_controls_per_column = 17, max_controls_per_column = 24;
+static const float default_similarity_threshold = 0.3;
+
 void
 GenericPluginUI::build ()
 
@@ -135,6 +190,7 @@
 	int output_rows, output_cols;
 	int button_rows, button_cols;
 	guint32 n_ins=0, n_outs = 0;
+	int tallest_column = 0;	
 
 	prefheight = 30;
 	hpacker.set_spacing (10);
@@ -161,6 +217,7 @@
 
 	bt_frame = manage (new Frame);
 	bt_frame->set_name ("BaseFrame");
+	bt_frame->set_label (_("Switches"));
 	bt_frame->add (button_table);
 	hpacker.pack_start(*bt_frame, true, true);
 
@@ -175,6 +232,7 @@
 	hpacker.pack_start(*frame, true, true);
 
 	/* find all ports. build control elements for all appropriate control ports */
+	std::vector<ControlUI *> cui_controls_list;
 
 	for (i = 0; i < plugin->parameter_count(); ++i) {
 
@@ -187,34 +245,15 @@
 			}
 
 			ControlUI* cui;
-	
-			/* if we are scrollable, just use one long column */
-
-			if (!is_scrollable) {
-				if (x++ > 20){
-					frame = manage (new Frame);
-					frame->set_name ("BaseFrame");
-					box = manage (new VBox);
-					
-					box->set_border_width (5);
-					box->set_spacing (1);
-
-					frame->add (*box);
-					hpacker.pack_start(*frame,true,true);
-
-					x = 1;
-				}
-			}
-
 			if ((cui = build_control_ui (i, plugin->get_nth_control (i))) == 0) {
 				error << string_compose(_("Plugin Editor: could not build control element for port %1"), i) << endmsg;
 				continue;
 			}
 				
 			if (cui->control || cui->clickbox || cui->combo) {
-
-				box->pack_start (*cui, false, false);
-
+				// get all of the controls into a list so we can lay them out
+				// a bit more nicely
+				cui_controls_list.push_back(cui);
 			} else if (cui->button) {
 
 				if (button_row == button_rows) {
@@ -240,35 +279,99 @@
 					output_cols ++;
 					output_table.resize (output_rows, output_cols);
 				}
-				
-				/* old code, which divides meters into
-				 * columns first, rows later. New code divides into one row
-				 
-				if (output_row == output_rows) {
-					output_row = 0;
-					if (++output_col == output_cols) {
-						output_cols += 2;
-						output_table.resize (output_rows, output_cols);
+			}
+		} 
+	}
+
+	string label, previous_label = "";
+	int numbers_in_labels[cui_controls_list.size()];
+	
+	float similarity_scores[cui_controls_list.size()];
+	float most_similar = 0.0, least_similar = 1.0;
+	
+	i = 0;
+	for (vector<ControlUI*>::iterator cuip = cui_controls_list.begin(); cuip != cui_controls_list.end(); ++cuip, ++i) {
+		label = (*cuip)->label.get_text();
+		numbers_in_labels[i] = get_number(label);
+
+		if (i > 0) {
+			// a hand-wavy calculation of how similar this control's
+			// label is to the previous.
+			similarity_scores[i] = 
+				(float) ( 
+					( matching_chars_at_head(label, previous_label) + 
+					  matching_chars_at_tail(label, previous_label) +
+					  1 
+					) 
+				) / (label.length() + previous_label.length());
+			if (numbers_in_labels[i] >= 0) {
+				similarity_scores[i] += (numbers_in_labels[i] == numbers_in_labels[i-1]);
+			}
+			least_similar = min(least_similar, similarity_scores[i]);
+			most_similar  = max(most_similar, similarity_scores[i]);
+		} else {
+			similarity_scores[0] = 1.0;
+		}
+
+		// cerr << "label: " << label << " sim: " << fixed << setprecision(3) << similarity_scores[i] << " num: " << numbers_in_labels[i] << endl;
+		previous_label = label;		                       
+	}
+
+	
+	// cerr << "most similar: " << most_similar << ", least similar: " << least_similar << endl;
+	float similarity_threshold;
+	
+	if (most_similar > 1.0) {
+		similarity_threshold = default_similarity_threshold;
+	} else {
+		similarity_threshold = most_similar - (1 - default_similarity_threshold);
+	}
+	
+
+	i = 0;
+	for (vector<ControlUI*>::iterator cuip = cui_controls_list.begin(); cuip != cui_controls_list.end(); ++cuip, ++i) {
+
+		ControlUI* cui = *cuip;
+		/* if we are scrollable, just use one long column */
+		if (!is_scrollable) {
+			x++;
+			if (x > max_controls_per_column || similarity_scores[i] <= similarity_threshold) {
+				if (x > min_controls_per_column) {
+					frame = manage (new Frame);
+					frame->set_name ("BaseFrame");
+					frame->set_label (_("Controls"));
+					box = manage (new VBox);
+					
+					box->set_border_width (5);
+					box->set_spacing (1);
+
+					frame->add (*box);
+					hpacker.pack_start(*frame,true,true);
+					
+					if (tallest_column < x) {
+						tallest_column = x;
 					}
+					x = 0;
+				} else {
+					HSeparator *split = new HSeparator();
+					split->set_size_request(-1, 5);
+					box->pack_start(*split, false, false, 0);
 				}
-				
-				output_table.attach (*cui, output_col, output_col + 1, output_row, output_row+1, 
-						     FILL|EXPAND, FILL);
- 
-				output_row++;
-				*/
 			}
-				
-			/* HACK: ideally the preferred height would be queried from
-			   the complete hpacker, but I can't seem to get that
-			   information in time, so this is an estimation 
-			*/
+		}
 
-			prefheight += 30;
+		box->pack_start (*cui, false, false);
+		previous_label = cui->label.get_text();
 
-		} 
 	}
+	
+	/* HACK: ideally the preferred height would be queried from
+	   the complete hpacker, but I can't seem to get that
+	   information in time, so this is an estimation 
+	*/
 
+	prefheight = (tallest_column + 1) * 20;
+
 	n_ins = plugin->get_info()->n_inputs;
 	n_outs = plugin->get_info()->n_outputs;
 
@@ -283,6 +386,7 @@
 	if (!output_table.children().empty()) {
 		frame = manage (new Frame);
 		frame->set_name ("BaseFrame");
+		frame->set_label(_("Meters"));
 		frame->add (output_table);
 		hpacker.pack_end (*frame, true, true);
 	}

colinf

2010-04-22 13:43

updater   ~0007517

Well, still pretty much a hack, but I tidied up my original patch a bit, so I thought I might as well upload a newer version here, just in case anyone's interested.

seablade

2010-04-22 14:47

manager   ~0007518

Assigning to Paul to take a look at the patch.

    Seablade

2011-05-26 16:41

 

generic-plugin-sections-a3.patch (6,786 bytes)   
Index: gtk2_ardour/generic_pluginui.cc
===================================================================
--- gtk2_ardour/generic_pluginui.cc	(revision 9600)
+++ gtk2_ardour/generic_pluginui.cc	(working copy)
@@ -138,6 +138,57 @@
 	}
 }
 
+// Some functions for calculating the 'similarity' of two plugin
+// control labels.
+
+static int get_number(string label) {
+static const char *digits = "0123456789";
+int value = -1;
+
+	unsigned int first_digit_pos = label.find_first_of(digits);
+	if (first_digit_pos != string::npos) {
+		// found some digits: there's a number in there somewhere
+		stringstream s;
+		s << label.substr(first_digit_pos);
+		s >> value;
+	}
+	return value;
+}
+
+static int match_or_digit(char c1, char c2) {
+	return c1 == c2 || (isdigit(c1) && isdigit(c2));
+}	
+
+static int matching_chars_at_head(const string s1, const string s2) {
+int n = 0;
+int length;
+
+	length = min(s1.length(), s2.length());
+	while (n < length) {
+		if (!match_or_digit(s1[n], s2[n]))
+			break;
+		n++;
+	} 
+	return n;
+}
+
+static int matching_chars_at_tail(const string s1, const string s2) {
+int n = 0;
+int s1pos, s2pos;
+
+	s1pos = s1.length();
+	s2pos = s2.length();
+	while (s1pos-- > 0 && s2pos-- > 0) {
+		if (!match_or_digit(s1[s1pos], s2[s2pos])	)
+			break;
+		n++;
+	} 
+	return n;
+}
+
+static const guint32 min_controls_per_column = 17, max_controls_per_column = 24;
+static const float default_similarity_threshold = 0.3;
+
 void
 GenericPluginUI::build ()
 {
@@ -151,7 +202,6 @@
 	int output_rows, output_cols;
 	int button_rows, button_cols;
 
-	prefheight = 30;
 	hpacker.set_spacing (10);
 
 	output_rows = initial_output_rows;
@@ -176,6 +226,7 @@
 
 	bt_frame = manage (new Frame);
 	bt_frame->set_name ("BaseFrame");
+	bt_frame->set_label (_("Switches"));
 	bt_frame->add (button_table);
 	hpacker.pack_start(*bt_frame, true, true);
 
@@ -190,6 +241,7 @@
 	hpacker.pack_start(*frame, true, true);
 
 	/* find all ports. build control elements for all appropriate control ports */
+	std::vector<ControlUI *> cui_controls_list;
 
 	for (i = 0; i < plugin->parameter_count(); ++i) {
 
@@ -203,24 +255,6 @@
 
 			ControlUI* cui;
 
-			/* if we are scrollable, just use one long column */
-
-			if (!is_scrollable) {
-				if (x++ > 20){
-					frame = manage (new Frame);
-					frame->set_name ("BaseFrame");
-					box = manage (new VBox);
-
-					box->set_border_width (5);
-					box->set_spacing (1);
-
-					frame->add (*box);
-					hpacker.pack_start(*frame, true, true);
-
-					x = 1;
-				}
-			}
-
 			boost::shared_ptr<ARDOUR::AutomationControl> c
 				= boost::dynamic_pointer_cast<ARDOUR::AutomationControl>(
 					insert->control(Evoral::Parameter(PluginAutomation, 0, i)));
@@ -231,9 +265,9 @@
 			}
 
 			if (cui->controller || cui->clickbox || cui->combo) {
-
-				box->pack_start (*cui, false, false);
-
+				// Get all of the controls into a list, so that
+				// we can lay them out a bit more nicely later.
+				cui_controls_list.push_back(cui);
 			} else if (cui->button) {
 
 				if (button_row == button_rows) {
@@ -259,33 +293,89 @@
 					output_cols ++;
 					output_table.resize (output_rows, output_cols);
 				}
+			}
+		} 
+	}
 
-				/* old code, which divides meters into
-				 * columns first, rows later. New code divides into one row
+	// Iterate over the list of controls to find which adjacent controls
+	// are similar enough to be grouped together.
+	
+	string label, previous_label = "";
+	int numbers_in_labels[cui_controls_list.size()];
+	
+	float similarity_scores[cui_controls_list.size()];
+	float most_similar = 0.0, least_similar = 1.0;
+	
+	i = 0;
+	for (vector<ControlUI*>::iterator cuip = cui_controls_list.begin(); cuip != cui_controls_list.end(); ++cuip, ++i) {
+		label = (*cuip)->label.get_text();
+		numbers_in_labels[i] = get_number(label);
 
-				if (output_row == output_rows) {
-					output_row = 0;
-					if (++output_col == output_cols) {
-						output_cols += 2;
-						output_table.resize (output_rows, output_cols);
-					}
-				}
-
-				output_table.attach (*cui, output_col, output_col + 1, output_row, output_row+1,
-						     FILL|EXPAND, FILL);
-
-				output_row++;
-				*/
+		if (i > 0) {
+			// A hand-wavy calculation of how similar this control's
+			// label is to the previous.
+			similarity_scores[i] = 
+				(float) ( 
+					( matching_chars_at_head(label, previous_label) + 
+					  matching_chars_at_tail(label, previous_label) +
+					  1 
+					) 
+				) / (label.length() + previous_label.length());
+			if (numbers_in_labels[i] >= 0) {
+				similarity_scores[i] += (numbers_in_labels[i] == numbers_in_labels[i-1]);
 			}
+			least_similar = min(least_similar, similarity_scores[i]);
+			most_similar  = max(most_similar, similarity_scores[i]);
+		} else {
+			similarity_scores[0] = 1.0;
+		}
 
-			/* HACK: ideally the preferred height would be queried from
-			   the complete hpacker, but I can't seem to get that
-			   information in time, so this is an estimation
-			*/
+		// cerr << "label: " << label << " sim: " << fixed << setprecision(3) << similarity_scores[i] << " num: " << numbers_in_labels[i] << endl;
+		previous_label = label;		                       
+	}
 
-			prefheight += 30;
+	
+	// cerr << "most similar: " << most_similar << ", least similar: " << least_similar << endl;
+	float similarity_threshold;
+	
+	if (most_similar > 1.0) {
+		similarity_threshold = default_similarity_threshold;
+	} else {
+		similarity_threshold = most_similar - (1 - default_similarity_threshold);
+	}
+	
+	// Now iterate over the list of controls to display them, placing an
+	// HSeparator between controls of less than a certain similarity, and 
+	// starting a new column when necessary.
+	
+	i = 0;
+	for (vector<ControlUI*>::iterator cuip = cui_controls_list.begin(); cuip != cui_controls_list.end(); ++cuip, ++i) {
 
+		ControlUI* cui = *cuip;
+		
+		if (!is_scrollable) {
+			x++;
 		}
+		
+		if (x > max_controls_per_column || similarity_scores[i] <= similarity_threshold) {
+			if (x > min_controls_per_column) {
+				frame = manage (new Frame);
+				frame->set_name ("BaseFrame");
+				frame->set_label (_("Controls"));
+				box = manage (new VBox);
+				box->set_border_width (5);
+				box->set_spacing (1);
+				frame->add (*box);
+				hpacker.pack_start(*frame,true,true);
+				x = 0;
+			} else {
+				HSeparator *split = new HSeparator();
+				split->set_size_request(-1, 5);
+				box->pack_start(*split, false, false, 0);
+			}
+
+		}
+		box->pack_start (*cui, false, false);
 	}
 
 	if (box->children().empty()) {
@@ -299,6 +389,7 @@
 	if (!output_table.children().empty()) {
 		frame = manage (new Frame);
 		frame->set_name ("BaseFrame");
+		frame->set_label(_("Meters"));
 		frame->add (output_table);
 		hpacker.pack_end (*frame, true, true);
 	}

colinf

2011-05-26 16:42

updater   ~0010805

Well, I've got thoroughly used to the controls of my LADSPA plugins grouping themselves with this patch applied to A2, and I was missing it in A3, so I spent a few idle moments porting the patch to A3.

Still just as much of a hack as it ever was, but just in case anyone else is interested, here it is...

I'll very happily take any suggestions of how to make it better, too.

paul

2011-07-08 19:01

administrator   ~0011002

colin - patch is being applied as i write. lets talk about the xpms. i like the brushed/illuminated ones, the others i think interfere with either readability or theme consistency.

paul

2011-07-08 19:35

administrator   ~0011003

the patch crashes because of the use of int rather than string::size_t in the static helper functions.

it also seems to blow up the vertical size of the plugin editor windows.

fix these two and i'll be happy to commit (a3)

colinf

2011-07-11 10:57

updater   ~0011059

Thanks very much for looking at this, Paul.

I've only tested on 32 bit systems, so there may well be some int/size_t oddities on 64 bits. Hopefully fixed now.

The vertical size blow-up was because I somehow completely removed the assignment to prefheight, leaving it undefined. It didn't always blow up the vertical size: sometimes it worked properly, and just occasionally it crashed.

I think that initialising prefheight to 0 in the GenericPluginUI constructor is probably the right thing to do: it only needs to be set to something more if (is_scrollable) is set.

2011-07-11 11:15

 

generic-plugin-sections-a3-v2.patch (7,143 bytes)   
Index: gtk2_ardour/generic_pluginui.cc
===================================================================
--- gtk2_ardour/generic_pluginui.cc	(revision 9834)
+++ gtk2_ardour/generic_pluginui.cc	(working copy)
@@ -128,6 +128,7 @@
 
 	bypass_button.set_active (!pi->active());
 
+	prefheight = 0;
 	build ();
 }
 
@@ -138,6 +139,55 @@
 	}
 }
 
+// Some functions for calculating the 'similarity' of two plugin
+// control labels.
+
+static int get_number(string label) {
+static const char *digits = "0123456789";
+int value = -1;
+
+	std::size_t first_digit_pos = label.find_first_of(digits);
+	if (first_digit_pos != string::npos) {
+		// found some digits: there's a number in there somewhere
+		stringstream s;
+		s << label.substr(first_digit_pos);
+		s >> value;
+	}
+	return value;
+}
+
+static int match_or_digit(char c1, char c2) {
+	return c1 == c2 || (isdigit(c1) && isdigit(c2));
+}	
+
+static std::size_t matching_chars_at_head(const string s1, const string s2) {
+std::size_t length, n = 0;
+
+	length = min(s1.length(), s2.length());
+	while (n < length) {
+		if (!match_or_digit(s1[n], s2[n]))
+			break;
+		n++;
+	} 
+	return n;
+}
+
+static std::size_t matching_chars_at_tail(const string s1, const string s2) {
+std::size_t s1pos, s2pos, n = 0;
+
+	s1pos = s1.length();
+	s2pos = s2.length();
+	while (s1pos-- > 0 && s2pos-- > 0) {
+		if (!match_or_digit(s1[s1pos], s2[s2pos])	)
+			break;
+		n++;
+	} 
+	return n;
+}
+
+static const guint32 min_controls_per_column = 17, max_controls_per_column = 24;
+static const float default_similarity_threshold = 0.3;
+
 void
 GenericPluginUI::build ()
 {
@@ -151,7 +201,6 @@
 	int output_rows, output_cols;
 	int button_rows, button_cols;
 
-	prefheight = 30;
 	hpacker.set_spacing (10);
 
 	output_rows = initial_output_rows;
@@ -176,6 +225,7 @@
 
 	bt_frame = manage (new Frame);
 	bt_frame->set_name ("BaseFrame");
+	bt_frame->set_label (_("Switches"));
 	bt_frame->add (button_table);
 	hpacker.pack_start(*bt_frame, true, true);
 
@@ -190,6 +240,7 @@
 	hpacker.pack_start(*frame, true, true);
 
 	/* find all ports. build control elements for all appropriate control ports */
+	std::vector<ControlUI *> cui_controls_list;
 
 	for (i = 0; i < plugin->parameter_count(); ++i) {
 
@@ -203,24 +254,6 @@
 
 			ControlUI* cui;
 
-			/* if we are scrollable, just use one long column */
-
-			if (!is_scrollable) {
-				if (x++ > 20){
-					frame = manage (new Frame);
-					frame->set_name ("BaseFrame");
-					box = manage (new VBox);
-
-					box->set_border_width (5);
-					box->set_spacing (1);
-
-					frame->add (*box);
-					hpacker.pack_start(*frame, true, true);
-
-					x = 1;
-				}
-			}
-
 			boost::shared_ptr<ARDOUR::AutomationControl> c
 				= boost::dynamic_pointer_cast<ARDOUR::AutomationControl>(
 					insert->control(Evoral::Parameter(PluginAutomation, 0, i)));
@@ -231,12 +264,12 @@
 			}
 
 			if (cui->controller || cui->clickbox || cui->combo) {
-
-				box->pack_start (*cui, false, false);
-
+				// Get all of the controls into a list, so that
+				// we can lay them out a bit more nicely later.
+				cui_controls_list.push_back(cui);
 			} else if (cui->button) {
 
-				if (button_row == button_rows) {
+				if (!is_scrollable && button_row == button_rows) {
 					button_row = 0;
 					if (++button_col == button_cols) {
 						button_cols += 2;
@@ -259,35 +292,95 @@
 					output_cols ++;
 					output_table.resize (output_rows, output_cols);
 				}
+			}
+		} 
+	}
 
-				/* old code, which divides meters into
-				 * columns first, rows later. New code divides into one row
+	// Iterate over the list of controls to find which adjacent controls
+	// are similar enough to be grouped together.
+	
+	string label, previous_label = "";
+	int numbers_in_labels[cui_controls_list.size()];
+	
+	float similarity_scores[cui_controls_list.size()];
+	float most_similar = 0.0, least_similar = 1.0;
+	
+	i = 0;
+	for (vector<ControlUI*>::iterator cuip = cui_controls_list.begin(); cuip != cui_controls_list.end(); ++cuip, ++i) {
+		label = (*cuip)->label.get_text();
+		numbers_in_labels[i] = get_number(label);
 
-				if (output_row == output_rows) {
-					output_row = 0;
-					if (++output_col == output_cols) {
-						output_cols += 2;
-						output_table.resize (output_rows, output_cols);
-					}
-				}
-
-				output_table.attach (*cui, output_col, output_col + 1, output_row, output_row+1,
-						     FILL|EXPAND, FILL);
-
-				output_row++;
-				*/
+		if (i > 0) {
+			// A hand-wavy calculation of how similar this control's
+			// label is to the previous.
+			similarity_scores[i] = 
+				(float) ( 
+					( matching_chars_at_head(label, previous_label) + 
+					  matching_chars_at_tail(label, previous_label) +
+					  1 
+					) 
+				) / (label.length() + previous_label.length());
+			if (numbers_in_labels[i] >= 0) {
+				similarity_scores[i] += (numbers_in_labels[i] == numbers_in_labels[i-1]);
 			}
+			least_similar = min(least_similar, similarity_scores[i]);
+			most_similar  = max(most_similar, similarity_scores[i]);
+		} else {
+			similarity_scores[0] = 1.0;
+		}
 
-			/* HACK: ideally the preferred height would be queried from
-			   the complete hpacker, but I can't seem to get that
-			   information in time, so this is an estimation
-			*/
+		// cerr << "label: " << label << " sim: " << fixed << setprecision(3) << similarity_scores[i] << " num: " << numbers_in_labels[i] << endl;
+		previous_label = label;		                       
+	}
 
-			prefheight += 30;
+	
+	// cerr << "most similar: " << most_similar << ", least similar: " << least_similar << endl;
+	float similarity_threshold;
+	
+	if (most_similar > 1.0) {
+		similarity_threshold = default_similarity_threshold;
+	} else {
+		similarity_threshold = most_similar - (1 - default_similarity_threshold);
+	}
+	
+	// Now iterate over the list of controls to display them, placing an
+	// HSeparator between controls of less than a certain similarity, and 
+	// starting a new column when necessary.
+	
+	i = 0;
+	for (vector<ControlUI*>::iterator cuip = cui_controls_list.begin(); cuip != cui_controls_list.end(); ++cuip, ++i) {
 
+		ControlUI* cui = *cuip;
+		
+		if (!is_scrollable) {
+			x++;
 		}
+		
+		if (x > max_controls_per_column || similarity_scores[i] <= similarity_threshold) {
+			if (x > min_controls_per_column) {
+				frame = manage (new Frame);
+				frame->set_name ("BaseFrame");
+				frame->set_label (_("Controls"));
+				box = manage (new VBox);
+				box->set_border_width (5);
+				box->set_spacing (1);
+				frame->add (*box);
+				hpacker.pack_start(*frame, true, true);
+				x = 0;
+			} else {
+				HSeparator *split = new HSeparator();
+				split->set_size_request(-1, 5);
+				box->pack_start(*split, false, false, 0);
+			}
+
+		}
+		box->pack_start (*cui, false, false);
 	}
 
+	if (is_scrollable) {
+		prefheight = 30 * i;
+	}
+
 	if (box->children().empty()) {
 		hpacker.remove (*frame);
 	}
@@ -299,6 +392,7 @@
 	if (!output_table.children().empty()) {
 		frame = manage (new Frame);
 		frame->set_name ("BaseFrame");
+		frame->set_label(_("Meters"));
 		frame->add (output_table);
 		hpacker.pack_end (*frame, true, true);
 	}

colinf

2011-07-11 12:14

updater   ~0011063

As for the XPM's, I didn't expect anyone to take that idea seriously. I don't think I even have the patch any more; I do remember it was a sufficiently horrid hack that I was ashamed to even post it here...

I think the images I used mostly came from Nick Copeland's Bristol synth, but I found a few others around too: I know the pink marble was from the JAMin sources.

paul

2011-07-11 12:34

administrator   ~0011064

committed as rev 9835. thanks!

colinf

2012-05-23 16:45

updater   ~0013282

Closing this long-resolved issue.

Issue History

Date Modified Username Field Change
2008-12-05 14:19 colinf New Issue
2008-12-05 14:19 colinf File Added: generic-plugin-sections.patch
2008-12-07 01:52 seablade Note Added: 0005504
2008-12-07 01:52 seablade Status new => acknowledged
2009-04-12 22:57 colinf File Added: 4-band-parametric-before.png
2009-04-12 22:58 colinf File Added: 4-band-parametric-after.png
2009-04-12 22:58 colinf File Added: tap-equalizer-bw-before.png
2009-04-12 22:59 colinf File Added: tap-equalizer-bw-after.png
2009-04-12 23:07 colinf Note Added: 0005893
2009-11-10 18:44 colinf File Added: ardour-plugins-aargh-my-eyes.png
2009-11-10 18:45 colinf Note Added: 0007099
2009-11-11 14:16 thorgal Note Added: 0007101
2010-04-22 13:40 colinf File Added: generic-plugin-sections-v2.patch
2010-04-22 13:43 colinf Note Added: 0007517
2010-04-22 14:47 seablade Note Added: 0007518
2010-04-22 14:47 seablade Status acknowledged => assigned
2010-04-22 14:47 seablade Assigned To => paul
2011-05-26 16:41 colinf File Added: generic-plugin-sections-a3.patch
2011-05-26 16:42 colinf Note Added: 0010805
2011-05-26 16:50 cth103 cost => 0.00
2011-05-26 16:50 cth103 Target Version => 3.0-beta1
2011-07-08 19:01 paul Note Added: 0011002
2011-07-08 19:35 paul Note Added: 0011003
2011-07-11 10:57 colinf Note Added: 0011059
2011-07-11 11:15 colinf File Added: generic-plugin-sections-a3-v2.patch
2011-07-11 12:14 colinf Note Added: 0011063
2011-07-11 12:34 paul Note Added: 0011064
2011-07-11 12:34 paul Status assigned => resolved
2011-07-11 12:34 paul Resolution open => fixed
2012-05-23 16:45 colinf Note Added: 0013282
2012-05-23 16:45 colinf Status resolved => closed