| Line | Revision | Contents |
| 1 | 185 | /* Calliope Music Player |
| 2 | * Copyright 2005-09 Sam Thursfield <ssssam gmail.com> | |
| 3 | * | |
| 4 | * This program is free software: you can redistribute it and/or modify | |
| 5 | * it under the terms of the GNU General Public License as published by | |
| 6 | * the Free Software Foundation, either version 3 of the License, or | |
| 7 | * (at your option) any later version. | |
| 8 | * | |
| 9 | * This program is distributed in the hope that it will be useful, | |
| 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 12 | * GNU General Public License for more details. | |
| 13 | * | |
| 14 | * You should have received a copy of the GNU General Public License | |
| 15 | 261 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | 185 | */ |
| 17 | ||
| 18 | /* genericview: presents a view on any source, using only musicsource base methods. */ | |
| 19 | ||
| 20 | #include <stdlib.h> | |
| 21 | #include <string.h> | |
| 22 | #include "main.h" | |
| 23 | #include "musicsourceview.h" | |
| 24 | #include "musicsourceview-private.h" | |
| 25 | #include "genericview.h" | |
| 26 | #include "genericview-private.h" | |
| 27 | ||
| 28 | #define PP MUSIC_SOURCE_VIEW(self)->priv | |
| 29 | ||
| 30 | static GObject *constructor(GType type, guint n_construct_properties, GObjectConstructParam *construct_params); | |
| 31 | static void finalize(GObject *self); | |
| 32 | ||
| 33 | //static gboolean entry_is_orphan(MusicSourceView *self, int id); | |
| 34 | static void initial_read(MusicSourceView *self, ViewNode *node); | |
| 35 | 261 | static ViewPage *read_page_at (MusicSourceView *music_source_view, ViewPage *head, |
| 36 | 185 | ViewPage *tail, int *e); |
| 37 | static void read_next_page(MusicSourceView *music_source_view, ViewPage *page); | |
| 38 | ||
| 39 | 361 | static void entry_notify (MusicSource *source, Entry *old_entry, Entry *new_entry, |
| 40 | MusicSourceView *self); | |
| 41 | 185 | |
| 42 | 434 | static void reading_trace (int level, const char *format, ...); |
| 43 | static void change_trace (char *format, ...); | |
| 44 | 185 | |
| 45 | G_DEFINE_TYPE(GenericView, generic_view, MUSIC_SOURCE_VIEW_TYPE); | |
| 46 | ||
| 47 | static void generic_view_class_init(GenericViewClass *class) { | |
| 48 | generic_view_parent_class=g_type_class_peek_parent(class); | |
| 49 | 261 | |
| 50 | 185 | GObjectClass *object_class=(GObjectClass *) class; |
| 51 | object_class->constructor=constructor; | |
| 52 | object_class->finalize=finalize; | |
| 53 | //MUSIC_SOURCE_VIEW_CLASS(class)->entry_is_orphan=entry_is_orphan; | |
| 54 | MUSIC_SOURCE_VIEW_CLASS(class)->initial_read=initial_read; | |
| 55 | MUSIC_SOURCE_VIEW_CLASS(class)->read_page_at = read_page_at; | |
| 56 | MUSIC_SOURCE_VIEW_CLASS(class)->read_next_page=read_next_page; | |
| 57 | MUSIC_SOURCE_VIEW_CLASS(class)->get_path_for_id = get_path_for_id; | |
| 58 | MUSIC_SOURCE_VIEW_CLASS(class)->get_path_for_id_begin = get_path_for_id_begin; | |
| 59 | MUSIC_SOURCE_VIEW_CLASS(class)->get_path_for_id_step = get_path_for_id_step; | |
| 60 | MUSIC_SOURCE_VIEW_CLASS(class)->get_path_for_id_end = get_path_for_id_end; | |
| 61 | } | |
| 62 | ||
| 63 | static void generic_view_init (GenericView *self) { | |
| 64 | for (int i=0; i<ENTRY_TYPE_COUNT; i++) | |
| 65 | self->ignore_list[i] = NULL; | |
| 66 | }; | |
| 67 | ||
| 68 | static GObject *constructor(GType type, guint n_construct_properties, GObjectConstructParam *construct_params) { | |
| 69 | GObject *object = ((GObjectClass *)generic_view_parent_class)->constructor(type, n_construct_properties, construct_params); | |
| 70 | GenericView *self = GENERIC_VIEW(object); | |
| 71 | ||
| 72 | // Watch the source for changes | |
| 73 | 261 | // |
| 74 | 399 | music_source_connect_entry_notify_object (PP->source, -1, TRUE, (EntryNotifyFunc)entry_notify, |
| 75 | 393 | G_OBJECT(self)); |
| 76 | 185 | |
| 77 | return object; | |
| 78 | }; | |
| 79 | ||
| 80 | static void finalize(GObject *object) { | |
| 81 | 261 | //GenericView *self = GENERIC_VIEW(object); |
| 82 | 431 | //printf ("view:finalise .. \n"); fflush (stdout); |
| 83 | 185 | G_OBJECT_CLASS(generic_view_parent_class)->finalize (object); |
| 84 | }; | |
| 85 | ||
| 86 | 438 | /************************************************************************************** |
| 87 | * Utilities (used externally too, I wonder if this is the best place for them or not) | |
| 88 | */ | |
| 89 | 185 | |
| 90 | 438 | /* check_clnode_for_entry: TRUE if 'clnode' is on the same branch as 'entry'. |
| 91 | * | |
| 92 | * *branch_id, *branch_tested_n: state variables for recursive use. Pass pointers to variables of | |
| 93 | * the appropriate type, both set to 1. | |
| 94 | * FIXME: put this in musicsource, along with the other viewconfig functions that require a source | |
| 95 | * I think. */ | |
| 96 | gboolean check_clnode_for_entry (MusicSource *source, ViewConfig *config, GList *clnode, | |
| 97 | Entry *entry, guint32 *branch_id, int *branch_tested_n) { | |
| 98 | 388 | |
| 99 | 384 | // If we aren't at the orphaning join node, recurse forwards. |
| 100 | ViewConfigNode *cnode = CONFIG_NODE(clnode); | |
| 101 | ||
| 102 | while (CONFIG_NODE(clnode)->branch_n > (*branch_tested_n)) { | |
| 103 | // Find the RHS entry of the orphaning join. 'entry' must be beyond the orphaning join | |
| 104 | // in the config, or there's no possibility of it's config node being on that node's | |
| 105 | // orphaning branch ........ FIXME: is this true? What about the artist preceeding the | |
| 106 | // recording on an orphan branch or something like that? :( :( | |
| 107 | // | |
| 108 | 388 | // To understand the function better, imagine the following: |
| 109 | // (artist[name]:album[name]:release:track:recording[name]:file[bitrate]) | |
| 110 | // / (artist[name]:recording:file[bitrate]) | |
| 111 | // | |
| 112 | // 'clnode' must be of the same type of 'entry' of course, but could be on either branch. | |
| 113 | // | |
| 114 | ||
| 115 | // orphaning_clnode is the recording in the main branch. | |
| 116 | 438 | GList *orphaning_clnode = config->orphaning_join_node[(*branch_tested_n)-1]; |
| 117 | 388 | ViewConfigNode *orphaning_cnode = CONFIG_NODE(orphaning_clnode); |
| 118 | ||
| 119 | // We need to use the horizontal position (n) of the nodes to see if we need to walk | |
| 120 | // forwards or backwards from clnode to reach the orphaned (one) type. If clnode is on the | |
| 121 | // orphan branch, these probably won't match up. We find the difference in their lengths | |
| 122 | // using the reference point of the one node's n in each branch. (The recording nodes in the | |
| 123 | // example above). | |
| 124 | int branch_n_delta = 0; | |
| 125 | if (!CLNODE_IS_ON_BRANCH(orphaning_clnode, clnode)) | |
| 126 | branch_n_delta = orphaning_cnode->n - CONFIG_NODE(orphaning_cnode->orphanage)->n; | |
| 127 | 390 | //printf ("clnode branch id %i, orphaning %i - delta %i\n", CONFIG_NODE(clnode)->branch_id, orphaning_cnode->branch_id, branch_n_delta); fflush (stdout); |
| 128 | 388 | |
| 129 | 384 | Entry *one; |
| 130 | 388 | if (entry->type == orphaning_cnode->type) { |
| 131 | // clnode is the "one" node - the recording in the example above. | |
| 132 | g_warn_if_fail ((CONFIG_NODE(clnode)->n + branch_n_delta) == orphaning_cnode->n); | |
| 133 | 384 | one = entry; |
| 134 | 388 | } else if ((CONFIG_NODE(clnode)->n + branch_n_delta) > orphaning_cnode->n) { |
| 135 | 438 | // clnode is past the "one" node (the file in the above example). We can walk backwards |
| 136 | // to find the 'one' entry. | |
| 137 | 431 | one = view_config_walk_backwards_specific |
| 138 | 438 | (clnode, source, entry, orphaning_cnode->type, NULL, NULL, NULL, NULL); |
| 139 | 384 | g_return_val_if_fail (one!=NULL, FALSE); |
| 140 | 388 | } else { |
| 141 | 438 | // clnode is before the "one" node (imagine an artist in the example). This is a more |
| 142 | // complicated case because the entry could appear on *both* branches - it's on a branch | |
| 143 | // if any one of its children is on that branch. | |
| 144 | // | |
| 145 | // In the context of the example, if 'entry' is of type 'artist', here | |
| 146 | // view_config_step_forward_fast() returns a list of that artist's 'album' entries, and | |
| 147 | // then a list of that artist's 'recording' entries. It is up to us to filter the | |
| 148 | // recording entries to see if any are actually orphans and will appear on that branch - | |
| 149 | // but we only need to check one, since if the 'artist' entry has any orphan children we | |
| 150 | // know it will appear on the branch. | |
| 151 | ||
| 152 | // view_config_step_forwards_fast() returns a list of entry lists, one for each branch. | |
| 153 | // We only need to check the first entry of each branch. | |
| 154 | // FIXME: we could actually be cleverer, because the results are in order of branch_id, | |
| 155 | // so we only need to test if branch 'branch_id' has any entries, I think. That's a job | |
| 156 | // for another commit however. | |
| 157 | GSList *rhs_branch_list = view_config_step_forwards_fast(source, config, clnode, entry); | |
| 158 | 388 | gboolean result = FALSE; |
| 159 | 438 | for (GSList *rhs_list=rhs_branch_list; rhs_list; rhs_list=rhs_list->next) { |
| 160 | GSList *rhs_node = rhs_list->data; | |
| 161 | 444 | if (rhs_node == NULL) |
| 162 | continue; | |
| 163 | ||
| 164 | 438 | if (!result && check_clnode_for_entry(source, config, clnode->next, rhs_node->data, |
| 165 | branch_id, branch_tested_n)) | |
| 166 | result = TRUE; | |
| 167 | entry_query_list_free (rhs_node, "walk-config"); | |
| 168 | 388 | }; |
| 169 | 438 | g_slist_free (rhs_branch_list); |
| 170 | 388 | return result; |
| 171 | 384 | }; |
| 172 | ||
| 173 | const int rel_apid = orphaning_cnode->relation_apid; | |
| 174 | const int relation_count = music_source_query_n_relations | |
| 175 | 438 | (source, one->id, APID_GET_TYPE(rel_apid), |
| 176 | APID_GET_PROPERTY_ID(rel_apid), 1); | |
| 177 | 384 | |
| 178 | if (entry->type != orphaning_cnode->type) | |
| 179 | entry_unref (one, "walk-config"); | |
| 180 | //printf ("%s %i on %s.%s: %i relations.\n", ENTRY_PF(one), APID_NAME(rel_apid), | |
| 181 | // relation_count); fflush (stdout); | |
| 182 | ||
| 183 | if (relation_count==0) | |
| 184 | (*branch_id) |= (1<<(*branch_tested_n)); | |
| 185 | (*branch_tested_n) ++; | |
| 186 | }; | |
| 187 | ||
| 188 | 390 | //change_trace ("check_clnode_for_entry: new n %i, id %x (clnode %i, %x): result %i\n", *branch_tested_n, |
| 189 | // *branch_id, cnode->branch_id, cnode->branch_n, (cnode->branch_id == (*branch_id))); | |
| 190 | 384 | return (cnode->branch_id == (*branch_id)); |
| 191 | }; | |
| 192 | ||
| 193 | 263 | GList *_find_clnode_for_entry (GenericView *self, Entry *entry) { |
| 194 | 383 | // config->index[entry->type] gives a list of clnodes for entries of the correct type. We then |
| 195 | // need to work out which of these is on the correct branch for 'entry'. | |
| 196 | 224 | GSList *node_list = PP->config->index[entry->type]; |
| 197 | 261 | |
| 198 | 224 | if (node_list==NULL) { |
| 199 | g_warning ("There is no node for %s in view config.", entry_type_name[entry->type]); | |
| 200 | 263 | return NULL; |
| 201 | 224 | }; |
| 202 | 261 | |
| 203 | 383 | if (node_list->next==NULL) |
| 204 | 263 | return node_list->data; |
| 205 | 261 | |
| 206 | 383 | // At this point we find the clnode on the correct branch for 'entry'. We test entry against |
| 207 | // each orphaning join up to clnode's branch_n; the results are stored in branch_id, while | |
| 208 | // branch_queried_n stores the highest branch_n tested for so far. | |
| 209 | 384 | guint32 entry_branch_id = 1; int branch_tested_n = 1; |
| 210 | 224 | for (GSList *node_list_node=node_list; node_list_node; node_list_node=node_list_node->next) { |
| 211 | 383 | GList *clnode = node_list_node->data; |
| 212 | ||
| 213 | // Check against the relevant orphaning joins. | |
| 214 | 438 | if (check_clnode_for_entry (PP->source, PP->config, clnode, entry, |
| 215 | &entry_branch_id, &branch_tested_n)) | |
| 216 | 383 | return clnode; |
| 217 | 261 | }; |
| 218 | 383 | |
| 219 | return NULL; | |
| 220 | 261 | }; |
| 221 | 185 | |
| 222 | ||
| 223 | 384 | |
| 224 | 185 | ///////////////////////////////////////// |
| 225 | // Page reading functions | |
| 226 | // | |
| 227 | ||
| 228 | typedef struct { | |
| 229 | 229 | gboolean complete; // TRUE when this is the clnode _append_entries wants. |
| 230 | 261 | |
| 231 | GList *clnode; // clnode this result represents. | |
| 232 | int id; | |
| 233 | 216 | int intermediate_n, *intermediate_ids; |
| 234 | 185 | int sort_group; |
| 235 | } _Result; | |
| 236 | ||
| 237 | // Convert a list of entry id's (as returned by some music source query functions) into a | |
| 238 | // list of _Result values. | |
| 239 | // | |
| 240 | 217 | static GSList *_id_list_to_result_list(GSList *in, GList *clnode) { |
| 241 | 185 | GSList *out = NULL; |
| 242 | for (GSList *node=in; node; node=node->next) { | |
| 243 | _Result *r = g_slice_new0(_Result); | |
| 244 | 227 | r->clnode = clnode; |
| 245 | 185 | r->id = GPOINTER_TO_INT(node->data); |
| 246 | out = g_slist_prepend(out, r); | |
| 247 | }; | |
| 248 | g_slist_free (in); | |
| 249 | return out; | |
| 250 | }; | |
| 251 | ||
| 252 | // FIXME: this really is painfully slow | |
| 253 | // FIXME: at the very least, it should be done internally to the source since then we wouldn't | |
| 254 | // waste time refing and unreffing the entries, and for library would make things a whole | |
| 255 | // lot faster when adding entries. | |
| 256 | // As said elsewhere, once an api for sorted queries exists it makes libraryview | |
| 257 | // seem less useful. | |
| 258 | 261 | // FIXME: also, we could cache some of the properties in the _result node somehow ... or |
| 259 | 185 | // something. |
| 260 | 261 | GSList *_sort_result_list (MusicSourceView *self, GSList *list, EntryType type, int property, |
| 261 | 219 | int flags) { |
| 262 | MusicSource *source = SP->source; | |
| 263 | 261 | |
| 264 | 219 | int compare (_Result *a_r, _Result *b_r) { |
| 265 | Entry *a = music_source_query_entry(source, type, a_r->id, "musicsourceview::sort-id-list"), | |
| 266 | *b = music_source_query_entry(source, type, b_r->id, "musicsourceview::sort-id-list"); | |
| 267 | 185 | g_return_val_if_fail (a!=NULL, 0); |
| 268 | g_return_val_if_fail (b!=NULL, 0); | |
| 269 | int result = entry_compare_property(a, b, property); | |
| 270 | 453 | reading_trace (4, "compare: %s %i = %s, %s %i = %s: %i.\n", ENTRY_PF(a), |
| 271 | entry_property_to_string (a, property), ENTRY_PF(b), | |
| 272 | entry_property_to_string (b, property), result); | |
| 273 | 185 | entry_unref (a, "musicsourceview::sort-id-list"); |
| 274 | entry_unref (b, "musicsourceview::sort-id-list"); | |
| 275 | 261 | |
| 276 | 185 | if (flags & VIEW_CONFIG_NODE_SORT_DESCENDING) |
| 277 | return -result; | |
| 278 | return result; | |
| 279 | }; | |
| 280 | 261 | |
| 281 | 453 | reading_trace (3, "_sort_result_list: %s.%s\n", |
| 282 | entry_type_name[type], schema[type][property].name); | |
| 283 | ||
| 284 | 185 | // Implemented as quicksort. |
| 285 | 453 | GSList *result = g_slist_sort(list, (GCompareFunc)compare); |
| 286 | reading_trace (4, "\n"); | |
| 287 | return result; | |
| 288 | 185 | }; |
| 289 | ||
| 290 | 217 | // Finds entries of type described in prev->next_clnode, connected by prev->id |
| 291 | 227 | static GSList *_query_join (MusicSourceView *self, ViewNode *node, _Result *prev) { |
| 292 | 216 | GSList *result; |
| 293 | ||
| 294 | // Finding entries that link to the previous node (most common case). | |
| 295 | 261 | GList *clnode = prev->id==-1? node->head: prev->clnode->next; |
| 296 | //GList *clnode = prev->clnode->next; | |
| 297 | 227 | ViewConfigNode *cnode = CONFIG_NODE(clnode); |
| 298 | 261 | |
| 299 | 216 | if (APID_GET_TYPE(cnode->relation_apid)==cnode->type) { |
| 300 | 261 | // 1:many join - this is easy, we just find all entries with predecessor_id |
| 301 | // as property rel_apid. | |
| 302 | 216 | GSList *result_ids = music_source_query_entry_children_ids |
| 303 | 261 | (SP->source, prev->id, cnode->type, |
| 304 | 216 | APID_GET_PROPERTY_ID(cnode->relation_apid)); |
| 305 | 384 | result = _id_list_to_result_list(result_ids, clnode); |
| 306 | 261 | |
| 307 | } else if (clnode->prev!=NULL | |
| 308 | 216 | && APID_GET_TYPE(cnode->relation_apid)==CONFIG_NODE(clnode->prev)->type) { |
| 309 | // many:1 join - in this case, things are harder. eg. track:recording - here | |
| 310 | // predecessor_id is the track id, config_node->type=recording, but | |
| 311 | // rel_apid=track:recording_id - so we can't use query_entry_children_ids. | |
| 312 | 217 | g_warn_if_fail (cnode->flags & VIEW_CONFIG_NODE_MANY_TO_ONE_JOIN); |
| 313 | 261 | |
| 314 | 216 | // Find the id of the entry we want, by looking up rel_apid in |
| 315 | // the predecessor entry. | |
| 316 | _Result *r = g_slice_new0(_Result); | |
| 317 | 261 | Entry *predecessor = music_source_query_entry(SP->source, |
| 318 | APID_GET_TYPE(cnode->relation_apid), | |
| 319 | 216 | prev->id, "musicsourceview"); |
| 320 | 261 | r->clnode = clnode; |
| 321 | r->id = ((Entry *)entry_get_property(predecessor, | |
| 322 | 216 | APID_GET_PROPERTY_ID(cnode->relation_apid)))->id; |
| 323 | 261 | |
| 324 | 220 | // It isn't done that way any more. |
| 325 | // The new intermediate id is added to result. The existing intermediate ids are copied in | |
| 326 | // afterwards by the code in append_entries_step. | |
| 327 | r->intermediate_ids = g_new0(int, cnode->level_break_count); | |
| 328 | r->intermediate_n = prev->intermediate_n; | |
| 329 | r->intermediate_ids[r->intermediate_n++] = prev->id; | |
| 330 | 261 | |
| 331 | 216 | entry_unref (predecessor, "musicsourceview"); |
| 332 | result = g_slist_append(NULL, r); | |
| 333 | 351 | } else { |
| 334 | 261 | g_warning ("Don't know how to query id's of %ss on property %s.%s :(\n", |
| 335 | 227 | entry_type_name[cnode->type], APID_NAME(cnode->relation_apid)); |
| 336 | 351 | result = NULL; |
| 337 | }; | |
| 338 | 216 | |
| 339 | 261 | return result; |
| 340 | }; | |
| 341 | 216 | |
| 342 | 185 | // Utils for append_entries. |
| 343 | // The strategy here is very simplistic. Its job is to identify all entries | |
| 344 | 261 | // of config_node->type which are not referenced by the foreign key |
| 345 | 185 | // config_node->relation_apid. |
| 346 | 217 | static GSList *query_orphans(MusicSourceView *self, GList *source_clnode, GList *dest_clnode, |
| 347 | _Result *prev) { | |
| 348 | 261 | g_return_val_if_fail (source_clnode!=NULL, NULL); |
| 349 | g_return_val_if_fail (dest_clnode!=NULL, NULL); | |
| 350 | ||
| 351 | 185 | GSList *result = NULL; |
| 352 | 217 | ViewConfigNode *source_cnode = CONFIG_NODE(source_clnode), |
| 353 | *dest_cnode = CONFIG_NODE(dest_clnode); | |
| 354 | 261 | |
| 355 | 185 | // FIXME: this must be glacially slow. |
| 356 | // FIXME: this would be much (well, a bit) faster done within the source. | |
| 357 | ||
| 358 | 217 | int orphan_type = source_cnode->type; |
| 359 | int parent_type = APID_GET_TYPE(source_cnode->relation_apid); | |
| 360 | 185 | |
| 361 | int orphan_highest_id = 0, parent_highest_id = 0; | |
| 362 | music_source_get_n_entries (SP->source, orphan_type, &orphan_highest_id, NULL); | |
| 363 | music_source_get_n_entries (SP->source, parent_type, &parent_highest_id, NULL); | |
| 364 | char flag[orphan_highest_id]; memset(flag, 0, orphan_highest_id); | |
| 365 | 261 | |
| 366 | 185 | // FIXME: the most obvious optimisation I can think of is to work out the |
| 367 | // flags once per fill_node call - if the orphanage isn't the root then we | |
| 368 | 261 | // can execute this code a lot. |
| 369 | 185 | for (int i=1; i<=parent_highest_id; i++) { |
| 370 | 261 | Entry *parent_entry = music_source_query_entry(SP->source, parent_type, i, |
| 371 | 217 | "musicsourceview"); |
| 372 | 185 | if (parent_entry!=NULL) { |
| 373 | 261 | Entry *child = entry_get_property(parent_entry, |
| 374 | 217 | APID_GET_PROPERTY_ID(source_cnode->relation_apid)); |
| 375 | 216 | flag[(child->id)-1] = TRUE; |
| 376 | 185 | entry_unref (parent_entry, "musicsourceview"); |
| 377 | 261 | }; |
| 378 | 185 | }; |
| 379 | 261 | |
| 380 | 185 | for (int i=1; i<=orphan_highest_id; i++) { |
| 381 | if (flag[i-1]==FALSE && music_source_is_valid_id(SP->source, orphan_type, i)) { | |
| 382 | _Result *r = g_slice_new0(_Result); | |
| 383 | 216 | if (prev->id!=-1) { |
| 384 | // Orphanage isn't the root. | |
| 385 | 217 | //g_return_val_if_fail (dest_clnode->prev!=NULL, NULL); |
| 386 | 185 | |
| 387 | 261 | // This code is similar to below. FIXME: but even less |
| 388 | 185 | // efficient. It performs a select on the result set on |
| 389 | // orphan_relation_apid = predecessor_id. | |
| 390 | 220 | //printf ("cnode rel apid: %s.%s, cnode type %s.\n", APID_NAME(dest_cnode->relation_apid), |
| 391 | // entry_type_name[dest_cnode->type]); | |
| 392 | 217 | if (APID_GET_TYPE(dest_cnode->relation_apid) == dest_cnode->type) { |
| 393 | 261 | Entry *entry = music_source_query_entry(SP->source, orphan_type, i, |
| 394 | 216 | "musicsourceview"), |
| 395 | 217 | *foreign_entry = entry_get_property |
| 396 | 261 | (entry, |
| 397 | 217 | APID_GET_PROPERTY_ID(dest_cnode->relation_apid)); |
| 398 | 185 | int foreign_id = foreign_entry->id; |
| 399 | entry_unref (entry, "musicsourceview"); | |
| 400 | if (foreign_id!=prev->id) | |
| 401 | continue; | |
| 402 | } else { | |
| 403 | 261 | g_warning ("config@%s: Orphan relation can't be many:1 (it is %s.%s)", |
| 404 | entry_type_name[dest_cnode->type], | |
| 405 | 217 | APID_NAME(dest_cnode->relation_apid)); |
| 406 | 185 | continue; |
| 407 | } | |
| 408 | } | |
| 409 | 227 | r->clnode = dest_clnode; |
| 410 | 185 | r->id = i; |
| 411 | result = g_slist_prepend(result, r); | |
| 412 | } | |
| 413 | 261 | } |
| 414 | return result; | |
| 415 | 185 | }; |
| 416 | ||
| 417 | 217 | // Find all entries regarding 'prev'. Returns a list of result lists, where each result list |
| 418 | // shares its next_clnode. | |
| 419 | 227 | static GSList *_append_entries_step (MusicSourceView *self, ViewNode *node, _Result *prev) { |
| 420 | GSList *result_set = NULL, *ids; | |
| 421 | 261 | |
| 422 | GList *clnode = prev->id==-1? node->head: prev->clnode->next; | |
| 423 | 216 | ViewConfigNode *cnode = CONFIG_NODE(clnode); |
| 424 | 261 | |
| 425 | 434 | reading_trace (2, "On %s [%s] .. %s.%s = %i. Intermediate n %i, sort group %i\n", |
| 426 | CONFIG_TYPE_NAME(clnode), cnode->flat? "flat": "branching", | |
| 427 | APID_NAME(cnode->relation_apid), prev->id, prev->intermediate_n, | |
| 428 | prev->sort_group); | |
| 429 | 185 | |
| 430 | 216 | /* The correct way is: if current node branches, add for prev and then add for orphans regarding |
| 431 | * the orphaning join prev's branch references. */ | |
| 432 | ||
| 433 | 434 | if (prev->id==-1) { |
| 434 | 185 | // At the head of the top level, looking for all entries of this type. |
| 435 | 227 | ids = _id_list_to_result_list(music_source_query_ids(SP->source, cnode->type), clnode); |
| 436 | 185 | } else { |
| 437 | 227 | ids = _query_join (self, node, prev); |
| 438 | 216 | }; |
| 439 | 217 | |
| 440 | 227 | if (ids!=NULL) |
| 441 | result_set = g_slist_append(result_set, ids); | |
| 442 | 261 | |
| 443 | 216 | if (cnode->orphan_branch!=NULL) { |
| 444 | 383 | int branch_n = CONFIG_NODE(cnode->orphan_branch)->branch_n; |
| 445 | ids = query_orphans(self, PP->config->orphaning_join_node[branch_n-2], | |
| 446 | 223 | cnode->orphan_branch, prev); |
| 447 | 227 | if (ids!=NULL) |
| 448 | result_set = g_slist_append (result_set, ids); | |
| 449 | 185 | }; |
| 450 | ||
| 451 | 220 | // Propogate intermediate ids to new results. |
| 452 | 434 | reading_trace (3, "...Prev [id %i] n %i, type %s, ids %x.\n", prev->id, prev->intermediate_n, |
| 453 | CONFIG_TYPE_NAME(prev->clnode), prev->intermediate_ids); | |
| 454 | 219 | if (prev->intermediate_n > 0) { |
| 455 | 185 | int *int_ids = prev->intermediate_ids; |
| 456 | 261 | for (GSList *result_set_node=result_set; result_set_node; |
| 457 | 227 | result_set_node=result_set_node->next) |
| 458 | for (GSList *n = result_set_node->data; n; n=n->next) { | |
| 459 | 220 | _Result *r = n->data; |
| 460 | ||
| 461 | if (r->intermediate_ids==NULL) { | |
| 462 | g_warn_if_fail (r->intermediate_n==0); | |
| 463 | r->intermediate_n = prev->intermediate_n; | |
| 464 | ||
| 465 | if (prev->intermediate_ids!=NULL) { | |
| 466 | // Reuse the previous array for the first result without an existing one | |
| 467 | 261 | r->intermediate_ids = int_ids; |
| 468 | 220 | prev->intermediate_ids = NULL; |
| 469 | continue; | |
| 470 | } else | |
| 471 | r->intermediate_ids = g_new0(int, cnode->level_break_count); | |
| 472 | } else | |
| 473 | g_warn_if_fail (r->intermediate_n==prev->intermediate_n+1); | |
| 474 | 261 | |
| 475 | 220 | memcpy (r->intermediate_ids, int_ids, prev->intermediate_n * sizeof(int)); |
| 476 | 185 | }; |
| 477 | 261 | |
| 478 | 220 | if (prev->intermediate_ids!=NULL) { |
| 479 | 261 | // Array was not reused - possible if there are no results, or a m:1 join with no |
| 480 | 220 | // orphans (because m:1 results already have an intermediate ids array to store the new |
| 481 | // intermediate id created by that m:1 join). | |
| 482 | g_free (prev->intermediate_ids); | |
| 483 | prev->intermediate_ids = NULL; | |
| 484 | 185 | }; |
| 485 | 261 | }; |
| 486 | ||
| 487 | 227 | return result_set; |
| 488 | 185 | }; |
| 489 | ||
| 490 | // This function takes the current node's results. If sort grouping is needed, it swallows them | |
| 491 | 217 | // until the group is complete and then returns the whole sorted group. It only should be used |
| 492 | // on result lists of all one type (next_clnode always the same). | |
| 493 | 185 | // |
| 494 | // Each _Result has a sort group property. This should be 0 for no sort group, or > 0. | |
| 495 | // | |
| 496 | 261 | static GSList *_append_entries_group_sorts (MusicSourceView *self, GSList *pids_node, |
| 497 | GSList *ungrouped, GSList **sort_group, | |
| 498 | 217 | int *current_sort_group_id) { |
| 499 | 227 | _Result *prev = pids_node->data; |
| 500 | 261 | |
| 501 | // 'next_clnode' should be constant throughout the result list - this is the reason for | |
| 502 | 227 | // _append_entries_step returning a set of lists, one for each branch, rather than combining all |
| 503 | // the results together. | |
| 504 | ViewConfigNode *cnode = ((_Result *)ungrouped->data)->clnode->data; | |
| 505 | 261 | |
| 506 | 434 | reading_trace (3, "group_sorts: cnode %s, prev sg %i. %x, %x\n", entry_type_name[cnode->type], |
| 507 | prev->sort_group, prev->clnode, | |
| 508 | pids_node->next? ((_Result *)pids_node->next->data)->clnode: 0); | |
| 509 | 261 | |
| 510 | GSList *grouped = ungrouped; | |
| 511 | 185 | if (prev->sort_group==0) { |
| 512 | // Previous node didn't need sort grouping. If this node is sorted, sort it. If not, | |
| 513 | // begin a new sort group for each entry of this node. | |
| 514 | // | |
| 515 | 217 | if (cnode->sort_property_id >= 0) |
| 516 | 261 | grouped = _sort_result_list(self, ungrouped, cnode->type, |
| 517 | 219 | cnode->sort_property_id, cnode->flags); |
| 518 | 185 | else // Create new sort group. |
| 519 | 261 | for (GSList *n=ungrouped; n; n=n->next) { |
| 520 | _Result *r = n->data; | |
| 521 | 185 | r->sort_group = prev->id; |
| 522 | }; | |
| 523 | 261 | |
| 524 | 185 | } else { |
| 525 | // Previous node was sort grouped! If this node is unsorted too, propogate the sort | |
| 526 | // group; if it is sorted, add to the current sort group or start a new one. | |
| 527 | // | |
| 528 | 261 | if (cnode->sort_property_id==-1) { |
| 529 | 221 | // We are in the middle of an unsorted range, eg. |
| 530 | // artist[name]:composition:recording:file[path] | |
| 531 | // ^^^ or ^^^ | |
| 532 | 261 | // Here we just continue the sort groups from the previous node. |
| 533 | 221 | // |
| 534 | 185 | for (GSList *n=ungrouped; n; n=n->next) { |
| 535 | _Result *r = n->data; | |
| 536 | r->sort_group = prev->sort_group; | |
| 537 | 261 | }; |
| 538 | } else { | |
| 539 | // We are at the end of un unsorted range, eg. | |
| 540 | 221 | // artist[name]:composition:recording:file[path] |
| 541 | // ^^^ | |
| 542 | 229 | // The current group is ended when |
| 543 | // a) prev->sort_group (in this example, the artist id) changes | |
| 544 | // b) pids_node->next==NULL (in the example, at the recording result) | |
| 545 | // c) pids_node->clnode==next pids_node->clnode | |
| 546 | const gboolean last_group = (pids_node->next==NULL || | |
| 547 | prev->clnode!=((_Result *)pids_node->next->data)->clnode); | |
| 548 | if (*current_sort_group_id==prev->sort_group && !last_group) { | |
| 549 | 221 | // Add this set of results to the current sort group, don't sort anything yet. |
| 550 | 261 | // |
| 551 | 221 | *sort_group = g_slist_concat(*sort_group, ungrouped); |
| 552 | 453 | reading_trace (4, "added %i results to current group %i, new length %i.\n", |
| 553 | g_slist_length (ungrouped), *current_sort_group_id, | |
| 554 | g_slist_length (*sort_group)); | |
| 555 | 221 | grouped = NULL; |
| 556 | } else { | |
| 557 | // Sort current sort group ('sort_group'), and start a new one ('ungrouped'). This | |
| 558 | // code path is also called on the last result, because the sort group needs to be | |
| 559 | // emptied. | |
| 560 | 261 | // |
| 561 | 453 | if (last_group && *current_sort_group_id==prev->sort_group) { |
| 562 | 185 | *sort_group = g_slist_concat(*sort_group, ungrouped); |
| 563 | ungrouped = NULL; | |
| 564 | }; | |
| 565 | 261 | |
| 566 | 221 | g_return_val_if_fail (cnode!=NULL, NULL); |
| 567 | 453 | reading_trace (4, "sorted and closing group %i, length %i.\n", |
| 568 | *current_sort_group_id, g_slist_length (*sort_group)); | |
| 569 | 261 | grouped = _sort_result_list(self, *sort_group, cnode->type, |
| 570 | 453 | cnode->sort_property_id, cnode->flags); |
| 571 | ||
| 572 | if (last_group && ungrouped != NULL) { | |
| 573 | // We won't get called again, so sort the last group now too. | |
| 574 | ungrouped = _sort_result_list(self, ungrouped, cnode->type, | |
| 575 | cnode->sort_property_id, cnode->flags); | |
| 576 | grouped = g_slist_concat (grouped, ungrouped); | |
| 577 | ungrouped = NULL; | |
| 578 | } | |
| 579 | 221 | |
| 580 | 185 | *sort_group = ungrouped; |
| 581 | *current_sort_group_id = prev->sort_group; | |
| 582 | 261 | }; |
| 583 | 185 | } |
| 584 | 453 | reading_trace (4, "\n"); |
| 585 | 185 | } |
| 586 | return grouped; | |
| 587 | }; | |
| 588 | ||
| 589 | 269 | /* _fill_node: the main function which populates 'node'. */ |
| 590 | 215 | static void _fill_node(MusicSourceView *self, ViewNode *node) { |
| 591 | 269 | // Shouldn't have been read already. |
| 592 | g_return_if_fail (node->n_children==-1 && node->e_children==-1); | |
| 593 | 434 | reading_trace (1, "fill_node: %x [%i]\n", (guint)node, node->depth); |
| 594 | 261 | |
| 595 | 229 | // Leaf nodes have head=NULL, but we should never be asked to fill one. |
| 596 | 216 | g_return_if_fail (node->head!=NULL); |
| 597 | 261 | g_return_if_fail (node->tail!=NULL); |
| 598 | ||
| 599 | 215 | node->n_children = node->e_children = 0; |
| 600 | ||
| 601 | 185 | // Our method is essentially to iterate through previous ids, and at each node, find each |
| 602 | // entry at the next level and store this in ids. previous_ids then becomes ids, and the | |
| 603 | // cycle repeats. sort_group is used to lookback so eg. release[name]:track:recording[name] | |
| 604 | // doesn't group recordings arbitrarily by track. | |
| 605 | // | |
| 606 | 261 | GSList *ids = NULL, *sort_group; |
| 607 | ||
| 608 | // For this function only, id of -1 indicates we're at the root node. clnode is NULL for the | |
| 609 | 227 | // root node but is special cased where necessary to node->head. |
| 610 | 185 | _Result *seed = g_slice_new0(_Result); |
| 611 | 227 | seed->id = (node==&SP->root? -1: node->id); |
| 612 | seed->clnode = node->head->prev; | |
| 613 | 261 | |
| 614 | 185 | GSList *previous_ids = g_slist_append(NULL, seed); |
| 615 | ||
| 616 | // Iterate through configuration. We go node by node through the whole of the level, | |
| 617 | 269 | // ie. from one > join to the next. The nodes are actually added after the loop completes. |
| 618 | 185 | // |
| 619 | while (1) { | |
| 620 | 261 | |
| 621 | 220 | //printf("\n\n---------------\n"); |
| 622 | 215 | |
| 623 | 185 | // Iterate through the results of the previous node ('previous_ids') and store the new |
| 624 | // info into 'ids'. | |
| 625 | 229 | gboolean finished = TRUE; |
| 626 | 350 | sort_group = NULL; |
| 627 | 231 | |
| 628 | GSList *pids_node = previous_ids; | |
| 629 | if (pids_node==NULL) | |
| 630 | goto empty; | |
| 631 | //g_return_if_fail (pids_node!=NULL); | |
| 632 | ||
| 633 | 351 | int current_sort_group; |
| 634 | 261 | current_sort_group = ((_Result *)pids_node->data)->sort_group; |
| 635 | while (pids_node!=NULL) { | |
| 636 | // Query all the entries across prev's join. result_set contains a *list* of result | |
| 637 | 227 | // lists. Each result list has a common next_clnode (ie. is of the same branch). So for |
| 638 | 261 | // example, at the second node of 'artist:(album ..)/(recording ..)' result_list will |
| 639 | // contain a GSList of _Result entries of type 'album', and then a GSList of _Result | |
| 640 | 227 | // entries of type 'recording'. |
| 641 | // | |
| 642 | 261 | _Result *prev = pids_node->data; |
| 643 | ||
| 644 | // Some branches may finish before others. The results still need to be propogated so | |
| 645 | // that ordering is preserved. | |
| 646 | 229 | if (!prev->complete) { |
| 647 | finished = FALSE; | |
| 648 | GSList *result_set = _append_entries_step(self, node, prev); | |
| 649 | 261 | |
| 650 | 229 | for (GSList *result_set_node=result_set; result_set_node; |
| 651 | 261 | result_set_node=result_set_node->next) { |
| 652 | ||
| 653 | 269 | // If there are no results of a certain type, it's wrong to return a list node |
| 654 | // of NULL in the result set - should be no entry at all. | |
| 655 | 229 | g_warn_if_fail (result_set_node->data!=NULL); |
| 656 | 261 | |
| 657 | 434 | #if defined(GENERIC_VIEW_TRACE_READING) && GENERIC_VIEW_TRACE_READING > 3 |
| 658 | for (GSList *n=result_set_node->data; n; n=n->next) { | |
| 659 | 229 | _Result *r = n->data; |
| 660 | 434 | reading_trace (4, "prev %i[%s]: cnode %s. Result id: %i, int %x. Sort group %i\n", |
| 661 | 229 | prev->id, prev->clnode==NULL?"":CONFIG_TYPE_NAME(prev->clnode), |
| 662 | 261 | r->clnode->next?CONFIG_TYPE_NAME(r->clnode):"end", r->id, |
| 663 | 229 | (guint)r->intermediate_ids, r->sort_group); |
| 664 | } | |
| 665 | 434 | #endif |
| 666 | 217 | |
| 667 | 229 | GSList *sorted = _append_entries_group_sorts(self, pids_node, result_set_node->data, |
| 668 | &sort_group, ¤t_sort_group); | |
| 669 | 261 | |
| 670 | 229 | if (sorted!=NULL) { |
| 671 | // Determine if this is the type we want or if we need to step further. | |
| 672 | GList *next_clnode = ((_Result *)sorted->data)->clnode->next; | |
| 673 | if (next_clnode==NULL || !CONFIG_NODE(next_clnode->prev)->flat) | |
| 674 | for (GSList *n=sorted; n; n=n->next) | |
| 675 | ((_Result *)n->data)->complete = TRUE; | |
| 676 | 261 | |
| 677 | 217 | ids = g_slist_concat(ids, sorted); |
| 678 | }; | |
| 679 | }; | |
| 680 | 229 | g_warn_if_fail (prev->intermediate_ids==NULL); // Should have been freed or passed on. |
| 681 | 261 | |
| 682 | 229 | g_slice_free (_Result, prev); |
| 683 | } else { | |
| 684 | ids = g_slist_append(ids, prev); | |
| 685 | 216 | }; |
| 686 | 229 | |
| 687 | 185 | pids_node = pids_node->next; |
| 688 | }; | |
| 689 | 261 | |
| 690 | g_slist_free (previous_ids); | |
| 691 | ||
| 692 | empty: | |
| 693 | 229 | if (sort_group!=NULL) |
| 694 | 261 | g_warning ("_fill_node: Still in sort group: %i result(s)\n", |
| 695 | 229 | g_slist_length(sort_group)); |
| 696 | /*for (GSList *sg_node=sort_group; sg_node; sg_node=sg_node->next) { | |
| 697 | _Result *r = sg_node->data; | |
| 698 | 261 | printf ("\t%s %i.\n", CONFIG_TYPE_NAME(r->clnode), r->id); fflush (stdout); |
| 699 | 229 | };*/ |
| 700 | 261 | |
| 701 | ||
| 702 | 216 | //if (!cnode->flat) |
| 703 | 261 | // break; |
| 704 | 216 | // |
| 705 | 229 | if (finished) |
| 706 | 185 | break; |
| 707 | previous_ids = ids; ids = NULL; | |
| 708 | }; | |
| 709 | 261 | |
| 710 | 269 | // Add the entries we found to the node in pages. All the _e numbers are set to the same as the |
| 711 | // _n's because we have no need to estimate, we just queried everything. | |
| 712 | 185 | // |
| 713 | 269 | int n_children = g_slist_length(ids); |
| 714 | 261 | |
| 715 | 216 | ViewPage *page = node->children; |
| 716 | 185 | if (page==NULL) { |
| 717 | 216 | node->children = page = _music_source_view_new_page(node, NULL, NULL); |
| 718 | page->start_e = 0; page->start_n = 0; | |
| 719 | } else while (page->next!=NULL) page = page->next; | |
| 720 | 185 | |
| 721 | 216 | int page_i = page->start_n + page->size; |
| 722 | 185 | |
| 723 | for (GSList *result_node=ids; result_node; result_node=result_node->next) { | |
| 724 | _Result *r = result_node->data; | |
| 725 | 216 | if (page_i > MUSIC_SOURCE_VIEW_MIN_PAGE_SIZE) { |
| 726 | page->chain_next = TRUE; | |
| 727 | page->next = _music_source_view_new_page(node, page, NULL); | |
| 728 | page->next->start_e = page->next->start_n = page->start_n+page->size; | |
| 729 | page = page->next; | |
| 730 | page_i = 0; | |
| 731 | 185 | }; |
| 732 | 261 | |
| 733 | 434 | reading_trace (2, "Adding %s %i.\n\n", CONFIG_TYPE_NAME(r->clnode), r->id); fflush (stdout); |
| 734 | 228 | ViewNode *child = _music_source_view_new_node(node->depth+1, r->clnode, NULL, NULL); |
| 735 | 216 | child->parent_page = page; |
| 736 | child->n = page_i; | |
| 737 | 261 | child->id = r->id; |
| 738 | 216 | g_array_append_val (page->nodes, child); |
| 739 | 185 | |
| 740 | //printf ("n %i id %i ints %x\n", page_i, r->id, (guint)r->intermediate_ids); fflush (stdout); | |
| 741 | if (r->intermediate_ids!=NULL) { | |
| 742 | //printf("Setting int sot %x - %i\n", r->intermediate_ids, r->intermediate_ids[0]);fflush(stdout); | |
| 743 | child->intermediate_ids = r->intermediate_ids; | |
| 744 | }; | |
| 745 | ||
| 746 | page->size++; page_i++; | |
| 747 | 269 | |
| 748 | // 'Unknown Artist' and 'Various Artists' are persistant entries. Make sure we only show | |
| 749 | // them if they have any children. | |
| 750 | if (CONFIG_NODE(r->clnode)->type==ENTRY_TYPE_ARTIST && (r->id==1 || r->id==2)) { | |
| 751 | if (r->clnode->next==NULL) | |
| 752 | // This config is just a list of artists - strange, but no reason to disallow it | |
| 753 | break; | |
| 754 | ||
| 755 | _fill_node (self, child); | |
| 756 | g_warn_if_fail (child->n_children >= 0); | |
| 757 | if (child->n_children==0) { | |
| 758 | // Empty - remove the node again. | |
| 759 | g_array_remove_index (page->nodes, --page_i); | |
| 760 | page->size--; n_children--; | |
| 761 | }; | |
| 762 | }; | |
| 763 | ||
| 764 | 185 | g_slice_free (_Result, r); |
| 765 | }; | |
| 766 | 261 | |
| 767 | 269 | node->n_children = n_children; node->e_children = n_children; |
| 768 | 185 | g_slist_free(ids); |
| 769 | 215 | |
| 770 | 220 | //printf ("n children: %i.\n", node->n_children); fflush (stdout); |
| 771 | 215 | |
| 772 | 185 | //gtk_tree_path_free(path_base); |
| 773 | ||
| 774 | #ifdef CHECK_TREE | |
| 775 | music_source_view_check_tree(self); | |
| 776 | #endif | |
| 777 | }; | |
| 778 | ||
| 779 | ||
| 780 | ///////////////////////////////////////////////////////// | |
| 781 | // Internal musicsourceview routines ! | |
| 782 | // | |
| 783 | ||
| 784 | // Read the entire source - it's probably empty anyway, and we assume in-memory access since | |
| 785 | // it's not a library. | |
| 786 | // | |
| 787 | 209 | static void initial_read (MusicSourceView *self, ViewNode *node) { |
| 788 | 261 | g_return_if_fail (node!=NULL); |
| 789 | 209 | g_return_if_fail (node->e_children==-1); // Should be uninitialised |
| 790 | 261 | |
| 791 | 216 | // if (SP->config_info->n_levels==0) return; |
| 792 | 261 | |
| 793 | 209 | _fill_node (self, node); |
| 794 | 185 | }; |
| 795 | ||
| 796 | 261 | static ViewPage *read_page_at (MusicSourceView *music_source_view, ViewPage *head, |
| 797 | 209 | ViewPage *tail, int *e) { |
| 798 | g_warn_if_reached (); | |
| 799 | 185 | return NULL; |
| 800 | }; | |
| 801 | ||
| 802 | 209 | static void read_next_page (MusicSourceView *music_source_view, ViewPage *page) { |
| 803 | 261 | g_warn_if_reached (); |
| 804 | 185 | }; |
| 805 | ||
| 806 | ||
| 807 | 362 | /*************************************************************************************************** |
| 808 | * View editing methods | |
| 809 | 369 | * |
| 810 | * Some assumptions central to this code: | |
| 811 | * 1. if there is a change at a certain horizontal position across a row, the only possible | |
| 812 | * change in the view is the position of that range - not its length. | |
| 813 | 362 | */ |
| 814 | ||
| 815 | 367 | typedef enum { |
| 816 | INSERT, CHANGE_FIND_NEW, | |
| 817 | CHANGE_FIND_OLD, REMOVE | |
| 818 | } RowAlterationType; | |
| 819 | ||
| 820 | typedef struct { | |
| 821 | RowAlterationType type; | |
| 822 | GList *clnode; Entry *entry; | |
| 823 | ViewNode *parent_node; | |
| 824 | ||
| 825 | /* Only used by INSERT */ | |
| 826 | int *intermediate_ids; int intermediate_n; | |
| 827 | 369 | |
| 828 | 367 | /* Created on CHANGE_FIND_OLD & used by CHANGE_FIND_NEW */ |
| 829 | 369 | GSList **deltas; |
| 830 | 367 | } RowAlteration; |
| 831 | ||
| 832 | /* Change notification is actioned in two steps. The first step locates the existing rows for | |
| 833 | 378 | * old_entry, and returns a list of RowDeltas (one for each row). The rows for all of the deltas are |
| 834 | * then removed from the node, to avoid confusing locate_entry on the second pass. | |
| 835 | 380 | * |
| 836 | * The second step searches for where new_entry should go, finds the deltas in the list (based on | |
| 837 | * entry id and intermediate ids) and sets new_n accordingly. The rows are reinserted at the new | |
| 838 | 378 | * positions. From this, it's possible to emit rows-reordered and row-changed appropriate. |
| 839 | 374 | * |
| 840 | * We have to first find all the deltas and then process them all, because the change in the | |
| 841 | * entry could have been something drastic involving m:1 joins. */ | |
| 842 | 367 | typedef struct { |
| 843 | 378 | ViewNode *node; |
| 844 | 374 | |
| 845 | /* Extent of the delta in its original position */ | |
| 846 | 378 | int original_n, new_n; |
| 847 | 367 | } RowDelta; |
| 848 | ||
| 849 | ||
| 850 | // FIXME: merge these two into process_insert/process_remove ? | |
| 851 | 362 | /* _insert_row_in_page: Insert a child in an existing page, pushing around other nodes as |
| 852 | * appropriate. Note that we assume the page is queried already - in practice, | |
| 853 | * it will have by necessity have been read already to find where to insert. */ | |
| 854 | 439 | void _insert_row_in_page (MusicSourceView *self, ViewNode *parent_node, ViewPage *page, |
| 855 | int page_i, ViewNode *node, gboolean signal) { | |
| 856 | 362 | g_return_if_fail (page_i <= page->size); |
| 857 | ||
| 858 | node->parent_page = page; | |
| 859 | 185 | node->n = page_i; |
| 860 | 261 | |
| 861 | 385 | g_return_if_fail (page->size == page->nodes->len); |
| 862 | 362 | g_array_insert_val (page->nodes, page_i, node); |
| 863 | 261 | |
| 864 | 185 | // Renumber subsequent nodes |
| 865 | 362 | for (int i=page_i+1; i<page->size+1; i++) { |
| 866 | ViewNode *n = g_array_index(page->nodes, ViewNode *, i); | |
| 867 | 385 | n->n ++; |
| 868 | 185 | //g_warn_if_fail (n->n==i+1); |
| 869 | }; | |
| 870 | 261 | |
| 871 | 185 | // Move subsequent pages down. |
| 872 | 362 | for (ViewPage *p=page->next; p; p=p->next) { |
| 873 | 261 | p->start_e++; |
| 874 | 208 | if (p->start_n > -1) |
| 875 | 185 | p->start_n++; |
| 876 | }; | |
| 877 | ||
| 878 | parent_node->e_children++; | |
| 879 | if (parent_node->n_children!=-1) | |
| 880 | parent_node->n_children++; | |
| 881 | ||
| 882 | 362 | page->size++; |
| 883 | 261 | |
| 884 | 362 | ROW_MARKERS_ROW_INSERTED (MUSIC_SOURCE_VIEW(self), parent_node, page_i+page->start_e); |
| 885 | 261 | |
| 886 | 408 | if (signal) { |
| 887 | 419 | // signal is only false when the row is being inserted because it was reordered. |
| 888 | ||
| 889 | 408 | GtkTreeIter iter; |
| 890 | iter.stamp = 0; | |
| 891 | iter.user_data = parent_node; | |
| 892 | iter.user_data2 = page; | |
| 893 | iter.user_data3 = GINT_TO_POINTER(page_i); | |
| 894 | // FIXME: we could have built a path along the way | |
| 895 | GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(self), &iter); | |
| 896 | change_trace ("EMIT: row_inserted @ page_i %i\n", page_i); | |
| 897 | gtk_tree_model_row_inserted(GTK_TREE_MODEL(self), path, &iter); | |
| 898 | 419 | |
| 899 | 420 | if (parent_node->n_children==1 && parent_node->parent_page != NULL) { |
| 900 | // Emit row-has-child-toggled, because this is the first child. (Except on the root). | |
| 901 | 419 | GtkTreeIter parent_iter; |
| 902 | gtk_tree_model_iter_parent (GTK_TREE_MODEL(self), &parent_iter, &iter); | |
| 903 | gtk_tree_path_up (path); | |
| 904 | gtk_tree_model_row_has_child_toggled (GTK_TREE_MODEL(self), path, &parent_iter); | |
| 905 | }; | |
| 906 | ||
| 907 | 408 | gtk_tree_path_free(path); |
| 908 | }; | |
| 909 | 185 | }; |
| 910 | ||
| 911 | 362 | /* _remove_row_in_page: Remove row at page_i from page. Assumes parent_node has been read and page |
| 912 | 388 | * is definitely in memory. |
| 913 | 385 | * signal: whether to emit row-changed signal. |
| 914 | * free: whether to free node data. */ | |
| 915 | 362 | void _remove_row_in_page (GenericView *self, ViewNode *parent_node, ViewPage *page, |
| 916 | 385 | int page_i, gboolean signal, gboolean free) { |
| 917 | 269 | g_return_if_fail (page_i >= 0 && page_i < page->size); |
| 918 | ||
| 919 | ViewNode *dead_node = g_array_index(page->nodes, ViewNode *, page_i); | |
| 920 | ||
| 921 | 385 | if (free) |
| 922 | _music_source_view_free_node (dead_node); | |
| 923 | 269 | |
| 924 | page->nodes = g_array_remove_index(page->nodes, page_i); | |
| 925 | page->size--; | |
| 926 | ||
| 927 | // Renumber the rest of the nodes in this page. | |
| 928 | for (int i=page_i; i<page->size; i++) { | |
| 929 | ViewNode *node = g_array_index (page->nodes, ViewNode *, i); | |
| 930 | node->n--; | |
| 931 | }; | |
| 932 | ||
| 933 | // Renumber the remaining pages. | |
| 934 | for (ViewPage *p = page->next; p; p=p->next) { | |
| 935 | p->start_e--; | |
| 936 | if (p->start_n!=-1) | |
| 937 | p->start_n--; | |
| 938 | }; | |
| 939 | ||
| 940 | parent_node->e_children--; | |
| 941 | if (parent_node->n_children!=-1) | |
| 942 | parent_node->n_children--; | |
| 943 | ||
| 944 | if (signal) { | |
| 945 | g_return_if_fail (page->start_e >= 0); | |
| 946 | ||
| 947 | GtkTreeIter iter; | |
| 948 | iter.stamp = 0; | |
| 949 | iter.user_data = parent_node; | |
| 950 | iter.user_data2 = page; | |
| 951 | iter.user_data3 = GINT_TO_POINTER(page_i+page->start_e); | |
| 952 | ||
| 953 | // FIXME: we could have built a path along the way before calling this function in some cases | |
| 954 | GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(self), &iter); | |
| 955 | ||
| 956 | ROW_MARKERS_ROW_DELETED (MUSIC_SOURCE_VIEW(self), parent_node, page_i+page->start_e); | |
| 957 | ||
| 958 | gtk_tree_model_row_deleted(GTK_TREE_MODEL(self), path); | |
| 959 | 422 | |
| 960 | // Don't emit row-has-child-toggled for rows that are about to be deleted. There is no | |
| 961 | // point. We only emit it for rows that are added because otherwise, GtkTreeView doesn't | |
| 962 | // display an expander. | |
| 963 | /*if (parent_node->n_children==0) { | |
| 964 | 419 | // Emit row-has-child-toggled, because this is the last child. |
| 965 | // FIXME: this row should actually be removed now, we should ensure that it is somehow | |
| 966 | // FIXME: And, there's possibly no point emitted this signal, but we do it for | |
| 967 | // consistency at the moment. | |
| 968 | GtkTreeIter parent_iter; | |
| 969 | gtk_tree_model_iter_parent (GTK_TREE_MODEL(self), &parent_iter, &iter); | |
| 970 | gtk_tree_path_up (path); | |
| 971 | 422 | gtk_tree_model_row_has_child_toggled (GTK_TREE_MODEL(self), path, &parent_iter); |
| 972 | };*/ | |
| 973 | 419 | |
| 974 | 269 | gtk_tree_path_free(path); |
| 975 | }; | |
| 976 | 442 | |
| 977 | // 'Special' entries persist and so we have to deal with hiding them when they have no children. | |
| 978 | if (parent_node->parent_tail != NULL | |
| 979 | && ENTRY_IS_SPECIAL(CONFIG_NODE(parent_node->parent_tail)->type, parent_node->id)) | |
| 980 | if (parent_node->e_children == 0) | |
| 981 | _remove_row_in_page (self, parent_node->parent_page->parent_node, | |
| 982 | parent_node->parent_page, parent_node->n, signal, free); | |
| 983 | 269 | }; |
| 984 | ||
| 985 | ||
| 986 | 185 | |
| 987 | // Utility | |
| 988 | // FIXME: put this in entry.c somehow .. | |
| 989 | static void reference_entry_callback(Entry *entry) { | |
| 990 | entry_ref(entry, "walk-config"); | |
| 991 | }; | |
| 992 | ||
| 993 | void dump_test_entry(Entry *entry) { | |
| 994 | if (entry->type==ENTRY_TYPE_FILE) | |
| 995 | 261 | printf("'%s'\n", (const char *)entry_get_property(entry, FILE_PATH)); |
| 996 | 185 | fflush(stdout); |
| 997 | }; | |
| 998 | ||
| 999 | 431 | /* partial_node_match: decide if row 'node' represents 'entry'. |
| 1000 | */ | |
| 1001 | static gboolean partial_node_match (ViewNode *view_node, GList *clnode, Entry *entry, | |
| 1002 | 388 | int *intermediate_ids, int intermediate_n) { |
| 1003 | 431 | g_return_val_if_fail (CONFIG_NODE(clnode)->type == entry->type, FALSE); |
| 1004 | ||
| 1005 | // When clnode is the tail node of a row, we can use the normal match function. | |
| 1006 | // | |
| 1007 | 374 | if (!CONFIG_NODE(clnode)->flat) { |
| 1008 | 431 | if (clnode != view_node->parent_tail) |
| 1009 | 385 | return FALSE; |
| 1010 | 431 | change_trace ("\tpartial_node_match[%s %i]: Matching whole [at cnode %s - vnode n %i id %i].\n", |
| 1011 | ENTRY_PF(entry), CONFIG_TYPE_NAME(clnode), view_node->n, view_node->id); | |
| 1012 | return _music_source_view_match_node(view_node, entry->id, intermediate_ids); | |
| 1013 | 374 | }; |
| 1014 | ||
| 1015 | 385 | // Walk to tail and check with the node's parent tail. |
| 1016 | for (GList *tail_clnode=clnode; tail_clnode; tail_clnode=tail_clnode->next) | |
| 1017 | if (!CONFIG_NODE(tail_clnode)->flat) { | |
| 1018 | 431 | if (tail_clnode!=view_node->parent_tail) return FALSE; |
| 1019 | 385 | break; |
| 1020 | }; | |
| 1021 | ||
| 1022 | 374 | // Match partially on intermediate ids. |
| 1023 | // | |
| 1024 | if (intermediate_n==0) { | |
| 1025 | 431 | change_trace ("\tpartial_node_match: No int ids yet.\n"); |
| 1026 | 374 | return TRUE; |
| 1027 | }; | |
| 1028 | ||
| 1029 | 431 | int *ii = view_node->intermediate_ids, *jj = intermediate_ids; |
| 1030 | 374 | g_warn_if_fail (ii != NULL && jj != NULL); |
| 1031 | for (int i=0; i<intermediate_n; i++) { | |
| 1032 | g_return_val_if_fail (*ii!=0 && *jj!=0, FALSE); | |
| 1033 | if (*ii != *jj) | |
| 1034 | return FALSE; | |
| 1035 | }; | |
| 1036 | return TRUE; | |
| 1037 | }; | |
| 1038 | ||
| 1039 | 362 | |
| 1040 | 365 | static void process_row_insert (GenericView *self, GList *tail_clnode, Entry *tail_entry, |
| 1041 | ViewNode *parent_node, int *intermediate_ids, int intermediate_n, | |
| 1042 | gboolean entry_matched, ViewPage *page, int n_min, int n_max) { | |
| 1043 | 362 | if (entry_matched) { |
| 1044 | // There are a couple of possibilities here (when _locate_entry appears to have | |
| 1045 | // found the entry that we haven't yet added to the view). | |
| 1046 | ||
| 1047 | // 0) This is impossible, since _locate_entry_range should have returned FALSE. | |
| 1048 | g_return_if_fail (n_min < n_max); | |
| 1049 | ||
| 1050 | // 1) Consider "album[name]:release:track:recording:file[path]". Add recording 2 | |
| 1051 | // and file 2, linked to album 1, release 1, track 2. Now add recording 2 and | |
| 1052 | // file 2 again, but on album 2, release 2, track 1. insert-recursive will try to | |
| 1053 | // add file 2 for both album 1 and album 2, and here is where we catch the fact | |
| 1054 | // that file 2 has already been added once. We shouldn't actually have to because | |
| 1055 | // the ignore list should sort the problem out. | |
| 1056 | // | |
| 1057 | // | |
| 1058 | if (n_min == n_max-1) { | |
| 1059 | ViewNode *child_node = g_array_index(page->nodes, ViewNode *, | |
| 1060 | n_min - page->start_e); | |
| 1061 | if (child_node->id == tail_entry->id) { | |
| 1062 | change_trace ("%s %i already added to view in this position.\n", | |
| 1063 | ENTRY_PF(tail_entry)); | |
| 1064 | //printf ("Already added\n"); entry_dump (link_entry); | |
| 1065 | return; | |
| 1066 | }; | |
| 1067 | }; | |
| 1068 | ||
| 1069 | // 2) This level is sorted by a property which isn't very unique, such as | |
| 1070 | // file.bitrate. It's not unlikely that an existing file has the same bitrate as | |
| 1071 | // the one being added. | |
| 1072 | // | |
| 1073 | change_trace ("%s %i fits in range [%i, %i) - appending.\n", ENTRY_PF(tail_entry), | |
| 1074 | n_min, n_max); | |
| 1075 | }; | |
| 1076 | ||
| 1077 | if (n_min!=n_max || page==NULL) { | |
| 1078 | // If nowhere was found, just append ... | |
| 1079 | // FIXME: don't, this is never really a good thing, something must have gone wrong | |
| 1080 | for (page=parent_node->children; page->next!=NULL; page=page->next); | |
| 1081 | n_min = n_max==0? 0: n_max-1; | |
| 1082 | }; | |
| 1083 | ||
| 1084 | /*if (n_min!=n_max) | |
| 1085 | g_warning ("Error finding index for %s %i - got between %i and %i\n", | |
| 1086 | entry_type_name[link_entry->type], link_entry->id, n_min, n_max); | |
| 1087 | g_warn_if_fail (page!=NULL);*/ | |
| 1088 | ||
| 1089 | change_trace ("Inserting node for %s %i at %i, n %i: parent_tail %s.\n\n\n", | |
| 1090 | ENTRY_PF(tail_entry), parent_node->depth, n_min, | |
| 1091 | CONFIG_TYPE_NAME(tail_clnode)); | |
| 1092 | ||
| 1093 | ViewNode *node = _music_source_view_new_node(parent_node->depth+1, tail_clnode, | |
| 1094 | NULL, NULL); | |
| 1095 | node->id = tail_entry->id; | |
| 1096 | node->intermediate_ids = intermediate_ids; | |
| 1097 | ||
| 1098 | _insert_row_in_page (MUSIC_SOURCE_VIEW(self), parent_node, page, n_min - page->start_e, | |
| 1099 | 408 | node, TRUE); |
| 1100 | 362 | }; |
| 1101 | ||
| 1102 | 397 | /* process_row_change_old: create changed deltas from a row range. */ |
| 1103 | 374 | static void process_row_change_old (GenericView *self, RowAlteration *alteration, |
| 1104 | GList *tail_clnode, Entry *tail_entry, gboolean entry_matched, | |
| 1105 | 367 | ViewPage *page, int n_min, int n_max) { |
| 1106 | 380 | change_trace ("process_row_change_old: [%i, %i) tail %s %i\n", n_min, n_max, |
| 1107 | ENTRY_PF(tail_entry)); | |
| 1108 | 388 | |
| 1109 | 431 | if (page == NULL) { |
| 1110 | 388 | page = alteration->parent_node->children; |
| 1111 | 385 | n_min = 0; n_max = alteration->parent_node->e_children; |
| 1112 | }; | |
| 1113 | 388 | |
| 1114 | 431 | // Sanity check |
| 1115 | g_return_if_fail (page->size == page->nodes->len); | |
| 1116 | ||
| 1117 | 374 | // The range supplied could be way too broad. The entry that changed could be anywhere along the |
| 1118 | // config, and if it's in the middle of a lot of unsorted nodes the range will probably be way | |
| 1119 | 431 | // too big. Or everything could be called "Unknown Artist - Unknown Title" at this point. For |
| 1120 | // this reason, we check all of the id's of each node, and only create a delta if it's exactly | |
| 1121 | // what we want. | |
| 1122 | 438 | |
| 1123 | // We don't do this checking if entry_matched is true. This is more than just an optimisation: | |
| 1124 | // on change_old propagation where the trigger isn't the tail we do matching with brute force | |
| 1125 | // instead of the iterative binary search, which means alteration->intermediate_ids might not | |
| 1126 | // be filled properly - thus breaking partial_node_match. | |
| 1127 | g_return_if_fail (!entry_matched || n_min+1==n_max); | |
| 1128 | ||
| 1129 | 374 | for (int n=n_min; n<n_max; n++) { |
| 1130 | // Is current node a horizontal match? We don't use match_node because we only want to match | |
| 1131 | // as far as tail_clnode in the config, which might not be the end .. | |
| 1132 | 431 | g_return_if_fail (n - page->start_e < page->nodes->len); |
| 1133 | ||
| 1134 | 380 | ViewNode *node = g_array_index(page->nodes, ViewNode *, n - page->start_e); |
| 1135 | 431 | g_return_if_fail (node != NULL); |
| 1136 | 374 | |
| 1137 | 438 | if (entry_matched || |
| 1138 | (node->parent_tail == tail_clnode && | |
| 1139 | partial_node_match(node, tail_clnode, tail_entry, alteration->intermediate_ids, | |
| 1140 | alteration->intermediate_n))) { | |
| 1141 | 380 | change_trace ("\t%i has changed [%i].\n", n, node->id); |
| 1142 | 378 | RowDelta *delta = g_slice_new(RowDelta); |
| 1143 | 431 | _music_source_view_check_tree (MUSIC_SOURCE_VIEW(self), node); |
| 1144 | 378 | delta->node = node; |
| 1145 | delta->original_n = n; | |
| 1146 | 385 | delta->new_n = -1; |
| 1147 | 378 | (*alteration->deltas) = g_slist_prepend(*alteration->deltas, delta); |
| 1148 | 380 | } else { |
| 1149 | change_trace ("\t%i hasn't changed [%i]\n", n, node->id); | |
| 1150 | 378 | }; |
| 1151 | 380 | |
| 1152 | 378 | // FIXME: perhaps implement a map_row_range function ... |
| 1153 | if (n_min - page->start_e > page->size) { | |
| 1154 | if (!page->chain_next) | |
| 1155 | _music_source_view_read_next_page(MUSIC_SOURCE_VIEW(self), page); | |
| 1156 | page = page->next; | |
| 1157 | }; | |
| 1158 | 374 | }; |
| 1159 | 438 | |
| 1160 | change_trace ("\tpropagate_change_old: alteration has %i deltas.\n", | |
| 1161 | g_slist_length (*alteration->deltas)); | |
| 1162 | 367 | }; |
| 1163 | ||
| 1164 | 431 | /* process_row_change_new: find existing delta for row, and store its new position. |
| 1165 | * | |
| 1166 | * At this point the rows that have changed has been removed, and will be reinserted later. | |
| 1167 | * change_new is propagated all the way to the tail, so we locate the exact insertion point for | |
| 1168 | * the row. | |
| 1169 | 377 | * */ |
| 1170 | 369 | static void process_row_change_new (GenericView *self, RowAlteration *alteration, |
| 1171 | GList *tail_clnode, Entry *tail_entry, gboolean entry_matched, | |
| 1172 | ViewPage *page, int n_min, int n_max) { | |
| 1173 | 431 | change_trace ("process_row_change_new: tail %s %i, [%i, %i] .. %i deltas\n", |
| 1174 | ENTRY_PF(tail_entry), n_min, n_max, g_slist_length (*alteration->deltas)); | |
| 1175 | 367 | |
| 1176 | 431 | // Find the delta in our list whose view node represents 'tail_entry' at 'tail_clnode'. |
| 1177 | 367 | // FIXME: this could potentially be meaningfully slow - in flat views, lots of rows can change |
| 1178 | // at once .. | |
| 1179 | RowDelta *delta = NULL; | |
| 1180 | 374 | for (GSList *deltas_node=(*alteration->deltas); deltas_node; deltas_node=deltas_node->next) { |
| 1181 | 378 | delta = deltas_node->data; |
| 1182 | g_return_if_fail (delta->node != NULL); | |
| 1183 | 380 | |
| 1184 | 431 | // At this point, 'node' is not inside any parent node. |
| 1185 | if (partial_node_match (delta->node, tail_clnode, tail_entry, | |
| 1186 | alteration->intermediate_ids, alteration->intermediate_n)) | |
| 1187 | 378 | goto found; |
| 1188 | 367 | }; |
| 1189 | 369 | |
| 1190 | 377 | g_warning ("process_row_change_new: Unable to find delta for %s %i @ [%i, %i)\n", |
| 1191 | ENTRY_PF(tail_entry), n_min, n_max); | |
| 1192 | return; | |
| 1193 | ||
| 1194 | found:; | |
| 1195 | 364 | |
| 1196 | 374 | int n = n_min; |
| 1197 | 378 | delta->new_n = n; |
| 1198 | change_trace ("Reinserting at n%i.\n", n); | |
| 1199 | // node->n is updated by _insert_row_in_page | |
| 1200 | 388 | _insert_row_in_page (MUSIC_SOURCE_VIEW(self), alteration->parent_node, page, n-page->start_e, |
| 1201 | 408 | delta->node, FALSE); |
| 1202 | 399 | music_source_view_add_row_marker (MUSIC_SOURCE_VIEW(self), &delta->new_n, |
| 1203 | alteration->parent_node, TRUE); | |
| 1204 | 364 | }; |
| 1205 | ||
| 1206 | ||
| 1207 | 380 | /* process_row_delete: called by propagate methods to remove the row representing 'tail_entry' |
| 1208 | * in [n_min, n_max) on parent_node:page */ | |
| 1209 | 445 | static void process_row_delete (GenericView *self, RowAlteration *alteration, GList *tail_clnode, |
| 1210 | Entry *tail_entry, ViewNode *parent_node, gboolean entry_matched, | |
| 1211 | ViewPage *page, int n_min, int n_max) { | |
| 1212 | 442 | change_trace ("process_row_delete: %s %i, matched %i.\n", ENTRY_PF(tail_entry), entry_matched); |
| 1213 | 362 | if (!entry_matched) { |
| 1214 | 416 | // We should have found the entry, but there is a possibility it was already removed: |
| 1215 | // in config artist:album:release:track:recording:file, when an artist is removed we act on | |
| 1216 | // the track notify and the file notify so end up removing each row twice. | |
| 1217 | // FIXME: do something more robust than just ignoring failure to find the row. | |
| 1218 | // OLD: g_warning ("_remove_recursive: Unable to find %s %i in view..", ENTRY_PF(tail_entry)); | |
| 1219 | 362 | return; |
| 1220 | }; | |
| 1221 | ||
| 1222 | // FIXME: test for this .. remove 1 file out of a bunch with same bitrate, we should use | |
| 1223 | // brute force to find the entry. | |
| 1224 | if (n_min < n_max+1) { | |
| 1225 | 445 | entry_matched = _locate_entry_brute_force (self, tail_clnode, tail_entry, alteration->entry, page, |
| 1226 | n_min, n_max, &page, &n_min); | |
| 1227 | 362 | g_return_if_fail (entry_matched); |
| 1228 | }; | |
| 1229 | ||
| 1230 | 385 | _remove_row_in_page (self, parent_node, page, n_min - page->start_e, TRUE, TRUE); |
| 1231 | 362 | }; |
| 1232 | ||
| 1233 | 431 | /* propogate_backwards and propogate_forwards: perform 'change' on every row referencing 'entry'. |
| 1234 | * | |
| 1235 | * This is a more complicated business than you'd think, mainly due to many:1 joins. Picture the | |
| 1236 | * config "artist:album:release:track:recording:file": | |
| 1237 | 362 | * a) The same recording can appear multiple times on different tracks, or in the orphanage, |
| 1238 | * so if a file is added or removed this must also be done in multiple places. | |
| 1239 | // FIXME: finish writing this :) | |
| 1240 | * | |
| 1241 | * link_clnode, link_entry: This pair are walked backwards and then forwards through the config. | |
| 1242 | * | |
| 1243 | * entry_clnode, entry: The entry that triggered the change. If it's being inserted or removed, | |
| 1244 | * it must be a tail entry or the LHS of a m:1 join. | |
| 1245 | * | |
| 1246 | * parent_node: Where all of this action is taking place. | |
| 1247 | * | |
| 1248 | * accumulator: While walking backwards, the entries we pass through are stored in the | |
| 1249 | * accumulator to make it much easier to walk forwards again. | |
| 1250 | * | |
| 1251 | 381 | * |
| 1252 | * Specific to propagate_forwards: | |
| 1253 | 399 | * past_trigger_entry: We could have been triggered by an entry in the middle of the level, |
| 1254 | 397 | * but we still need to find the tail entries to match them against rows. |
| 1255 | 399 | * past_trigger_entry indicates the accumulator has been exhausted, and we |
| 1256 | * should use view_config_step_forwards and find all of the subsequent | |
| 1257 | 397 | * rows. |
| 1258 | 381 | * |
| 1259 | 384 | * branch_id, Once past_trigger_entry, we could be querying all entries across a join |
| 1260 | * branch_tested_n: when in fact, the join is within an orphan branch and we should filter | |
| 1261 | * out entries that don't belong on the branch. | |
| 1262 | 381 | * |
| 1263 | * As an example, picture the following config: | |
| 1264 | * (album[name]:release:track:recording:file[path]) / (artist[name]:recording:file[path]) | |
| 1265 | * Imagine we receive change notify on 'artist 3'. Because this is the trigger entry, | |
| 1266 | * propagate_forwards proceeds to call view_config_step_forwards, and gets all the recordings of | |
| 1267 | * that artist. Some of them aren't orphaned, however, so they don't actually appear where we are | |
| 1268 | 384 | * looking. We need to check them with check_clnode_for_entry. |
| 1269 | * | |
| 1270 | * This is only necessary when doing change_find_new. When doing a remove or insert the entry | |
| 1271 | * could in fact have moved to a different clnode, so the code will get really confused. | |
| 1272 | 374 | * |
| 1273 | * Terminology here is specific: a 'change' means specifically an entry that this row represents has | |
| 1274 | 365 | * changed. An 'alteration' is a row being removed, inserted or changed. |
| 1275 | */ | |
| 1276 | 438 | |
| 1277 | static int propagate_forwards (GenericView *self, RowAlteration *alteration, GList *link_clnode, | |
| 1278 | Entry *link_entry, GSList *accumulator_node, | |
| 1279 | gboolean past_trigger_entry, guint32 branch_id, int branch_tested_n, | |
| 1280 | ViewPage *_page, int _n_min, int _n_max); | |
| 1281 | ||
| 1282 | static int propagate_forwards_recurse (GenericView *self, RowAlteration *alteration, | |
| 1283 | GList *link_clnode, Entry *link_entry, guint32 branch_id, | |
| 1284 | int branch_tested_n, ViewPage *page, int n_min, int n_max) | |
| 1285 | { | |
| 1286 | // This is a utility function called when the trigger entry is not the tail. In most cases, we | |
| 1287 | // then recurse through every entry of type link->next that has a foreign key on link_entry. | |
| 1288 | GList *prev_link_clnode = link_clnode->prev; | |
| 1289 | ||
| 1290 | if (alteration->type == CHANGE_FIND_OLD) { | |
| 1291 | // When looking for entries that have changed in their old position, we can't use the above | |
| 1292 | // tactic because an entry past the trigger entry might have changed too (and still be | |
| 1293 | // waiting in the notify queue). This might prevent us from finding it using the binary | |
| 1294 | // search. Instead, we use brute force search for rows that represent the trigger entry. | |
| 1295 | change_trace ("propagate_forwards_recurse: brute forcing: link %s %i, trigger %s %i.\n", | |
| 1296 | ENTRY_PF(link_entry), ENTRY_PF(alteration->entry)); | |
| 1297 | ||
| 1298 | // We don't recurse further, so we must be at the trigger entry and not past it. | |
| 1299 | g_return_val_if_fail (link_entry == alteration->entry, 0); | |
| 1300 | ||
| 1301 | ViewPage *search_page; int search_n; | |
| 1302 | int rows = 0; | |
| 1303 | 445 | while (_locate_entry_brute_force (self, prev_link_clnode, link_entry, NULL, page, |
| 1304 | n_min, n_max, &search_page, &search_n)) { | |
| 1305 | 438 | ViewNode *child_node = g_array_index(search_page->nodes, ViewNode *, search_n); |
| 1306 | EntryType tail_entry_type = CONFIG_NODE(child_node->parent_tail)->type; | |
| 1307 | Entry *tail_entry = music_source_query_entry (PP->source, tail_entry_type, | |
| 1308 | child_node->id, "propagate"); | |
| 1309 | process_row_change_old (self, alteration, child_node->parent_tail, tail_entry, TRUE, | |
| 1310 | page, search_n, search_n+1); | |
| 1311 | entry_unref (tail_entry, "propagate"); | |
| 1312 | page = search_page; n_min = search_n + 1; | |
| 1313 | ||
| 1314 | if (n_min == n_max) break; | |
| 1315 | } | |
| 1316 | return rows; | |
| 1317 | } | |
| 1318 | ||
| 1319 | change_trace ("propagate_forwards_recurse: link is %s %i, trigger %s %i.\n", | |
| 1320 | ENTRY_PF(link_entry), ENTRY_PF(alteration->entry)); | |
| 1321 | ||
| 1322 | // view_config_step_forwards_fast() returns a list of entry lists, for each branch at | |
| 1323 | // link_clnode->prev. The list of entries for each branch match the type of the config node, | |
| 1324 | // but aren't checked for if they actually belong on that branch (that's the 'fast' part of the | |
| 1325 | // name). For efficiency, we don't check the branches straight away: we wait and see if we reach | |
| 1326 | // orphaned entry type in the config and check then. If it's not reached we check at the tail | |
| 1327 | // node. | |
| 1328 | // | |
| 1329 | // FIXME: in the case of small range, would it be quicker to actually use the above method? | |
| 1330 | // instead of all the entry queries i binary search? | |
| 1331 | int row_count = 0; | |
| 1332 | GSList *link_branch_list = view_config_step_forwards_fast | |
| 1333 | (PP->source, PP->config, prev_link_clnode, link_entry); | |
| 1334 | for (GSList *link_list=link_branch_list; link_list; link_list=link_list->next) { | |
| 1335 | g_return_val_if_fail (link_clnode != NULL, 0); | |
| 1336 | change_trace ("\tpropagate_forwards_recurse: at %s: %i entries on branch %i (%s) from %s %i" | |
| 1337 | ".\n", CONFIG_TYPE_NAME(prev_link_clnode), g_slist_length (link_list), | |
| 1338 | CONFIG_NODE(link_clnode)->branch_id, CONFIG_TYPE_NAME(link_clnode), | |
| 1339 | ENTRY_PF(link_entry)); | |
| 1340 | for (GSList *node=link_list->data; node; node=node->next) { | |
| 1341 | Entry *entry = node->data; | |
| 1342 | change_trace ("\t\tpropagate_forwards_recurse: %s %i.\n", ENTRY_PF(entry)); | |
| 1343 | ||
| 1344 | guint32 bid = branch_id; int btn = branch_tested_n; | |
| 1345 | ||
| 1346 | if (CONFIG_NODE(link_clnode)->orphaning_join != NULL | |
| 1347 | && (alteration->type==CHANGE_FIND_OLD || alteration->type==CHANGE_FIND_NEW)) | |
| 1348 | // We are at the type of this branch's orphaning join. Check if the entry belongs on | |
| 1349 | // the branch or if it's actually. | |
| 1350 | // | |
| 1351 | // FIXME: at the moment there is only one possible orphan branch, but if two or more | |
| 1352 | // were possible, and the orphaned join entry was inside another orphan branch, this | |
| 1353 | // code wouldn't trigger and things might break horribly. We would need to maintain | |
| 1354 | // a list of past orphaning joins in the cnode, perhaps, or pass one along | |
| 1355 | // propagate_forwards. | |
| 1356 | if (!check_clnode_for_entry(PP->source, PP->config, link_clnode, entry, &bid, &btn)) | |
| 1357 | continue; | |
| 1358 | ||
| 1359 | // FIXME: Filter entry by branch if it is the type of one of the ... | |
| 1360 | ||
| 1361 | // intermediate_ids array is shared, which is fine because the calls are sequential. | |
| 1362 | const int intermediate_n = alteration->intermediate_n; | |
| 1363 | int rows = propagate_forwards(self, alteration, link_clnode, entry, NULL, | |
| 1364 | TRUE, bid, btn, page, n_min, n_max); | |
| 1365 | ||
| 1366 | switch (alteration->type) { | |
| 1367 | case INSERT: case CHANGE_FIND_NEW: n_max += rows; break; | |
| 1368 | case REMOVE: n_max -= rows; break; | |
| 1369 | }; | |
| 1370 | row_count += rows; | |
| 1371 | alteration->intermediate_n = intermediate_n; | |
| 1372 | } | |
| 1373 | entry_query_list_free (link_list->data, "walk-config"); | |
| 1374 | ||
| 1375 | // Update the config node to point to the orphan branch. | |
| 1376 | link_clnode = CONFIG_NODE(link_clnode)->orphan_branch; | |
| 1377 | } | |
| 1378 | g_slist_free (link_branch_list); | |
| 1379 | ||
| 1380 | // FIXME: I commented this out and it didn't seem to break anything. | |
| 1381 | //if (link_entry != alteration->entry) | |
| 1382 | // entry_unref (link_entry, "walk-config"); | |
| 1383 | return row_count; | |
| 1384 | } | |
| 1385 | ||
| 1386 | 365 | static int propagate_forwards (GenericView *self, RowAlteration *alteration, GList *link_clnode, |
| 1387 | Entry *link_entry, GSList *accumulator_node, | |
| 1388 | 384 | gboolean past_trigger_entry, guint32 branch_id, int branch_tested_n, |
| 1389 | 381 | ViewPage *_page, int _n_min, int _n_max) { |
| 1390 | 384 | change_trace ("propagate_forwards - [%i, %i) - for %s %i, link %s %i @ %s\n", _n_min, _n_max, |
| 1391 | ENTRY_PF(alteration->entry), ENTRY_PF(link_entry), CONFIG_TYPE_NAME(link_clnode)); | |
| 1392 | 185 | ViewPage *page = _page; int n_min = _n_min, n_max = _n_max; |
| 1393 | 261 | |
| 1394 | 438 | // locate_entry normally reads entries from the source, using the id's stored in the view. |
| 1395 | // During a change notification we might want to find an entry not in the source (in particular, | |
| 1396 | // the previous version of an entry). | |
| 1397 | Entry *search_substitute_entry = past_trigger_entry? link_entry: alteration->entry; | |
| 1398 | 237 | //printf("\tThe accumulator has: "); |
| 1399 | //for (GSList *node=accumulator_node; node; node=node->next) { | |
| 1400 | // Entry *entry = node->data; | |
| 1401 | // printf(" [%x]", (guint)entry); fflush(stdout); | |
| 1402 | // printf(" %s %i", ENTRY_PF(entry)); fflush(stdout); | |
| 1403 | //}; | |
| 1404 | //printf("\n"); fflush(stdout); | |
| 1405 | 261 | |
| 1406 | 374 | // Walk forwards through the config. Current position is link_entry representing link_clnode. On |
| 1407 | // each iteration we try and narrow the range [n_min, n_max). | |
| 1408 | 209 | gboolean entry_matched = FALSE; |
| 1409 | 384 | ViewConfigNode *link_cnode = link_clnode->data; |
| 1410 | 185 | do { |
| 1411 | 226 | g_warn_if_fail (link_clnode!=NULL); |
| 1412 | 261 | g_warn_if_fail (link_cnode->type==link_entry->type); |
| 1413 | ||
| 1414 | 374 | // If the config node is sorted, narrow the range using binary search. |
| 1415 | // | |
| 1416 | 380 | if ((n_min!=n_max || page==NULL) && link_cnode->sort_property_id!=-1) { |
| 1417 | 438 | entry_matched = _locate_entry_range(self, link_clnode, link_entry, |
| 1418 | search_substitute_entry, alteration->parent_node, | |
| 1419 | &page, &n_min, &n_max); | |
| 1420 | 380 | change_trace ("\ton %s: new range [%i, %i)\n", CONFIG_TYPE_NAME(link_clnode), n_min, |
| 1421 | n_max); | |
| 1422 | }; | |
| 1423 | 261 | |
| 1424 | 374 | if (!link_cnode->flat) |
| 1425 | 185 | break; |
| 1426 | 431 | if (!link_clnode->next) { |
| 1427 | g_warning ("Unexpected end of config, at node %s.\n", | |
| 1428 | entry_type_name[link_cnode->type]); | |
| 1429 | break; | |
| 1430 | } | |
| 1431 | 261 | |
| 1432 | 383 | // Move to next node in config - going down an orphan branch if alteration->clnode (trigger) |
| 1433 | // is down that branch. | |
| 1434 | // | |
| 1435 | 261 | link_clnode = link_clnode->next; |
| 1436 | 383 | if (CONFIG_NODE(link_clnode)->orphan_branch!=NULL |
| 1437 | && CLNODE_IS_ON_BRANCH(alteration->clnode, CONFIG_NODE(link_clnode)->orphan_branch)) | |
| 1438 | link_clnode = CONFIG_NODE(link_clnode)->orphan_branch; | |
| 1439 | link_cnode = CONFIG_NODE(link_clnode); | |
| 1440 | 261 | |
| 1441 | 185 | // many:1 joins are flagged on the 1 side, so we look ahead for them. |
| 1442 | // | |
| 1443 | 365 | // FIXME: I don't think this works, because we should be storing the id of the entry *before* |
| 1444 | // the node flagged as many:1. Doesn't seem to be tested yet. | |
| 1445 | 374 | if (alteration->type==INSERT || alteration->type==CHANGE_FIND_OLD |
| 1446 | || alteration->type==CHANGE_FIND_NEW) | |
| 1447 | 383 | if (link_cnode->flags & VIEW_CONFIG_NODE_MANY_TO_ONE_JOIN) { |
| 1448 | 365 | g_return_val_if_fail (alteration->intermediate_ids!=NULL, 0); |
| 1449 | alteration->intermediate_ids[alteration->intermediate_n++] = link_entry->id; | |
| 1450 | 362 | }; |
| 1451 | 261 | |
| 1452 | 365 | if (link_entry==alteration->entry) |
| 1453 | 362 | past_trigger_entry = TRUE; |
| 1454 | 185 | |
| 1455 | if (accumulator_node!=NULL) { | |
| 1456 | // If we are still working towards the entry type which was added | |
| 1457 | // | |
| 1458 | 281 | //printf ("for %s %i: inp: Unref %s %i.\n", ENTRY_PF(entry), ENTRY_PF(link_entry)); fflush (stdout); |
| 1459 | 185 | entry_unref (link_entry, "walk-config"); |
| 1460 | 261 | link_entry = accumulator_node->data; |
| 1461 | 185 | accumulator_node = accumulator_node->next; |
| 1462 | 365 | } else if (!past_trigger_entry && link_entry!=alteration->entry) { |
| 1463 | // After the accumulator comes trigger itself. | |
| 1464 | 185 | // |
| 1465 | entry_unref (link_entry, "walk-config"); | |
| 1466 | 365 | link_entry = alteration->entry; |
| 1467 | 185 | } else { |
| 1468 | 438 | // If we are here, we have got to the trigger entry, and it's not the tail join. We need |
| 1469 | // (probably) to propagate further, by recursing to each entry that has a foreign key | |
| 1470 | // to the current entry. | |
| 1471 | return propagate_forwards_recurse (self, alteration, link_clnode, link_entry, | |
| 1472 | branch_id, branch_tested_n, page, n_min, n_max); | |
| 1473 | 185 | }; |
| 1474 | 226 | } while (link_clnode!=NULL); |
| 1475 | 300 | |
| 1476 | 384 | g_return_val_if_fail (link_clnode!=NULL && !(link_cnode->flat), 0); |
| 1477 | 374 | |
| 1478 | // Notice link_clnode/entry is used rather than alteration->entry. This trigger entry | |
| 1479 | // might have been an m:1 join entry rather than a tail node, whereas the link is now | |
| 1480 | // definitely a tail node because we just walked it to the tail of the level. | |
| 1481 | // | |
| 1482 | if (!past_trigger_entry) { | |
| 1483 | 384 | g_warn_if_fail (link_cnode->type==CONFIG_NODE(alteration->clnode)->type); |
| 1484 | g_warn_if_fail (link_cnode->type==alteration->entry->type); | |
| 1485 | 374 | }; |
| 1486 | ||
| 1487 | 383 | // If past the trigger entry, link_entry might not apply to link_clnode - it could be in a |
| 1488 | // different branch. (This is because of the way view_config_step_forwards works). | |
| 1489 | 384 | change_trace ("past %i - tested n %i link %i\n", past_trigger_entry, branch_tested_n, |
| 1490 | link_cnode->branch_n); | |
| 1491 | if (past_trigger_entry && alteration->type==CHANGE_FIND_NEW) { | |
| 1492 | if (branch_tested_n < link_cnode->branch_n) | |
| 1493 | 438 | if (!check_clnode_for_entry(PP->source, PP->config, link_clnode, link_entry, &branch_id, |
| 1494 | 384 | &branch_tested_n)) |
| 1495 | 383 | return 0; |
| 1496 | 384 | |
| 1497 | // Branch id can only go wrong when we are past the trigger entry, because of the use of | |
| 1498 | // view_config_step_forwards. | |
| 1499 | g_return_val_if_fail (branch_id == link_cnode->branch_id, 0); | |
| 1500 | }; | |
| 1501 | 381 | |
| 1502 | 377 | // Int ids is expected to be null terminated correctly |
| 1503 | if (alteration->type==INSERT || alteration->type==CHANGE_FIND_NEW) | |
| 1504 | 378 | if (alteration->intermediate_ids != NULL) |
| 1505 | alteration->intermediate_ids[alteration->intermediate_n] = 0; | |
| 1506 | 377 | |
| 1507 | 374 | switch (alteration->type) { |
| 1508 | case INSERT: | |
| 1509 | process_row_insert (self, link_clnode, link_entry, alteration->parent_node, | |
| 1510 | alteration->intermediate_ids, alteration->intermediate_n, | |
| 1511 | entry_matched, page, n_min, n_max); | |
| 1512 | break; | |
| 1513 | ||
| 1514 | case CHANGE_FIND_OLD: | |
| 1515 | process_row_change_old (self, alteration, link_clnode, link_entry, entry_matched, | |
| 1516 | page, n_min, n_max); | |
| 1517 | break; | |
| 1518 | ||
| 1519 | case CHANGE_FIND_NEW: | |
| 1520 | process_row_change_new (self, alteration, link_clnode, link_entry, entry_matched, | |
| 1521 | page, n_min, n_max); | |
| 1522 | break; | |
| 1523 | ||
| 1524 | case REMOVE: | |
| 1525 | 445 | process_row_delete (self, alteration, link_clnode, link_entry, alteration->parent_node, |
| 1526 | 374 | entry_matched, page, n_min, n_max); |
| 1527 | break; | |
| 1528 | default: g_warn_if_reached (); | |
| 1529 | }; | |
| 1530 | ||
| 1531 | return 1; | |
| 1532 | 185 | }; |
| 1533 | ||
| 1534 | 365 | static void propagate_backwards (GenericView *self, RowAlteration *alteration, GList *link_clnode, |
| 1535 | Entry *link_entry, GSList **accumulator) { | |
| 1536 | 389 | change_trace ("propagate_backwards: parent %x depth %i, n %i %s %i <%s>\n", |
| 1537 | alteration->parent_node, alteration->parent_node->depth, alteration->parent_node->n, | |
| 1538 | ENTRY_PF(alteration->entry), CONFIG_TYPE_NAME(alteration->clnode)); | |
| 1539 | 362 | g_return_if_fail (CONFIG_NODE(link_clnode)->type == link_entry->type); |
| 1540 | 261 | |
| 1541 | 234 | // Walk from current position to the previous break - the head join of the parent node, or the |
| 1542 | // *RHS* of the m:1 join. | |
| 1543 | 362 | change_trace ("\twalk to join: from %s %i, [config %s.%i], ..", ENTRY_PF(link_entry), |
| 1544 | 365 | CONFIG_TYPE_NAME(link_clnode), CONFIG_NODE(alteration->clnode)->branch_id); |
| 1545 | 362 | |
| 1546 | link_entry = view_config_walk_backwards_specific(link_clnode, PP->source, link_entry, | |
| 1547 | 431 | VIEW_CONFIG_WALK_TO_BREAK, NULL, NULL, |
| 1548 | 362 | &link_clnode, accumulator); |
| 1549 | ||
| 1550 | change_trace ("to %s %i [%s].\n", ENTRY_PF(link_entry), CONFIG_TYPE_NAME(link_clnode)); | |
| 1551 | ||
| 1552 | if (CONFIG_NODE(link_clnode)->flags & VIEW_CONFIG_NODE_MANY_TO_ONE_JOIN) { | |
| 1553 | 281 | // We're not at the head of the level yet, we hit a break. Find all the entries on the |
| 1554 | 362 | // other side and run _insert_in_node to continue down each of those paths. |
| 1555 | ViewConfigNode *link_cnode = link_clnode->data; | |
| 1556 | GSList *many = music_source_query_relations(PP->source, link_entry->type, link_entry->id, | |
| 1557 | link_cnode->relation_apid, "entry-added"); | |
| 1558 | 226 | g_return_if_fail (g_slist_length(many) > 0); |
| 1559 | ||
| 1560 | for (GSList *many_node=many; many_node; many_node=many_node->next) { | |
| 1561 | Entry *many_entry = many_node->data; | |
| 1562 | 185 | |
| 1563 | 261 | // Copy the accumulator for each iteration (because it needs to be different for |
| 1564 | 226 | // each one), but use actual accumulator on last go. |
| 1565 | // FIXME: couldn't you just remove relation from it and add new relation each time? | |
| 1566 | 281 | // the recursion runs in sequence, not in parallel |
| 1567 | 185 | GSList **accumulator_to_use = accumulator, *copied_accumulator = NULL; |
| 1568 | 226 | if (many_node->next!=NULL) { |
| 1569 | 185 | copied_accumulator = g_slist_copy(*accumulator); |
| 1570 | 226 | g_slist_foreach (copied_accumulator, (GFunc)reference_entry_callback, NULL); |
| 1571 | 185 | accumulator_to_use = &copied_accumulator; |
| 1572 | }; | |
| 1573 | ||
| 1574 | 226 | entry_ref (many_entry, "walk-config"); |
| 1575 | *accumulator_to_use = g_slist_prepend(*accumulator_to_use, many_entry); | |
| 1576 | 261 | |
| 1577 | 365 | propagate_backwards (self, alteration, link_clnode->prev, many_entry, |
| 1578 | accumulator_to_use); | |
| 1579 | 185 | //printf("Set to ignore %s %i\n", entry_type_name[relation->type], relation->id); fflush(stdout); |
| 1580 | 261 | |
| 1581 | 365 | if (alteration->type==INSERT) { |
| 1582 | GSList **ignore_list = &self->ignore_list[many_entry->type]; | |
| 1583 | (*ignore_list) = g_slist_prepend(*ignore_list, GINT_TO_POINTER(many_entry->id)); | |
| 1584 | }; | |
| 1585 | 261 | |
| 1586 | 185 | if (copied_accumulator!=NULL) |
| 1587 | g_slist_free (copied_accumulator); | |
| 1588 | }; | |
| 1589 | 261 | |
| 1590 | entry_query_list_free (many, "entry-added"); | |
| 1591 | 365 | } else if (link_clnode==alteration->parent_node->head || link_clnode->prev==NULL) { |
| 1592 | 300 | // We hit the head - now we go back the way we came, pinpointing the insert position, |
| 1593 | 281 | // using the forwards-recursive insert_recursive. |
| 1594 | 185 | // |
| 1595 | 442 | change_trace ("\tpropagate_backwards: parent (%s %i) has %i children\n", |
| 1596 | CONFIG_TYPE_NAME(alteration->parent_node->parent_tail), | |
| 1597 | alteration->parent_node->id, alteration->parent_node->e_children); | |
| 1598 | 438 | ViewPage *page = alteration->parent_node->children; |
| 1599 | int n_min = 0, n_max = alteration->parent_node->e_children; | |
| 1600 | 261 | |
| 1601 | 367 | // Per-alteration init for forwards |
| 1602 | switch (alteration->type) { | |
| 1603 | 374 | case INSERT: case CHANGE_FIND_OLD: case CHANGE_FIND_NEW: { |
| 1604 | 367 | alteration->intermediate_ids = NULL, alteration->intermediate_n = 0; |
| 1605 | ViewConfigNode *level_head = CONFIG_NODE(alteration->parent_node->head); | |
| 1606 | if (level_head->level_break_count > 1) | |
| 1607 | alteration->intermediate_ids = g_new0(int, level_head->level_break_count); | |
| 1608 | break; | |
| 1609 | }; | |
| 1610 | 365 | }; |
| 1611 | 185 | |
| 1612 | 281 | // First entry of accumulator == new_entry |
| 1613 | 300 | GSList *accumulator_node = *accumulator!=NULL? (*accumulator)->next: NULL; |
| 1614 | ||
| 1615 | 365 | propagate_forwards (self, alteration, link_clnode, link_entry, accumulator_node, FALSE, |
| 1616 | 384 | 1, 1, page, n_min, n_max); |
| 1617 | 185 | }; |
| 1618 | 365 | }; |
| 1619 | ||
| 1620 | static void propagate_alteration (GenericView *self, RowAlterationType type, GList *clnode, | |
| 1621 | Entry *entry, ViewNode *parent_node) { | |
| 1622 | g_return_if_fail (CONFIG_NODE(clnode)->type == entry->type); | |
| 1623 | ||
| 1624 | 369 | // Use propogate_change for change notifications. |
| 1625 | g_return_if_fail (type==INSERT || type==REMOVE); | |
| 1626 | ||
| 1627 | 365 | RowAlteration *alteration = g_slice_new(RowAlteration); |
| 1628 | alteration->type = type; | |
| 1629 | alteration->clnode = clnode; | |
| 1630 | alteration->entry = entry; | |
| 1631 | alteration->parent_node = parent_node; | |
| 1632 | ||
| 1633 | GSList *accumulator = NULL; | |
| 1634 | ||
| 1635 | propagate_backwards (self, alteration, clnode, entry, &accumulator); | |
| 1636 | ||
| 1637 | // propagate_forwards unrefs entry and the rest of the accumulator, but doesn't free the list. | |
| 1638 | g_slist_free (accumulator); | |
| 1639 | g_slice_free (RowAlteration, alteration); | |
| 1640 | 185 | }; |
| 1641 | ||
| 1642 | 431 | /* FIXME: this is an extra-confusing name. You should say 'update' instead of 'change', since it's |
| 1643 | * a slightly less broad term. */ | |
| 1644 | 369 | static void propagate_change (GenericView *self, GList *clnode, Entry *old_entry, Entry *new_entry, |
| 1645 | ViewNode *parent_node, GSList **deltas) { | |
| 1646 | g_return_if_fail (CONFIG_NODE(clnode)->type == old_entry->type); | |
| 1647 | g_return_if_fail (CONFIG_NODE(clnode)->type == new_entry->type); | |
| 1648 | ||
| 1649 | 438 | change_trace ("propagate_change: %s.\n", CONFIG_TYPE_NAME(clnode)); |
| 1650 | ||
| 1651 | 369 | RowAlteration *alteration = g_slice_new(RowAlteration); |
| 1652 | alteration->parent_node = parent_node; | |
| 1653 | alteration->clnode = clnode; | |
| 1654 | alteration->deltas = deltas; | |
| 1655 | ||
| 1656 | GSList *accumulator = NULL; | |
| 1657 | ||
| 1658 | /* FIXME: do we only need to walk backwards once, and go forwards twice, I think ?? */ | |
| 1659 | ||
| 1660 | 438 | // Find every row that represents old_entry and create a RowDelta entry for it (holding its |
| 1661 | // old position). This is harder than it would seem because the musicsource holds the new | |
| 1662 | // version of the entry, so if the search functions query the source for it they get misled. | |
| 1663 | 369 | alteration->type = CHANGE_FIND_OLD; |
| 1664 | alteration->entry = old_entry; | |
| 1665 | propagate_backwards (self, alteration, clnode, old_entry, &accumulator); | |
| 1666 | ||
| 1667 | g_slist_free (accumulator); | |
| 1668 | 375 | |
| 1669 | // After all the deltas are found we have to remove the existing rows, so | |
| 1670 | 377 | // locate_entry_range doesn't get confused by entries in the wrong place when finding the new |
| 1671 | 375 | // insertion points. Instead of using markers we sort the list and remove from top down. |
| 1672 | // FIXME: which way would be faster? | |
| 1673 | // FIXME: it's really inefficient to remove every entry that's had a property changed just | |
| 1674 | // to reinsert it in the same place. Surely it will only have moved if the property it's | |
| 1675 | // sorted on has changed. | |
| 1676 | int delta_compare_func(RowDelta *a, RowDelta *b) { | |
| 1677 | 378 | return a->original_n < b->original_n? -1: |
| 1678 | a->original_n > b->original_n? 1: 0; | |
| 1679 | 375 | }; |
| 1680 | *deltas = g_slist_sort(*deltas, (GCompareFunc)delta_compare_func); | |
| 1681 | ViewPage *page = parent_node->children; | |
| 1682 | int rows_removed = 0; | |
| 1683 | 438 | GSList *previous_delta_node = NULL; int previous_delta_original_n = -1; |
| 1684 | 375 | for (GSList *deltas_node=*deltas; deltas_node; deltas_node=deltas_node->next) { |
| 1685 | 377 | RowDelta *delta = deltas_node->data; |
| 1686 | ||
| 1687 | 438 | // There is a chance of getting duplicate deltas, when m:1 joins are at play. It seems that |
| 1688 | // the most efficient way to deal is to remove them here once the list is sorted. | |
| 1689 | // FIXME: of course, a more efficient way might be to make the list a btree. | |
| 1690 | if (previous_delta_node != NULL && delta->original_n == previous_delta_original_n) { | |
| 1691 | g_slice_free (RowDelta, delta); | |
| 1692 | deltas_node = g_slist_remove_link (previous_delta_node, deltas_node); | |
| 1693 | continue; | |
| 1694 | } | |
| 1695 | ||
| 1696 | 375 | // FIXME: this is a bit of a hack. but, i don't think we should have to query any more pages |
| 1697 | // .. this assumption needs some testing :) If this sort of thing does happen, we need to | |
| 1698 | // add all of the delta ranges as markers. | |
| 1699 | 378 | int n = delta->original_n - (rows_removed++); |
| 1700 | page = _music_source_view_find_page_for_row(MUSIC_SOURCE_VIEW(self), parent_node, &n, | |
| 1701 | 375 | page); |
| 1702 | 378 | change_trace ("Removing at %i.\n", n); |
| 1703 | 385 | _remove_row_in_page (self, alteration->parent_node, page, n - page->start_e, FALSE, FALSE); |
| 1704 | 438 | |
| 1705 | previous_delta_original_n = delta->original_n; | |
| 1706 | previous_delta_node = deltas_node; | |
| 1707 | 375 | }; |
| 1708 | 377 | |
| 1709 | 369 | accumulator = NULL; |
| 1710 | 380 | |
| 1711 | 390 | //printf ("\n\nGot %i deltas. Node size %i\n\n", g_slist_length(*deltas), |
| 1712 | // parent_node->e_children); fflush (stdout); | |
| 1713 | 438 | // If any rows have changed: propagate from the new entry |
| 1714 | 374 | if (g_slist_length(*deltas) > 0) { |
| 1715 | alteration->type = CHANGE_FIND_NEW; | |
| 1716 | alteration->entry = new_entry; | |
| 1717 | propagate_backwards (self, alteration, clnode, new_entry, &accumulator); | |
| 1718 | }; | |
| 1719 | 369 | |
| 1720 | g_slice_free (RowAlteration, alteration); | |
| 1721 | }; | |
| 1722 | 185 | |
| 1723 | 380 | // Check if 'entry' is adopting any currently orphaned entries. |
| 1724 | 389 | static void entry_notify_process_adoption (GenericView *self, GList *clnode, Entry *entry) { |
| 1725 | 380 | // Find the 1 side of the join, assuming it is a normal many:1 join. |
| 1726 | // | |
| 1727 | ViewConfigNode *rhs = CONFIG_NODE(clnode->next); | |
| 1728 | g_return_if_fail (entry->type == APID_GET_TYPE(rhs->relation_apid)); | |
| 1729 | g_return_if_fail (APID_GET_PROPERTY(rhs->relation_apid).type == rhs->type); | |
| 1730 | Entry *one = entry_get_property(entry, APID_GET_PROPERTY_ID(rhs->relation_apid)); | |
| 1731 | ||
| 1732 | // Is this the only parent of the one entry (the recording)? | |
| 1733 | int relation_count = music_source_query_n_relations | |
| 1734 | (PP->source, one->id, APID_GET_TYPE(rhs->relation_apid), | |
| 1735 | APID_GET_PROPERTY_ID(rhs->relation_apid), 2); | |
| 1736 | g_warn_if_fail (relation_count > 0); | |
| 1737 | ||
| 1738 | if (relation_count==1) { | |
| 1739 | // Turns out we are adopting the orphan - remove it from the orphanage. | |
| 1740 | change_trace ("process_adoption: Entry %s %i is adopting orphan %s %i.\n", ENTRY_PF(entry), | |
| 1741 | ENTRY_PF(one)); | |
| 1742 | ||
| 1743 | // Find the clnode representing the orphan. | |
| 1744 | GList *orphan_clnode = rhs->orphanage; | |
| 1745 | g_return_if_fail (orphan_clnode!=NULL); | |
| 1746 | ||
| 1747 | 439 | GList *parent_list = _find_parent_nodes(self, orphan_clnode, one, FIND_PARENT_NODES_WARN); |
| 1748 | 380 | for (GList *parent_list_node=parent_list; parent_list_node!=NULL; |
| 1749 | 439 | parent_list_node=parent_list_node->next) { |
| 1750 | 380 | ViewNode *parent_node = parent_list_node->data; |
| 1751 | 389 | |
| 1752 | 380 | if (parent_node->e_children==-1) { |
| 1753 | // Node has not been read so nothing to do. | |
| 1754 | change_trace ("process_adoption: Parent node %i[%i] (%x) still empty!!\n\n", | |
| 1755 | parent_node->depth, parent_node->n, (guint)parent_node); | |
| 1756 | continue; | |
| 1757 | 389 | }; |
| 1758 | ||
| 1759 | // If the node was read since we started the notify function, it won't actually have | |
| 1760 | // the orphan entry in after all (it won't be stale). | |
| 1761 | if (g_slist_find(_music_source_view_get_initial_read_watch_list | |
| 1762 | (MUSIC_SOURCE_VIEW(self)), parent_node)) { | |
| 1763 | change_trace ("process_adoption: Parent node %x read during notify.\n", | |
| 1764 | (guint)parent_node); | |
| 1765 | continue; | |
| 1766 | }; | |
| 1767 | ||
| 1768 | change_trace ("process_adoption: Removing %s %i from orphanage\n", ENTRY_PF(one)); | |
| 1769 | propagate_alteration (self, REMOVE, orphan_clnode, one, parent_node); | |
| 1770 | 380 | }; |
| 1771 | g_list_free(parent_list); | |
| 1772 | 389 | } else |
| 1773 | change_trace ("process_adoption: Entry %s %i has %i relations.\n", ENTRY_PF(entry)); | |
| 1774 | 380 | }; |
| 1775 | 396 | |
| 1776 | 431 | /* entry_notify_action_deltas: emits 'rows-reordered' and 'row-changed' based on 'deltas' |
| 1777 | * | |
| 1778 | * deltas: a list of RowDelta structs, which represent rows in parent_node that have changed. | |
| 1779 | * A RowDelta stores old and new row number, and a pointer to the node. old_n sometimes | |
| 1780 | * == new_n. | |
| 1781 | */ | |
| 1782 | // FIXME: do we need to emit row_changed when we emit rows_reordered ?? | |
| 1783 | // I assume we do have to. | |
| 1784 | 396 | static void entry_notify_action_deltas (GenericView *self, ViewNode *parent_node, GSList *deltas_a) { |
| 1785 | g_return_if_fail (deltas_a != NULL); | |
| 1786 | 399 | |
| 1787 | 396 | GtkTreeModel *tree_model = GTK_TREE_MODEL(self); |
| 1788 | 431 | |
| 1789 | // To emit rows-reordered, we need to create an array 'new_order' of the format: | |
| 1790 | // new_order[newpos] = old_pos | |
| 1791 | ||
| 1792 | ||
| 1793 | // We work with two lists of the deltas: | |
| 1794 | // deltas_a: sorted by original n (which is done earlier and passed to us), and | |
| 1795 | // deltas_b: sorted by new n (which we do here) | |
| 1796 | 396 | // FIXME: in this and the other place deltas list is sorted, does g_slist_sort have the most |
| 1797 | // appropriate sorting algorithm? | |
| 1798 | 431 | int delta_new_n_compare_func (RowDelta *a, RowDelta *b) { |
| 1799 | 396 | return a->new_n < b->new_n? -1: |
| 1800 | a->new_n > b->new_n? 1: 0; | |
| 1801 | }; | |
| 1802 | GSList *deltas_b = g_slist_copy(deltas_a); | |
| 1803 | deltas_b = g_slist_sort(deltas_b, (GCompareFunc)delta_new_n_compare_func); | |
| 1804 | 399 | |
| 1805 | 438 | change_trace ("\tentry_notify_action_deltas: a %i, b %i. range: 0-%i\n", |
| 1806 | g_slist_length (deltas_a), g_slist_length (deltas_b), parent_node->e_children); | |
| 1807 | 431 | |
| 1808 | RowDelta *next_reordered_delta (GSList **node) { | |
| 1809 | 396 | if (*node==NULL) return NULL; |
| 1810 | RowDelta *delta = (*node)->data; | |
| 1811 | while (delta!=NULL && delta->original_n==delta->new_n) { | |
| 1812 | 431 | change_trace ("\t\tSkipping delta %x. (%i == %i)\n", delta, delta->original_n, delta->new_n); |
| 1813 | 399 | *node = (*node)->next; |
| 1814 | 396 | delta = *node!=NULL? (*node)->data: NULL; |
| 1815 | }; | |
| 1816 | return delta; | |
| 1817 | 399 | }; |
| 1818 | ||
| 1819 | 431 | // Create 'new_order' array to pass to rows_reordered, where new_order[newpos] = oldpos. |
| 1820 | 396 | int *new_order = g_new(int, parent_node->e_children); |
| 1821 | 399 | int shift = 0; |
| 1822 | ||
| 1823 | GSList *a_node = deltas_a, *b_node = deltas_b; | |
| 1824 | 431 | const RowDelta *a_delta = next_reordered_delta(&a_node), |
| 1825 | *b_delta = next_reordered_delta(&b_node); | |
| 1826 | 396 | if (a_delta==NULL && b_delta==NULL) goto no_movements; |
| 1827 | 399 | |
| 1828 | for (int i=0; i<parent_node->e_children; i++) { | |
| 1829 | while (b_delta!=NULL && i==b_delta->new_n) { | |
| 1830 | 438 | //printf ("\tdelta B %i->%i", b_delta->original_n, b_delta->new_n); fflush (stdout); |
| 1831 | 396 | new_order[i] = b_delta->original_n; |
| 1832 | 399 | shift --; |
| 1833 | 431 | b_node = b_node->next; b_delta = next_reordered_delta(&b_node); |
| 1834 | 396 | }; |
| 1835 | 399 | |
| 1836 | while (a_delta!=NULL && (shift>0?(i+shift):i)==a_delta->original_n) { | |
| 1837 | 438 | //printf ("\tdelta A %i->%i", a_delta->original_n, a_delta->new_n); fflush (stdout); |
| 1838 | 396 | shift ++; |
| 1839 | 431 | a_node = a_node->next; a_delta = next_reordered_delta(&a_node); |
| 1840 | 396 | }; |
| 1841 | 399 | |
| 1842 | 431 | //printf ("\tshift %i:\n", shift); fflush (stdout); |
| 1843 | 399 | |
| 1844 | new_order[i] = i + shift; | |
| 1845 | 396 | }; |
| 1846 | 399 | |
| 1847 | 431 | // The number of deltas (old and new) should be the same length. If we didn't reach the end of |
| 1848 | // either list, something is wrong. | |
| 1849 | g_warn_if_fail (a_node==NULL && b_node==NULL); | |
| 1850 | 399 | |
| 1851 | 406 | // Pass NULL as iter if it's the root node. |
| 1852 | GtkTreeIter real_parent_iter, *parent_iter = NULL; | |
| 1853 | if (parent_node->parent_page != NULL) { | |
| 1854 | real_parent_iter.stamp = 0; | |
| 1855 | real_parent_iter.user_data = parent_node->parent_page->parent_node; | |
| 1856 | real_parent_iter.user_data2 = parent_node->parent_page; | |
| 1857 | real_parent_iter.user_data3 = GINT_TO_POINTER(parent_node->n); | |
| 1858 | parent_iter = &real_parent_iter; | |
| 1859 | }; | |
| 1860 | GtkTreePath *parent_path = parent_iter? gtk_tree_model_get_path(tree_model, parent_iter): | |
| 1861 | gtk_tree_path_new(); | |
| 1862 | gtk_tree_model_rows_reordered (tree_model, parent_path, parent_iter, new_order); | |
| 1863 | 396 | gtk_tree_path_free (parent_path); |
| 1864 | ||
| 1865 | no_movements: | |
| 1866 | g_free (new_order); | |
| 1867 | ||
| 1868 | // Emit row-changed for all the deltas, and free them all | |
| 1869 | for (GSList *node = deltas_a; node; node=node->next) { | |
| 1870 | RowDelta *delta = node->data; | |
| 1871 | g_return_if_fail (delta->new_n >= 0); | |
| 1872 | ||
| 1873 | //printf ("Delta: %i -> %i\n", delta->original_n, delta->new_n); fflush (stdout); | |
| 1874 | //if (delta->original_n == delta->new_n) { | |
| 1875 | ViewNode *node = delta->node; | |
| 1876 | GtkTreeIter iter; GtkTreePath *path; | |
| 1877 | iter.user_data = parent_node; | |
| 1878 | iter.user_data2 = node->parent_page; | |
| 1879 | iter.user_data3 = GINT_TO_POINTER(delta->new_n - node->parent_page->start_e); | |
| 1880 | ||
| 1881 | path = gtk_tree_model_get_path (tree_model, &iter); | |
| 1882 | gtk_tree_model_row_changed (tree_model, path, &iter); | |
| 1883 | gtk_tree_path_free (path); | |
| 1884 | 399 | |
| 1885 | 397 | music_source_view_remove_row_marker (MUSIC_SOURCE_VIEW(self), &delta->new_n); |
| 1886 | 396 | |
| 1887 | g_slice_free (RowDelta, delta); | |
| 1888 | }; | |
| 1889 | }; | |
| 1890 | ||
| 1891 | 431 | /* entry_notify: responds to changes in the source and updates the view accordingly. */ |
| 1892 | 185 | // FIXME: these are always going to be too slow, which isn't the end of the world as they're |
| 1893 | // only for adding/changing data rather than playing, but you should still optimise as much | |
| 1894 | // as you can. | |
| 1895 | // | |
| 1896 | 361 | // FIXME: one idea is, don't emit row-changed straight away - group them instead and do it in an |
| 1897 | // idle, so we don't emit ten for the same row. It's dangerous to do that with row-inserted and | |
| 1898 | // row-deleted because row references would then be wrong, and all sorts of tiny fast bugs could | |
| 1899 | // creep in, but it should be okay to do this for row-changed. | |
| 1900 | // | |
| 1901 | 185 | // FIXME: balance max page sizes. (though it won't actually matter if it's one huge page ...) |
| 1902 | // The order of the day for this function is as follows: | |
| 1903 | // find parent node(s) using find_parent_nodes | |
| 1904 | 261 | // for each many:1 join in the level, and then for the tail join, |
| 1905 | // | |
| 1906 | 361 | // |
| 1907 | static void entry_notify (MusicSource *source, Entry *old_entry, Entry *new_entry, | |
| 1908 | MusicSourceView *music_source_view) { | |
| 1909 | 185 | GenericView *self = GENERIC_VIEW(music_source_view); |
| 1910 | 374 | change_trace ("\nGenericView::entry-notify: %s %i -> %s %i\n", ENTRY_PF(old_entry), |
| 1911 | 361 | ENTRY_PF(new_entry)); |
| 1912 | ||
| 1913 | 333 | if (PP->config==NULL) return; |
| 1914 | 361 | |
| 1915 | GList *old_clnode = NULL, *new_clnode = NULL; | |
| 1916 | GList *old_parents = NULL, *new_parents = NULL; | |
| 1917 | ||
| 1918 | // FIXME: These are calculated in a slow way, but it doesn't really matter because the lists | |
| 1919 | // are probably == and of length 1 ... | |
| 1920 | GList *inserted_list = NULL, // * Row-inserted in all of: new_parents - old_parents | |
| 1921 | *changed_list = NULL, // * Row-changed/rows-reordered in: old_parents U new_parents | |
| 1922 | *deleted_list = NULL; // * Row-deleted from all of: old_parents - new_parents | |
| 1923 | ||
| 1924 | if (old_entry == NULL) { | |
| 1925 | 431 | // This new_entry is being added - we only need to trigger if it is a break: the last node |
| 1926 | // on a level, or the many side of a many:1 join. | |
| 1927 | 361 | if (!PP->config->is_break[new_entry->type]) return; |
| 1928 | ||
| 1929 | // Look through the ignore list | |
| 1930 | // FIXME: why is there an ignore list again? | |
| 1931 | // FIXME: is a linked list fast enough? | |
| 1932 | // FIXME: also, empty the list on end transaction, it will have cruft in. | |
| 1933 | // FIXME: also, do this after we reject on entry type? | |
| 1934 | GSList *ignore_lnode = g_slist_find(self->ignore_list[new_entry->type], | |
| 1935 | GINT_TO_POINTER(new_entry->id)); | |
| 1936 | if (ignore_lnode!=NULL) { | |
| 1937 | self->ignore_list[new_entry->type] = | |
| 1938 | g_slist_remove_link(self->ignore_list[new_entry->type], ignore_lnode); | |
| 1939 | //printf("entry-added: ignoring %s %i\n", entry_type_name[type], id); fflush(stdout); | |
| 1940 | return; | |
| 1941 | }; | |
| 1942 | } else { | |
| 1943 | 431 | // This entry has changed - we always need to trigger in this case, to see if the row needs |
| 1944 | // to be reordered. | |
| 1945 | 361 | if (PP->config->index[old_entry->type]==NULL) |
| 1946 | return; | |
| 1947 | 390 | }; |
| 1948 | ||
| 1949 | if (new_entry == NULL) { | |
| 1950 | // Entry removal. | |
| 1951 | if (!PP->config->is_break[old_entry->type]) | |
| 1952 | return; | |
| 1953 | }; | |
| 1954 | ||
| 1955 | _music_source_view_start_initial_read_watch (music_source_view); | |
| 1956 | ||
| 1957 | if (old_entry != NULL) { | |
| 1958 | 418 | // Changed and removed notifications pass 'old_entry'. We are permissive of inconsistency |
| 1959 | // here to allow for the following eventuality: a row such as "track:recording", which | |
| 1960 | // will receive a remove notify for the recording and be deleted, but then receive one for | |
| 1961 | // the track and we will try to remove it again. | |
| 1962 | // FIXME: is there a better way of dealing with this issue? | |
| 1963 | 361 | old_clnode = _find_clnode_for_entry(self, old_entry); |
| 1964 | 439 | old_parents = _find_parent_nodes(self, old_clnode, old_entry, FIND_PARENT_NODES_IGNORE); |
| 1965 | 361 | } |
| 1966 | ||
| 1967 | 390 | if (new_entry != NULL) { |
| 1968 | 361 | // Changed and added notifications pass 'new_entry'. Note the config node could have changed on |
| 1969 | // changed notifications, if an entry was an orphan but is now not. | |
| 1970 | 441 | |
| 1971 | // FIXME: policy should be _WARN unless parent entry is special. Really, find_parent_nodes | |
| 1972 | // should be the one to deal at the moment. | |
| 1973 | 431 | new_clnode = _find_clnode_for_entry(self, new_entry); |
| 1974 | 439 | new_parents = _find_parent_nodes(self, new_clnode, new_entry, FIND_PARENT_NODES_CREATE); |
| 1975 | 361 | }; |
| 1976 | ||
| 1977 | if (old_clnode != new_clnode) { | |
| 1978 | // If adding or removing, or the entry is now in a different part of the config, all parents | |
| 1979 | // will have changed. | |
| 1980 | inserted_list = new_parents; deleted_list = old_parents; | |
| 1981 | } else { | |
| 1982 | for (GList *old_node=old_parents; old_node; old_node=old_node->next) { | |
| 1983 | GList *new_node = g_list_find(new_parents, old_node->data); | |
| 1984 | if (new_node != NULL) { | |
| 1985 | // This node is in old_parents U new_parents | |
| 1986 | changed_list = g_list_prepend (changed_list, old_node->data); | |
| 1987 | new_parents = g_list_remove(new_parents, old_node->data); | |
| 1988 | } else | |
| 1989 | // This node has gone - it's in old_parents - new_parents | |
| 1990 | deleted_list = g_list_prepend (deleted_list, old_node->data); | |
| 1991 | }; | |
| 1992 | inserted_list = new_parents; | |
| 1993 | g_list_free (old_parents); | |
| 1994 | }; | |
| 1995 | ||
| 1996 | 354 | // FIXME: what's the optimal order from gtktreeview's point of view to emit these? Not that it |
| 1997 | // really matters. | |
| 1998 | ||
| 1999 | 374 | change_trace ("\n\tInserted in %i, changed in %i, deleted in %i\n", g_list_length(inserted_list), |
| 2000 | 361 | g_list_length(changed_list), g_list_length(deleted_list)); |
| 2001 | ||
| 2002 | 354 | // Delete old rows! |
| 2003 | 361 | // FIXME: broken for m:1 joins .. |
| 2004 | 354 | for (GList *node=deleted_list; node; node=node->next) { |
| 2005 | 361 | ViewNode *parent_node = node->data; |
| 2006 | 442 | |
| 2007 | 361 | if (parent_node->e_children==-1) { |
| 2008 | 442 | // If the node isn't yet read, there's no need to do anything, unless it is a special |
| 2009 | // entry: in this case, we read it to see if it's now empty and in need of hiding. | |
| 2010 | change_trace ("Remving: parent_tail %s.\n", CONFIG_TYPE_NAME(parent_node->parent_tail)); | |
| 2011 | if (parent_node->parent_tail != NULL | |
| 2012 | && ENTRY_IS_SPECIAL(CONFIG_NODE(parent_node->parent_tail)->type, parent_node->id)) { | |
| 2013 | // Remember that this is reading the *current* state, so we don't need to propagate | |
| 2014 | // the entry removal inside this node. | |
| 2015 | _music_source_view_initial_read (MUSIC_SOURCE_VIEW(self), parent_node); | |
| 2016 | if (parent_node->n_children==0) | |
| 2017 | _remove_row_in_page (self, parent_node->parent_page->parent_node, | |
| 2018 | parent_node->parent_page, parent_node->n, TRUE, TRUE); | |
| 2019 | } else | |
| 2020 | change_trace ("entry-notify: deleted list: Node has not yet been read.\n\n"); | |
| 2021 | 361 | continue; |
| 2022 | 365 | }; |
| 2023 | ||
| 2024 | propagate_alteration (self, REMOVE, old_clnode, old_entry, parent_node); | |
| 2025 | 354 | }; |
| 2026 | ||
| 2027 | // Insert new rows! | |
| 2028 | for (GList *node=inserted_list; node; node=node->next) { | |
| 2029 | 361 | ViewNode *parent_node = node->data; |
| 2030 | 389 | if (parent_node->e_children!=-1) |
| 2031 | propagate_alteration (self, INSERT, new_clnode, new_entry, parent_node); | |
| 2032 | 420 | else { |
| 2033 | // Node has not been read yet; row-inserted is not triggered in this case; this is fine | |
| 2034 | // because only the treeview and row references will connect to that signal anyway. | |
| 2035 | 389 | change_trace ("entry-notify[add %s %d]: Parent node %i[%i] (%x) still empty!!\n\n", |
| 2036 | 420 | ENTRY_PF(new_entry), parent_node->depth, parent_node->n, |
| 2037 | (guint)parent_node); | |
| 2038 | ||
| 2039 | // We do however need to emit row-has-child-toggled if we hadn't already. | |
| 2040 | if (!parent_node->has_child_toggled_emitted) { | |
| 2041 | // FIXME: make an easier (= more concise) way to do this. .. | |
| 2042 | GtkTreeIter iter; | |
| 2043 | iter.user_data = parent_node->parent_page->parent_node; | |
| 2044 | iter.user_data2 = parent_node->parent_page; | |
| 2045 | iter.user_data3 = GINT_TO_POINTER(parent_node->n); | |
| 2046 | GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(self), &iter); | |
| 2047 | change_trace ("entry-notify: emitting has-child-toggled, at: %s.\n", | |
| 2048 | gtk_tree_path_to_string(path)); | |
| 2049 | gtk_tree_model_row_has_child_toggled (GTK_TREE_MODEL(self), path, &iter); | |
| 2050 | gtk_tree_path_free (path); | |
| 2051 | 422 | parent_node->has_child_toggled_emitted = TRUE; |
| 2052 | 420 | } |
| 2053 | }; | |
| 2054 | 380 | |
| 2055 | // If this is a m:1 join entry, see if it's adopted any orphans (ie. see if this is the | |
| 2056 | // first track for a recording which previously had none - the recording now needs removing | |
| 2057 | // from the orphanage. | |
| 2058 | if (new_clnode->next | |
| 2059 | && CONFIG_NODE(new_clnode->next)->flags & VIEW_CONFIG_NODE_MANY_TO_ONE_JOIN) | |
| 2060 | 389 | entry_notify_process_adoption (self, new_clnode, new_entry); |
| 2061 | 354 | }; |
| 2062 | ||
| 2063 | 389 | _music_source_view_end_initial_read_watch (music_source_view); |
| 2064 | ||
| 2065 | 361 | // Update rows that still exist! This is more complicated than it seems on the surface. Here are |
| 2066 | // the assumptions the code makes: | |
| 2067 | // * an entry can have more than one row inside each node - eg, a recording on two albums | |
| 2068 | // * the number of rows within a node *can* change when the entry changes - if a file is set | |
| 2069 | // to point to a different recording, for example. | |
| 2070 | // | |
| 2071 | // Here is what could have happened: | |
| 2072 | // | |
| 2073 | // The method (for each parent node): | |
| 2074 | 369 | if (changed_list != NULL) { |
| 2075 | 354 | g_warn_if_fail (old_clnode == new_clnode); |
| 2076 | 369 | |
| 2077 | for (GList *node=changed_list; node; node=node->next) { | |
| 2078 | GSList *deltas = NULL; | |
| 2079 | ViewNode *parent_node = node->data; | |
| 2080 | if (parent_node->e_children==-1) { | |
| 2081 | // Node has not been read yet; nothing to do (obviously it's not displayed yet). | |
| 2082 | change_trace ("entry-notify: Parent node %i[%i] (%x) still empty!!\n\n", | |
| 2083 | parent_node->depth, parent_node->n, (guint)parent_node); | |
| 2084 | continue; | |
| 2085 | } | |
| 2086 | ||
| 2087 | propagate_change (self, old_clnode, old_entry, new_entry, parent_node, &deltas); | |
| 2088 | 399 | |
| 2089 | 396 | if (deltas != NULL) { |
| 2090 | entry_notify_action_deltas (self, parent_node, deltas); | |
| 2091 | g_slist_free (deltas); | |
| 2092 | 378 | }; |
| 2093 | 369 | }; |
| 2094 | 364 | }; |
| 2095 | 361 | |
| 2096 | 369 | |
| 2097 | 361 | _music_source_view_check_tree (MUSIC_SOURCE_VIEW(self), NULL); |
| 2098 | g_list_free (deleted_list); g_list_free (inserted_list); g_list_free (changed_list); | |
| 2099 | 185 | }; |
| 2100 | ||
| 2101 | /////////////////////////////////////// | |
| 2102 | // Debugging | |
| 2103 | // | |
| 2104 | ||
| 2105 | 434 | static void reading_trace (int level, const char *format, ...) { |
| 2106 | #if defined(GENERIC_VIEW_TRACE_READING) && GENERIC_VIEW_TRACE_READING > 0 | |
| 2107 | va_list va; va_start (va, format); | |
| 2108 | 436 | TRACE(GENERIC_VIEW_TRACE_READING, level, format, va); |
| 2109 | 434 | va_end (va); |
| 2110 | #endif | |
| 2111 | }; | |
| 2112 | ||
| 2113 | 206 | static void change_trace (char *format, ...) { |
| 2114 | #ifdef GENERIC_VIEW_TRACE_CHANGES | |
| 2115 | va_list va; va_start (va, format); | |
| 2116 | vprintf (format, va); | |
| 2117 | fflush (stdout); | |
| 2118 | va_end (va); | |
| 2119 | #endif | |
| 2120 | }; |
Loggerhead is a web-based interface for Bazaar branches