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

Annotation of /gigedit/trunk/src/gigedit/CombineInstrumentsDialog.cpp

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2558 - (hide annotations) (download)
Sat May 17 23:55:30 2014 UTC (9 years, 10 months ago) by schoenebeck
File size: 43583 byte(s)
* Combine instruments: generalized this tool to allow combining instruments
  based on any dimension (selectable by combo box), not only the 'layer'
  dimension. Also fixed some bugs that could lead to undesired results
  when combining instruments, i.e. source instruments being copied into
  wrong target zones or in wrong order under certain conditions.
* 'Dimension Manager' dialog: show dialog window with at least 460x300px on
  initial usage (was far too small by default).

1 schoenebeck 2548 /*
2     Copyright (c) 2014 Christian Schoenebeck
3    
4     This file is part of "gigedit" and released under the terms of the
5     GNU General Public License version 2.
6     */
7    
8     #include "CombineInstrumentsDialog.h"
9    
10     // enable this for debug messages being printed while combining the instruments
11     #define DEBUG_COMBINE_INSTRUMENTS 0
12    
13     #include "global.h"
14 schoenebeck 2558 #include "compat.h"
15 schoenebeck 2548
16     #include <set>
17     #include <iostream>
18     #include <assert.h>
19 schoenebeck 2552 #include <stdarg.h>
20     #include <string.h>
21 schoenebeck 2548
22     #include <glibmm/ustring.h>
23     #include <gtkmm/stock.h>
24     #include <gtkmm/messagedialog.h>
25 schoenebeck 2558 #include <gtkmm/label.h>
26 schoenebeck 2548
27     Glib::ustring gig_to_utf8(const gig::String& gig_string);
28 schoenebeck 2558 Glib::ustring dimTypeAsString(gig::dimension_t d);
29 schoenebeck 2548
30 schoenebeck 2558 typedef std::vector< std::pair<gig::Instrument*, gig::Region*> > OrderedRegionGroup;
31 schoenebeck 2548 typedef std::map<gig::Instrument*, gig::Region*> RegionGroup;
32     typedef std::map<DLS::range_t,RegionGroup> RegionGroups;
33    
34     typedef std::vector<DLS::range_t> DimensionZones;
35     typedef std::map<gig::dimension_t,DimensionZones> Dimensions;
36    
37     typedef std::map<gig::dimension_t,int> DimensionCase;
38    
39     typedef std::map<gig::dimension_t, int> DimensionRegionUpperLimits;
40    
41 schoenebeck 2552 typedef std::set<Glib::ustring> Warnings;
42    
43 schoenebeck 2548 ///////////////////////////////////////////////////////////////////////////
44 schoenebeck 2552 // private static data
45    
46     static Warnings g_warnings;
47    
48     ///////////////////////////////////////////////////////////////////////////
49 schoenebeck 2548 // private functions
50    
51     #if DEBUG_COMBINE_INSTRUMENTS
52     static void printRanges(const RegionGroups& regions) {
53     std::cout << "{ ";
54     for (RegionGroups::const_iterator it = regions.begin(); it != regions.end(); ++it) {
55     if (it != regions.begin()) std::cout << ", ";
56     std::cout << (int)it->first.low << ".." << (int)it->first.high;
57     }
58     std::cout << " }" << std::flush;
59     }
60     #endif
61    
62     /**
63 schoenebeck 2552 * Store a warning message that shall be stored and displayed to the user as a
64     * list of warnings after the overall operation has finished. Duplicate warning
65     * messages are automatically eliminated.
66     */
67     inline void addWarning(const char* fmt, ...) {
68     va_list arg;
69     va_start(arg, fmt);
70     const int SZ = 255 + strlen(fmt);
71     char* buf = new char[SZ];
72     vsnprintf(buf, SZ, fmt, arg);
73     Glib::ustring s = buf;
74     delete [] buf;
75     va_end(arg);
76     std::cerr << _("WARNING:") << " " << s << std::endl << std::flush;
77     g_warnings.insert(s);
78     }
79    
80     /**
81 schoenebeck 2548 * If the two ranges overlap, then this function returns the smallest point
82     * within that overlapping zone. If the two ranges do not overlap, then this
83     * function will return -1 instead.
84     */
85     inline int smallestOverlapPoint(const DLS::range_t& r1, const DLS::range_t& r2) {
86     if (r1.overlaps(r2.low)) return r2.low;
87     if (r2.overlaps(r1.low)) return r1.low;
88     return -1;
89     }
90    
91     /**
92     * Get the most smallest region point (not necessarily its region start point)
93     * of all regions of the given instruments, start searching at keyboard
94     * position @a iStart.
95     *
96     * @returns very first region point >= iStart, or -1 if no region could be
97     * found with a range member point >= iStart
98     */
99     static int findLowestRegionPoint(std::vector<gig::Instrument*>& instruments, int iStart) {
100     DLS::range_t searchRange = { iStart, 127 };
101     int result = -1;
102     for (uint i = 0; i < instruments.size(); ++i) {
103     gig::Instrument* instr = instruments[i];
104     for (gig::Region* rgn = instr->GetFirstRegion(); rgn; rgn = instr->GetNextRegion()) {
105     if (rgn->KeyRange.overlaps(searchRange)) {
106     int lowest = smallestOverlapPoint(rgn->KeyRange, searchRange);
107     if (result == -1 || lowest < result) result = lowest;
108     }
109     }
110     }
111     return result;
112     }
113    
114     /**
115     * Get the most smallest region end of all regions of the given instruments,
116     * start searching at keyboard position @a iStart.
117     *
118     * @returns very first region end >= iStart, or -1 if no region could be found
119     * with a range end >= iStart
120     */
121     static int findFirstRegionEnd(std::vector<gig::Instrument*>& instruments, int iStart) {
122     DLS::range_t searchRange = { iStart, 127 };
123     int result = -1;
124     for (uint i = 0; i < instruments.size(); ++i) {
125     gig::Instrument* instr = instruments[i];
126     for (gig::Region* rgn = instr->GetFirstRegion(); rgn; rgn = instr->GetNextRegion()) {
127     if (rgn->KeyRange.overlaps(searchRange)) {
128     if (result == -1 || rgn->KeyRange.high < result)
129     result = rgn->KeyRange.high;
130     }
131     }
132     }
133     return result;
134     }
135    
136     /**
137     * Returns a list of all regions of the given @a instrument where the respective
138     * region's key range overlaps the given @a range.
139     */
140     static std::vector<gig::Region*> getAllRegionsWhichOverlapRange(gig::Instrument* instrument, DLS::range_t range) {
141     //std::cout << "All regions which overlap { " << (int)range.low << ".." << (int)range.high << " } : " << std::flush;
142     std::vector<gig::Region*> v;
143     for (gig::Region* rgn = instrument->GetFirstRegion(); rgn; rgn = instrument->GetNextRegion()) {
144     if (rgn->KeyRange.overlaps(range)) {
145     v.push_back(rgn);
146     //std::cout << (int)rgn->KeyRange.low << ".." << (int)rgn->KeyRange.high << ", " << std::flush;
147     }
148     }
149     //std::cout << " END." << std::endl;
150     return v;
151     }
152    
153     /**
154     * Returns all regions of the given @a instruments where the respective region's
155     * key range overlaps the given @a range. The regions returned are ordered (in a
156     * map) by their instrument pointer.
157     */
158     static RegionGroup getAllRegionsWhichOverlapRange(std::vector<gig::Instrument*>& instruments, DLS::range_t range) {
159     RegionGroup group;
160     for (uint i = 0; i < instruments.size(); ++i) {
161     gig::Instrument* instr = instruments[i];
162     std::vector<gig::Region*> v = getAllRegionsWhichOverlapRange(instr, range);
163     if (v.empty()) continue;
164     if (v.size() > 1) {
165 schoenebeck 2552 addWarning("More than one region found!");
166 schoenebeck 2548 }
167     group[instr] = v[0];
168     }
169     return group;
170     }
171    
172     /** @brief Identify required regions.
173     *
174     * Takes a list of @a instruments as argument (which are planned to be combined
175 schoenebeck 2558 * as separate dimension zones of a certain dimension into one single new
176     * instrument) and fulfills the following tasks:
177 schoenebeck 2548 *
178     * - 1. Identification of total amount of regions required to create a new
179 schoenebeck 2558 * instrument to become a combined version of the given instruments.
180 schoenebeck 2548 * - 2. Precise key range of each of those identified required regions to be
181     * created in that new instrument.
182     * - 3. Grouping the original source regions of the given original instruments
183     * to the respective target key range (new region) of the instrument to be
184     * created.
185     *
186     * @param instruments - list of instruments that are planned to be combined
187     * @returns structured result of the tasks described above
188     */
189     static RegionGroups groupByRegionIntersections(std::vector<gig::Instrument*>& instruments) {
190     RegionGroups groups;
191    
192     // find all region intersections of all instruments
193     std::vector<DLS::range_t> intersections;
194     for (int iStart = 0; iStart <= 127; ) {
195     iStart = findLowestRegionPoint(instruments, iStart);
196     if (iStart < 0) break;
197     const int iEnd = findFirstRegionEnd(instruments, iStart);
198     DLS::range_t range = { iStart, iEnd };
199     intersections.push_back(range);
200     iStart = iEnd + 1;
201     }
202    
203     // now sort all regions to those found intersections
204     for (uint i = 0; i < intersections.size(); ++i) {
205     const DLS::range_t& range = intersections[i];
206     RegionGroup group = getAllRegionsWhichOverlapRange(instruments, range);
207     if (!group.empty())
208     groups[range] = group;
209     else
210 schoenebeck 2552 addWarning("Empty region group!");
211 schoenebeck 2548 }
212    
213     return groups;
214     }
215    
216     /** @brief Identify required dimensions.
217     *
218     * Takes a planned new region (@a regionGroup) as argument and identifies which
219     * precise dimensions would have to be created for that new region, along with
220     * the amount of dimension zones and their precise individual zone sizes.
221     *
222     * @param regionGroup - planned new region for a new instrument
223     * @returns set of dimensions that shall be created for the given planned region
224     */
225     static Dimensions getDimensionsForRegionGroup(RegionGroup& regionGroup) {
226     std::map<gig::dimension_t, std::set<int> > dimUpperLimits;
227    
228     // collect all dimension region zones' upper limits
229     for (RegionGroup::iterator it = regionGroup.begin();
230     it != regionGroup.end(); ++it)
231     {
232     gig::Region* rgn = it->second;
233     int previousBits = 0;
234     for (uint d = 0; d < rgn->Dimensions; ++d) {
235     const gig::dimension_def_t& def = rgn->pDimensionDefinitions[d];
236     for (uint z = 0; z < def.zones; ++z) {
237     int dr = z << previousBits;
238     gig::DimensionRegion* dimRgn = rgn->pDimensionRegions[dr];
239     // Store the individual dimension zone sizes (or actually their
240     // upper limits here) for each dimension.
241     // HACK: Note that the velocity dimension is specially handled
242     // here. Instead of taking over custom velocity split sizes
243     // here, only a bogus number (zone index number) is stored for
244     // each velocity zone, that way only the maxiumum amount of
245     // velocity splits of all regions is stored here, and when their
246     // individual DimensionRegions are finally copied (later), the
247     // individual velocity split size are copied by that.
248     dimUpperLimits[def.dimension].insert(
249     (def.dimension == gig::dimension_velocity) ?
250     z : (def.split_type == gig::split_type_bit) ?
251     ((z+1) * 128/def.zones - 1) : dimRgn->DimensionUpperLimits[dr]
252     );
253     }
254     previousBits += def.bits;
255     }
256     }
257    
258     // convert upper limit set to range vector
259     Dimensions dims;
260     for (std::map<gig::dimension_t, std::set<int> >::const_iterator it = dimUpperLimits.begin();
261     it != dimUpperLimits.end(); ++it)
262     {
263     gig::dimension_t type = it->first;
264     int iLow = 0;
265     for (std::set<int>::const_iterator itNums = it->second.begin();
266     itNums != it->second.end(); ++itNums)
267     {
268     const int iUpperLimit = *itNums;
269     DLS::range_t range = { iLow, iUpperLimit };
270     dims[type].push_back(range);
271     iLow = iUpperLimit + 1;
272     }
273     }
274    
275     return dims;
276     }
277    
278     inline int getDimensionIndex(gig::dimension_t type, gig::Region* rgn) {
279     for (uint i = 0; i < rgn->Dimensions; ++i)
280     if (rgn->pDimensionDefinitions[i].dimension == type)
281     return i;
282     return -1;
283     }
284    
285     static void fillDimValues(uint* values/*[8]*/, DimensionCase dimCase, gig::Region* rgn, bool bShouldHaveAllDimensionsPassed) {
286 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
287     printf("dimvalues = { ");
288     fflush(stdout);
289     #endif
290 schoenebeck 2548 for (DimensionCase::iterator it = dimCase.begin(); it != dimCase.end(); ++it) {
291     gig::dimension_t type = it->first;
292     int iDimIndex = getDimensionIndex(type, rgn);
293     if (bShouldHaveAllDimensionsPassed) assert(iDimIndex >= 0);
294     else if (iDimIndex < 0) continue;
295     values[iDimIndex] = it->second;
296 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
297     printf("%x=%d, ", type, it->second);
298     #endif
299 schoenebeck 2548 }
300 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
301 schoenebeck 2558 printf("}\n");
302 schoenebeck 2550 #endif
303 schoenebeck 2548 }
304    
305     static DimensionRegionUpperLimits getDimensionRegionUpperLimits(gig::DimensionRegion* dimRgn) {
306     DimensionRegionUpperLimits limits;
307     gig::Region* rgn = dimRgn->GetParent();
308 schoenebeck 2549 for (uint d = 0; d < rgn->Dimensions; ++d) {
309 schoenebeck 2548 const gig::dimension_def_t& def = rgn->pDimensionDefinitions[d];
310     limits[def.dimension] = dimRgn->DimensionUpperLimits[d];
311     }
312     return limits;
313     }
314    
315     static void restoreDimensionRegionUpperLimits(gig::DimensionRegion* dimRgn, const DimensionRegionUpperLimits& limits) {
316     gig::Region* rgn = dimRgn->GetParent();
317     for (DimensionRegionUpperLimits::const_iterator it = limits.begin();
318     it != limits.end(); ++it)
319     {
320     int index = getDimensionIndex(it->first, rgn);
321     assert(index >= 0);
322     dimRgn->DimensionUpperLimits[index] = it->second;
323     }
324     }
325    
326     /**
327     * Returns the sum of all bits of all dimensions defined before the given
328     * dimensions (@a type). This allows to access cases of that particular
329     * dimension directly.
330     *
331     * @param type - dimension that shall be used
332     * @param rgn - parent region of that dimension
333     */
334     inline int baseBits(gig::dimension_t type, gig::Region* rgn) {
335     int previousBits = 0;
336 schoenebeck 2549 for (uint i = 0; i < rgn->Dimensions; ++i) {
337 schoenebeck 2548 if (rgn->pDimensionDefinitions[i].dimension == type) break;
338     previousBits += rgn->pDimensionDefinitions[i].bits;
339     }
340     return previousBits;
341     }
342    
343     inline int dimensionRegionIndex(gig::DimensionRegion* dimRgn) {
344     gig::Region* rgn = dimRgn->GetParent();
345     int sz = sizeof(rgn->pDimensionRegions) / sizeof(gig::DimensionRegion*);
346     for (int i = 0; i < sz; ++i)
347     if (rgn->pDimensionRegions[i] == dimRgn)
348     return i;
349     return -1;
350     }
351    
352     /** @brief Get exact zone ranges of given dimension.
353     *
354     * This function is useful for the velocity type dimension. In contrast to other
355     * dimension types, this dimension can have different zone ranges (that is
356     * different individual start and end points of its dimension zones) depending
357     * on which zones of other dimensions (on that gig::Region) are currently
358     * selected.
359     *
360     * @param type - dimension where the zone ranges should be retrieved for
361     * (usually the velocity dimension in this context)
362     * @param dimRgn - reflects the exact cases (zone selections) of all other
363     * dimensions than the given one in question
364     * @returns individual ranges for each zone of the questioned dimension type,
365     * it returns an empty result on errors instead
366     */
367     static DimensionZones preciseDimensionZonesFor(gig::dimension_t type, gig::DimensionRegion* dimRgn) {
368     DimensionZones zones;
369     gig::Region* rgn = dimRgn->GetParent();
370     int iDimension = getDimensionIndex(type, rgn);
371     if (iDimension < 0) return zones;
372     const gig::dimension_def_t& def = rgn->pDimensionDefinitions[iDimension];
373     int iDimRgn = dimensionRegionIndex(dimRgn);
374     int iBaseBits = baseBits(type, rgn);
375     int mask = ~(((1 << def.bits) - 1) << iBaseBits);
376    
377 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
378     printf("velo zones { ");
379     fflush(stdout);
380     #endif
381 schoenebeck 2548 int iLow = 0;
382     for (int z = 0; z < def.zones; ++z) {
383     gig::DimensionRegion* dimRgn2 =
384     rgn->pDimensionRegions[ (iDimRgn & mask) | ( z << iBaseBits) ];
385     int iHigh = dimRgn2->DimensionUpperLimits[iDimension];
386     DLS::range_t range = { iLow, iHigh};
387 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
388     printf("%d..%d, ", iLow, iHigh);
389     fflush(stdout);
390     #endif
391 schoenebeck 2548 zones.push_back(range);
392     iLow = iHigh + 1;
393     }
394 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
395     printf("}\n");
396     #endif
397 schoenebeck 2548 return zones;
398     }
399    
400 schoenebeck 2550 struct CopyAssignSchedEntry {
401     gig::DimensionRegion* src;
402     gig::DimensionRegion* dst;
403     int velocityZone;
404     int totalSrcVelocityZones;
405     };
406     typedef std::vector<CopyAssignSchedEntry> CopyAssignSchedule;
407 schoenebeck 2549
408 schoenebeck 2558 /** @brief Schedule copying DimensionRegions from source Region to target Region.
409 schoenebeck 2548 *
410 schoenebeck 2558 * Schedules copying the entire articulation informations (including sample
411     * reference) from all individual DimensionRegions of source Region @a inRgn to
412     * target Region @a outRgn. It is expected that the required dimensions (thus
413     * the required dimension regions) were already created before calling this
414     * function.
415 schoenebeck 2548 *
416 schoenebeck 2558 * To be precise, it does the task above only for the dimension zones defined by
417     * the three arguments @a mainDim, @a iSrcMainBit, @a iDstMainBit, which reflect
418     * a selection which dimension zones shall be copied. All other dimension zones
419     * will not be scheduled to be copied by a single call of this function. So this
420     * function needs to be called several time in case all dimension regions shall
421     * be copied of the entire region (@a inRgn, @a outRgn).
422 schoenebeck 2548 *
423     * @param outRgn - where the dimension regions shall be copied to
424     * @param inRgn - all dimension regions that shall be copied from
425 schoenebeck 2550 * @param dims - precise dimension definitions of target region
426 schoenebeck 2558 * @param mainDim - this dimension type, in combination with @a iSrcMainBit and
427     * @a iDstMainBit defines a selection which dimension region
428     * zones shall be copied by this call of this function
429     * @param iDstMainBit - destination bit of @a mainDim
430     * @param iSrcMainBit - source bit of @a mainDim
431     * @param schedule - list of all DimensionRegion copy operations which is filled
432     * during the nested loops / recursions of this function call
433 schoenebeck 2548 * @param dimCase - just for internal purpose (function recursion), don't pass
434     * anything here, this function will call itself recursively
435     * will fill this container with concrete dimension values for
436     * selecting the precise dimension regions during its task
437     */
438 schoenebeck 2558 static void scheduleCopyDimensionRegions(gig::Region* outRgn, gig::Region* inRgn,
439     Dimensions dims, gig::dimension_t mainDim,
440     int iDstMainBit, int iSrcMainBit,
441     CopyAssignSchedule* schedule,
442     DimensionCase dimCase = DimensionCase())
443     {
444 schoenebeck 2550 if (dims.empty()) { // reached deepest level of function recursion ...
445     CopyAssignSchedEntry e;
446    
447 schoenebeck 2548 // resolve the respective source & destination DimensionRegion ...
448     uint srcDimValues[8] = {};
449     uint dstDimValues[8] = {};
450     DimensionCase srcDimCase = dimCase;
451     DimensionCase dstDimCase = dimCase;
452 schoenebeck 2558 srcDimCase[mainDim] = iSrcMainBit;
453     dstDimCase[mainDim] = iDstMainBit;
454 schoenebeck 2548
455 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
456     printf("-------------------------------\n");
457 schoenebeck 2558 printf("iDstMainBit=%d iSrcMainBit=%d\n", iDstMainBit, iSrcMainBit);
458 schoenebeck 2550 #endif
459    
460 schoenebeck 2548 // first select source & target dimension region with an arbitrary
461     // velocity split zone, to get access to the precise individual velocity
462     // split zone sizes (if there is actually a velocity dimension at all,
463     // otherwise we already select the desired source & target dimension
464     // region here)
465 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
466     printf("src "); fflush(stdout);
467     #endif
468 schoenebeck 2548 fillDimValues(srcDimValues, srcDimCase, inRgn, false);
469 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
470     printf("dst "); fflush(stdout);
471     #endif
472 schoenebeck 2548 fillDimValues(dstDimValues, dstDimCase, outRgn, true);
473     gig::DimensionRegion* srcDimRgn = inRgn->GetDimensionRegionByValue(srcDimValues);
474     gig::DimensionRegion* dstDimRgn = outRgn->GetDimensionRegionByValue(dstDimValues);
475 schoenebeck 2549 #if DEBUG_COMBINE_INSTRUMENTS
476 schoenebeck 2558 printf("iDstMainBit=%d iSrcMainBit=%d\n", iDstMainBit, iSrcMainBit);
477 schoenebeck 2549 printf("srcDimRgn=%lx dstDimRgn=%lx\n", (uint64_t)srcDimRgn, (uint64_t)dstDimRgn);
478 schoenebeck 2550 printf("srcSample='%s' dstSample='%s'\n",
479     (!srcDimRgn->pSample ? "NULL" : srcDimRgn->pSample->pInfo->Name.c_str()),
480     (!dstDimRgn->pSample ? "NULL" : dstDimRgn->pSample->pInfo->Name.c_str())
481     );
482 schoenebeck 2549 #endif
483 schoenebeck 2548
484 schoenebeck 2550 assert(srcDimRgn->GetParent() == inRgn);
485     assert(dstDimRgn->GetParent() == outRgn);
486    
487 schoenebeck 2548 // now that we have access to the precise velocity split zone upper
488     // limits, we can select the actual source & destination dimension
489     // regions we need to copy (assuming that source or target region has
490     // a velocity dimension)
491     if (outRgn->GetDimensionDefinition(gig::dimension_velocity)) {
492 schoenebeck 2550 // re-select target dimension region (with correct velocity zone)
493     DimensionZones dstZones = preciseDimensionZonesFor(gig::dimension_velocity, dstDimRgn);
494 schoenebeck 2549 assert(dstZones.size() > 1);
495 schoenebeck 2558 const int iDstZoneIndex =
496     (mainDim == gig::dimension_velocity)
497     ? iDstMainBit : dstDimCase[gig::dimension_velocity]; // (mainDim == gig::dimension_velocity) exception case probably unnecessary here
498     e.velocityZone = iDstZoneIndex;
499 schoenebeck 2549 #if DEBUG_COMBINE_INSTRUMENTS
500 schoenebeck 2558 printf("dst velocity zone: %d/%d\n", iDstZoneIndex, (int)dstZones.size());
501 schoenebeck 2549 #endif
502 schoenebeck 2558 assert(uint(iDstZoneIndex) < dstZones.size());
503     dstDimCase[gig::dimension_velocity] = dstZones[iDstZoneIndex].low; // arbitrary value between low and high
504 schoenebeck 2549 #if DEBUG_COMBINE_INSTRUMENTS
505     printf("dst velocity value = %d\n", dstDimCase[gig::dimension_velocity]);
506 schoenebeck 2550 printf("dst refilled "); fflush(stdout);
507 schoenebeck 2549 #endif
508 schoenebeck 2548 fillDimValues(dstDimValues, dstDimCase, outRgn, true);
509     dstDimRgn = outRgn->GetDimensionRegionByValue(dstDimValues);
510 schoenebeck 2549 #if DEBUG_COMBINE_INSTRUMENTS
511     printf("reselected dstDimRgn=%lx\n", (uint64_t)dstDimRgn);
512 schoenebeck 2558 printf("dstSample='%s'%s\n",
513     (!dstDimRgn->pSample ? "NULL" : dstDimRgn->pSample->pInfo->Name.c_str()),
514     (dstDimRgn->pSample ? " <--- ERROR ERROR ERROR !!!!!!!!! " : "")
515 schoenebeck 2550 );
516 schoenebeck 2549 #endif
517 schoenebeck 2548
518 schoenebeck 2550 // re-select source dimension region with correct velocity zone
519     // (if it has a velocity dimension that is)
520 schoenebeck 2548 if (inRgn->GetDimensionDefinition(gig::dimension_velocity)) {
521 schoenebeck 2549 DimensionZones srcZones = preciseDimensionZonesFor(gig::dimension_velocity, srcDimRgn);
522 schoenebeck 2550 e.totalSrcVelocityZones = srcZones.size();
523 schoenebeck 2552 assert(srcZones.size() > 0);
524     if (srcZones.size() <= 1) {
525     addWarning("Input region has a velocity dimension with only ONE zone!");
526     }
527 schoenebeck 2558 int iSrcZoneIndex =
528     (mainDim == gig::dimension_velocity)
529     ? iSrcMainBit : iDstZoneIndex;
530     if (uint(iSrcZoneIndex) >= srcZones.size())
531     iSrcZoneIndex = srcZones.size() - 1;
532     srcDimCase[gig::dimension_velocity] = srcZones[iSrcZoneIndex].low; // same zone as used above for target dimension region (no matter what the precise zone ranges are)
533 schoenebeck 2550 #if DEBUG_COMBINE_INSTRUMENTS
534     printf("src refilled "); fflush(stdout);
535     #endif
536 schoenebeck 2548 fillDimValues(srcDimValues, srcDimCase, inRgn, false);
537     srcDimRgn = inRgn->GetDimensionRegionByValue(srcDimValues);
538 schoenebeck 2549 #if DEBUG_COMBINE_INSTRUMENTS
539     printf("reselected srcDimRgn=%lx\n", (uint64_t)srcDimRgn);
540 schoenebeck 2550 printf("srcSample='%s'\n",
541     (!srcDimRgn->pSample ? "NULL" : srcDimRgn->pSample->pInfo->Name.c_str())
542     );
543 schoenebeck 2549 #endif
544 schoenebeck 2548 }
545     }
546    
547 schoenebeck 2558 // Schedule copy operation of source -> target DimensionRegion for the
548 schoenebeck 2550 // time after all nested loops have been traversed. We have to postpone
549     // the actual copy operations this way, because otherwise it would
550     // overwrite informations inside the destination DimensionRegion object
551     // that we need to read in the code block above.
552     e.src = srcDimRgn;
553     e.dst = dstDimRgn;
554     schedule->push_back(e);
555 schoenebeck 2548
556 schoenebeck 2550 return; // returning from deepest level of function recursion
557 schoenebeck 2548 }
558    
559 schoenebeck 2549 // Copying n dimensions requires n nested loops. That's why this function
560     // is calling itself recursively to provide the required amount of nested
561     // loops. With each call it pops from argument 'dims' and pushes to
562     // argument 'dimCase'.
563    
564 schoenebeck 2548 Dimensions::iterator itDimension = dims.begin();
565     gig::dimension_t type = itDimension->first;
566     DimensionZones zones = itDimension->second;
567     dims.erase(itDimension);
568    
569     int iZone = 0;
570     for (DimensionZones::iterator itZone = zones.begin();
571     itZone != zones.end(); ++itZone, ++iZone)
572     {
573     DLS::range_t zoneRange = *itZone;
574     gig::dimension_def_t* def = outRgn->GetDimensionDefinition(type);
575     dimCase[type] = (def->split_type == gig::split_type_bit) ? iZone : zoneRange.low;
576 schoenebeck 2550
577 schoenebeck 2548 // recurse until 'dims' is exhausted (and dimCase filled up with concrete value)
578 schoenebeck 2558 scheduleCopyDimensionRegions(outRgn, inRgn, dims, mainDim, iDstMainBit, iSrcMainBit, schedule, dimCase);
579 schoenebeck 2548 }
580 schoenebeck 2558 }
581 schoenebeck 2550
582 schoenebeck 2558 static OrderedRegionGroup sortRegionGroup(const RegionGroup& group, const std::vector<gig::Instrument*>& instruments) {
583     OrderedRegionGroup result;
584     for (std::vector<gig::Instrument*>::const_iterator it = instruments.begin();
585     it != instruments.end(); ++it)
586     {
587     RegionGroup::const_iterator itRgn = group.find(*it);
588     if (itRgn == group.end()) continue;
589     result.push_back(
590     std::pair<gig::Instrument*, gig::Region*>(
591     itRgn->first, itRgn->second
592     )
593     );
594 schoenebeck 2550 }
595 schoenebeck 2558 return result;
596 schoenebeck 2548 }
597    
598     /** @brief Combine given list of instruments to one instrument.
599     *
600     * Takes a list of @a instruments as argument and combines them to one single
601 schoenebeck 2558 * new @a output instrument. For this task, it will create a dimension of type
602     * given by @a mainDimension in the new instrument and copies the source
603     * instruments to those dimension zones.
604 schoenebeck 2548 *
605     * @param instruments - (input) list of instruments that shall be combined,
606     * they will only be read, so they will be left untouched
607     * @param gig - (input/output) .gig file where the new combined instrument shall
608     * be created
609     * @param output - (output) on success this pointer will be set to the new
610     * instrument being created
611 schoenebeck 2558 * @param mainDimension - the dimension that shall be used to combine the
612     * instruments
613 schoenebeck 2548 * @throw RIFF::Exception on any kinds of errors
614     */
615 schoenebeck 2558 static void combineInstruments(std::vector<gig::Instrument*>& instruments, gig::File* gig, gig::Instrument*& output, gig::dimension_t mainDimension) {
616 schoenebeck 2548 output = NULL;
617    
618     // divide the individual regions to (probably even smaller) groups of
619     // regions, coping with the fact that the source regions of the instruments
620     // might have quite different range sizes and start and end points
621     RegionGroups groups = groupByRegionIntersections(instruments);
622     #if DEBUG_COMBINE_INSTRUMENTS
623     std::cout << std::endl << "New regions: " << std::flush;
624     printRanges(groups);
625     std::cout << std::endl;
626     #endif
627    
628     if (groups.empty())
629     throw gig::Exception(_("No regions found to create a new instrument with."));
630    
631     // create a new output instrument
632     gig::Instrument* outInstr = gig->AddInstrument();
633 schoenebeck 2549 outInstr->pInfo->Name = _("NEW COMBINATION");
634 schoenebeck 2548
635     // Distinguishing in the following code block between 'horizontal' and
636     // 'vertical' regions. The 'horizontal' ones are meant to be the key ranges
637     // in the output instrument, while the 'vertical' regions are meant to be
638     // the set of source regions that shall be layered to that 'horizontal'
639     // region / key range. It is important to know, that the key ranges defined
640     // in the 'horizontal' and 'vertical' regions might differ.
641    
642     // merge the instruments to the new output instrument
643     for (RegionGroups::iterator itGroup = groups.begin();
644     itGroup != groups.end(); ++itGroup) // iterate over 'horizontal' / target regions ...
645     {
646     gig::Region* outRgn = outInstr->AddRegion();
647     outRgn->SetKeyRange(itGroup->first.low, itGroup->first.high);
648 schoenebeck 2552 #if DEBUG_COMBINE_INSTRUMENTS
649     printf("---> Start target region %d..%d\n", itGroup->first.low, itGroup->first.high);
650     #endif
651 schoenebeck 2548
652 schoenebeck 2558 // detect the total amount of zones required for the given main
653     // dimension to build up this combi for current key range
654     int iTotalZones = 0;
655 schoenebeck 2548 for (RegionGroup::iterator itRgn = itGroup->second.begin();
656     itRgn != itGroup->second.end(); ++itRgn)
657     {
658     gig::Region* inRgn = itRgn->second;
659 schoenebeck 2558 gig::dimension_def_t* def = inRgn->GetDimensionDefinition(mainDimension);
660     iTotalZones += (def) ? def->zones : 1;
661 schoenebeck 2548 }
662 schoenebeck 2552 #if DEBUG_COMBINE_INSTRUMENTS
663 schoenebeck 2558 printf("Required total zones: %d\n", iTotalZones);
664 schoenebeck 2552 #endif
665 schoenebeck 2558
666 schoenebeck 2548 // create all required dimensions for this output region
667 schoenebeck 2558 // (except the main dimension used for separating the individual
668     // instruments, we create that particular dimension as next step)
669 schoenebeck 2548 Dimensions dims = getDimensionsForRegionGroup(itGroup->second);
670 schoenebeck 2558 // the given main dimension which is used to combine the instruments is
671     // created separately after the next code block, and the main dimension
672     // should not be part of dims here, because it also used for iterating
673     // all dimensions zones, which would lead to this dimensions being
674     // iterated twice
675     dims.erase(mainDimension);
676 schoenebeck 2548 {
677 schoenebeck 2552 std::vector<gig::dimension_t> skipTheseDimensions; // used to prevent a misbehavior (i.e. crash) of the combine algorithm in case one of the source instruments has a dimension with only one zone, which is not standard conform
678 schoenebeck 2548
679 schoenebeck 2552 for (Dimensions::iterator itDim = dims.begin();
680     itDim != dims.end(); ++itDim)
681     {
682     gig::dimension_def_t def;
683     def.dimension = itDim->first; // dimension type
684     def.zones = itDim->second.size();
685     def.bits = zoneCountToBits(def.zones);
686     if (def.zones < 2) {
687     addWarning(
688     "Attempt to create dimension with type=0x%x with only "
689     "ONE zone (because at least one of the source "
690     "instruments seems to have such a velocity dimension "
691     "with only ONE zone, which is odd)! Skipping this "
692     "dimension for now.",
693     (int)itDim->first
694     );
695     skipTheseDimensions.push_back(itDim->first);
696     continue;
697     }
698     #if DEBUG_COMBINE_INSTRUMENTS
699     std::cout << "Adding new regular dimension type=" << std::hex << (int)def.dimension << std::dec << ", zones=" << (int)def.zones << ", bits=" << (int)def.bits << " ... " << std::flush;
700     #endif
701     outRgn->AddDimension(&def);
702     #if DEBUG_COMBINE_INSTRUMENTS
703     std::cout << "OK" << std::endl << std::flush;
704     #endif
705     }
706     // prevent the following dimensions to be processed further below
707     // (since the respective dimension was not created above)
708     for (int i = 0; i < skipTheseDimensions.size(); ++i)
709     dims.erase(skipTheseDimensions[i]);
710 schoenebeck 2548 }
711    
712 schoenebeck 2558 // create the main dimension (if necessary for current key range)
713     if (iTotalZones > 1) {
714 schoenebeck 2548 gig::dimension_def_t def;
715 schoenebeck 2558 def.dimension = mainDimension; // dimension type
716     def.zones = iTotalZones;
717 schoenebeck 2548 def.bits = zoneCountToBits(def.zones);
718     #if DEBUG_COMBINE_INSTRUMENTS
719 schoenebeck 2558 std::cout << "Adding new main combi dimension type=" << std::hex << (int)def.dimension << std::dec << ", zones=" << (int)def.zones << ", bits=" << (int)def.bits << " ... " << std::flush;
720 schoenebeck 2548 #endif
721     outRgn->AddDimension(&def);
722     #if DEBUG_COMBINE_INSTRUMENTS
723     std::cout << "OK" << std::endl << std::flush;
724     #endif
725     }
726    
727 schoenebeck 2558 // for the next task we need to have the current RegionGroup to be
728     // sorted by instrument in the same sequence as the 'instruments' vector
729     // argument passed to this function (because the std::map behind the
730     // 'RegionGroup' type sorts by memory address instead, and that would
731     // sometimes lead to the source instruments' region to be sorted into
732     // the wrong target layer)
733     OrderedRegionGroup currentGroup = sortRegionGroup(itGroup->second, instruments);
734    
735     // schedule copying the source dimension regions to the target dimension
736     // regions
737     CopyAssignSchedule schedule;
738     int iDstMainBit = 0;
739     for (OrderedRegionGroup::iterator itRgn = currentGroup.begin();
740     itRgn != currentGroup.end(); ++itRgn) // iterate over 'vertical' / source regions ...
741 schoenebeck 2548 {
742     gig::Region* inRgn = itRgn->second;
743 schoenebeck 2552 #if DEBUG_COMBINE_INSTRUMENTS
744     printf("[source region of '%s']\n", inRgn->GetParent()->pInfo->Name.c_str());
745     #endif
746 schoenebeck 2558
747     // determine how many main dimension zones this input region requires
748     gig::dimension_def_t* def = inRgn->GetDimensionDefinition(mainDimension);
749     const int inRgnMainZones = (def) ? def->zones : 1;
750    
751     for (uint iSrcMainBit = 0; iSrcMainBit < inRgnMainZones; ++iSrcMainBit, ++iDstMainBit) {
752     scheduleCopyDimensionRegions(
753     outRgn, inRgn, dims, mainDimension,
754     iDstMainBit, iSrcMainBit, &schedule
755     );
756 schoenebeck 2548 }
757     }
758 schoenebeck 2558
759     // finally copy the scheduled source -> target dimension regions
760     for (uint i = 0; i < schedule.size(); ++i) {
761     CopyAssignSchedEntry& e = schedule[i];
762    
763     // backup the target DimensionRegion's current dimension zones upper
764     // limits (because the target DimensionRegion's upper limits are
765     // already defined correctly since calling AddDimension(), and the
766     // CopyAssign() call next, will overwrite those upper limits
767     // unfortunately
768     DimensionRegionUpperLimits dstUpperLimits = getDimensionRegionUpperLimits(e.dst);
769     DimensionRegionUpperLimits srcUpperLimits = getDimensionRegionUpperLimits(e.src);
770    
771     // now actually copy over the current DimensionRegion
772     const gig::Region* const origRgn = e.dst->GetParent(); // just for sanity check below
773     e.dst->CopyAssign(e.src);
774     assert(origRgn == e.dst->GetParent()); // if gigedit is crashing here, then you must update libgig (to at least SVN r2547, v3.3.0.svn10)
775    
776     // restore all original dimension zone upper limits except of the
777     // velocity dimension, because the velocity dimension zone sizes are
778     // allowed to differ for individual DimensionRegions in gig v3
779     // format
780     //
781     // if the main dinension is the 'velocity' dimension, then skip
782     // restoring the source's original velocity zone limits, because
783     // dealing with merging that is not implemented yet
784     // TODO: merge custom velocity splits if main dimension is the velocity dimension (for now equal sized velocity zones are used if mainDim is 'velocity')
785     if (srcUpperLimits.count(gig::dimension_velocity) && mainDimension != gig::dimension_velocity) {
786     if (!dstUpperLimits.count(gig::dimension_velocity)) {
787     addWarning("Source instrument seems to have a velocity dimension whereas new target instrument doesn't!");
788     } else {
789     dstUpperLimits[gig::dimension_velocity] =
790     (e.velocityZone >= e.totalSrcVelocityZones)
791     ? 127 : srcUpperLimits[gig::dimension_velocity];
792     }
793     }
794     restoreDimensionRegionUpperLimits(e.dst, dstUpperLimits);
795     }
796 schoenebeck 2548 }
797    
798     // success
799     output = outInstr;
800     }
801    
802     ///////////////////////////////////////////////////////////////////////////
803     // class 'CombineInstrumentsDialog'
804    
805     CombineInstrumentsDialog::CombineInstrumentsDialog(Gtk::Window& parent, gig::File* gig)
806     : Gtk::Dialog(_("Combine Instruments"), parent, true),
807     m_gig(gig), m_fileWasChanged(false), m_newCombinedInstrument(NULL),
808     m_cancelButton(Gtk::Stock::CANCEL), m_OKButton(Gtk::Stock::OK),
809 schoenebeck 2558 m_descriptionLabel(), m_tableDimCombo(2, 2), m_comboDimType(),
810     m_labelDimType(Glib::ustring(_("Combine by Dimension:")) + " ", Gtk::ALIGN_RIGHT)
811 schoenebeck 2548 {
812     get_vbox()->pack_start(m_descriptionLabel, Gtk::PACK_SHRINK);
813 schoenebeck 2558 get_vbox()->pack_start(m_tableDimCombo, Gtk::PACK_SHRINK);
814 schoenebeck 2548 get_vbox()->pack_start(m_treeView);
815     get_vbox()->pack_start(m_buttonBox, Gtk::PACK_SHRINK);
816    
817     #if GTKMM_MAJOR_VERSION >= 3
818     description.set_line_wrap();
819     #endif
820     m_descriptionLabel.set_text(_(
821 schoenebeck 2558 "Select at least two instruments below that shall be combined (as "
822     "separate dimension zones of the selected dimension type) as a new "
823     "instrument. The original instruments remain untouched.\n\n"
824     "You may use this tool for example to combine solo instruments into "
825     "a combi sound arrangement by selecting the 'layer' dimension, or you "
826     "might combine similar sounding solo sounds into separate velocity "
827     "split layers by using the 'velocity' dimension, and so on."
828     ));
829 schoenebeck 2548
830 schoenebeck 2558 // add dimension type combo box
831     {
832     int iLayerDimIndex = -1;
833     Glib::RefPtr<Gtk::ListStore> refComboModel = Gtk::ListStore::create(m_comboDimsModel);
834     for (int i = 0x01, iRow = 0; i < 0xff; i++) {
835     Glib::ustring sType =
836     dimTypeAsString(static_cast<gig::dimension_t>(i));
837     if (sType.find("Unknown") != 0) {
838     Gtk::TreeModel::Row row = *(refComboModel->append());
839     row[m_comboDimsModel.m_type_id] = i;
840     row[m_comboDimsModel.m_type_name] = sType;
841     if (i == gig::dimension_layer) iLayerDimIndex = iRow;
842     iRow++;
843     }
844     }
845     m_comboDimType.set_model(refComboModel);
846     m_comboDimType.pack_start(m_comboDimsModel.m_type_id);
847     m_comboDimType.pack_start(m_comboDimsModel.m_type_name);
848     m_tableDimCombo.attach(m_labelDimType, 0, 1, 0, 1);
849     m_tableDimCombo.attach(m_comboDimType, 1, 2, 0, 1);
850     m_comboDimType.set_active(iLayerDimIndex); // preselect "layer" dimension
851     }
852    
853 schoenebeck 2548 m_refTreeModel = Gtk::ListStore::create(m_columns);
854     m_treeView.set_model(m_refTreeModel);
855 schoenebeck 2550 m_treeView.set_tooltip_text(_(
856     "Use SHIFT + left click or CTRL + left click to select the instruments "
857     "you want to combine."
858     ));
859 schoenebeck 2548 m_treeView.append_column("Instrument", m_columns.m_col_name);
860     m_treeView.set_headers_visible(false);
861     m_treeView.get_selection()->set_mode(Gtk::SELECTION_MULTIPLE);
862     m_treeView.get_selection()->signal_changed().connect(
863     sigc::mem_fun(*this, &CombineInstrumentsDialog::onSelectionChanged)
864     );
865     m_treeView.show();
866    
867     for (int i = 0; true; ++i) {
868     gig::Instrument* instr = gig->GetInstrument(i);
869     if (!instr) break;
870    
871     #if DEBUG_COMBINE_INSTRUMENTS
872     {
873     std::cout << "Instrument (" << i << ") '" << instr->pInfo->Name << "' Regions: " << std::flush;
874     for (gig::Region* rgn = instr->GetFirstRegion(); rgn; rgn = instr->GetNextRegion()) {
875     std::cout << rgn->KeyRange.low << ".." << rgn->KeyRange.high << ", " << std::flush;
876     }
877     std::cout << std::endl;
878     }
879     std::cout << std::endl;
880     #endif
881    
882     Glib::ustring name(gig_to_utf8(instr->pInfo->Name));
883     Gtk::TreeModel::iterator iter = m_refTreeModel->append();
884     Gtk::TreeModel::Row row = *iter;
885     row[m_columns.m_col_name] = name;
886     row[m_columns.m_col_instr] = instr;
887     }
888    
889     m_buttonBox.set_layout(Gtk::BUTTONBOX_END);
890     m_buttonBox.set_border_width(5);
891     m_buttonBox.pack_start(m_cancelButton, Gtk::PACK_SHRINK);
892     m_buttonBox.pack_start(m_OKButton, Gtk::PACK_SHRINK);
893     m_buttonBox.show();
894    
895     m_cancelButton.show();
896     m_OKButton.set_sensitive(false);
897     m_OKButton.show();
898    
899     m_cancelButton.signal_clicked().connect(
900     sigc::mem_fun(*this, &CombineInstrumentsDialog::hide)
901     );
902    
903     m_OKButton.signal_clicked().connect(
904     sigc::mem_fun(*this, &CombineInstrumentsDialog::combineSelectedInstruments)
905     );
906    
907     show_all_children();
908 schoenebeck 2550
909     // show a warning to user if he uses a .gig in v2 format
910     if (gig->pVersion->major < 3) {
911     Glib::ustring txt = _(
912     "You are currently using a .gig file in old v2 format. The current "
913     "combine algorithm will most probably fail trying to combine "
914     "instruments in this old format. So better save the file in new v3 "
915     "format before trying to combine your instruments."
916     );
917     Gtk::MessageDialog msg(*this, txt, false, Gtk::MESSAGE_WARNING);
918     msg.run();
919     }
920 schoenebeck 2548 }
921    
922     void CombineInstrumentsDialog::combineSelectedInstruments() {
923     std::vector<gig::Instrument*> instruments;
924     std::vector<Gtk::TreeModel::Path> v = m_treeView.get_selection()->get_selected_rows();
925     for (uint i = 0; i < v.size(); ++i) {
926     Gtk::TreeModel::iterator it = m_refTreeModel->get_iter(v[i]);
927     Gtk::TreeModel::Row row = *it;
928     Glib::ustring name = row[m_columns.m_col_name];
929     gig::Instrument* instrument = row[m_columns.m_col_instr];
930     #if DEBUG_COMBINE_INSTRUMENTS
931     printf("Selection '%s' 0x%lx\n\n", name.c_str(), int64_t((void*)instrument));
932     #endif
933     instruments.push_back(instrument);
934     }
935    
936 schoenebeck 2552 g_warnings.clear();
937    
938 schoenebeck 2548 try {
939 schoenebeck 2558 // which main dimension was selected in the combo box?
940     gig::dimension_t mainDimension;
941     {
942     Gtk::TreeModel::iterator iterType = m_comboDimType.get_active();
943     if (!iterType) throw gig::Exception("No dimension selected");
944     Gtk::TreeModel::Row rowType = *iterType;
945     if (!rowType) throw gig::Exception("Something is wrong regarding dimension selection");
946     int iTypeID = rowType[m_comboDimsModel.m_type_id];
947     mainDimension = static_cast<gig::dimension_t>(iTypeID);
948     }
949    
950     // now start the actual cobination task ...
951     combineInstruments(instruments, m_gig, m_newCombinedInstrument, mainDimension);
952 schoenebeck 2548 } catch (RIFF::Exception e) {;
953     Gtk::MessageDialog msg(*this, e.Message, false, Gtk::MESSAGE_ERROR);
954     msg.run();
955     return;
956 schoenebeck 2553 } catch (...) {
957     Glib::ustring txt = _("An unknown exception occurred!");
958     Gtk::MessageDialog msg(*this, txt, false, Gtk::MESSAGE_ERROR);
959     msg.run();
960     return;
961 schoenebeck 2548 }
962    
963 schoenebeck 2552 if (!g_warnings.empty()) {
964     Glib::ustring txt = _(
965     "Combined instrument was created successfully, but there were warnings:"
966     );
967     txt += "\n\n";
968     for (Warnings::const_iterator itWarn = g_warnings.begin();
969     itWarn != g_warnings.end(); ++itWarn)
970     {
971     txt += "-> " + *itWarn + "\n";
972     }
973     txt += "\n";
974     txt += _(
975     "You might also want to check the console for further warnings and "
976     "error messages."
977     );
978     Gtk::MessageDialog msg(*this, txt, false, Gtk::MESSAGE_WARNING);
979     msg.run();
980     }
981    
982 schoenebeck 2548 // no error occurred
983     m_fileWasChanged = true;
984     hide();
985     }
986    
987     void CombineInstrumentsDialog::onSelectionChanged() {
988     std::vector<Gtk::TreeModel::Path> v = m_treeView.get_selection()->get_selected_rows();
989     m_OKButton.set_sensitive(v.size() >= 2);
990     }
991    
992     bool CombineInstrumentsDialog::fileWasChanged() const {
993     return m_fileWasChanged;
994     }
995    
996     gig::Instrument* CombineInstrumentsDialog::newCombinedInstrument() const {
997     return m_newCombinedInstrument;
998     }

  ViewVC Help
Powered by ViewVC