1/*
2* Copyright (c) 2018 (https://github.com/phase1geo/Minder)
3*
4* This program is free software; you can redistribute it and/or
5* modify it under the terms of the GNU General Public
6* License as published by the Free Software Foundation; either
7* version 2 of the License, or (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 GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*
19* Authored by: Trevor Williams <phase1geo@gmail.com>
20*/
21
22using GLib;
23using Gdk;
24using Gee;
25using Xml;
26
27public class ExportXMind : Export {
28
29  private int ids = 10000000;
30
31  public enum IdObjectType {
32    NODE = 0,
33    CONNECTION,
34    BOUNDARY
35  }
36
37  public class IdObject {
38    public IdObjectType typ   { get; set; default = IdObjectType.NODE; }
39    public Node?        node  { get; set; default = null; }
40    public Connection?  conn  { get; set; default = null; }
41    public NodeGroup?   group { get; set; default = null; }
42    public IdObject.for_node( Node n ) {
43      typ  = IdObjectType.NODE;
44      node = n;
45    }
46    public IdObject.for_connection( Connection c ) {
47      typ  = IdObjectType.CONNECTION;
48      conn = c;
49    }
50    public IdObject.for_boundary( NodeGroup g ) {
51      typ   = IdObjectType.BOUNDARY;
52      group = g;
53    }
54  }
55
56  public class FileItem {
57    public string name;
58    public string type;
59    public FileItem( string n, string t ) {
60      name = n;
61      type = t;
62    }
63  }
64
65  public class FileItems {
66    public Array<FileItem> items;
67    public FileItems() {
68      items = new Array<FileItem>();
69    }
70    public void add( string name, string type ) {
71      for( int i=0; i<items.length; i++ ) {
72        if( items.index( i ).name == name ) return;
73      }
74      items.append_val( new FileItem( name, type ) );
75    }
76  }
77
78  /* Constructor */
79  public ExportXMind() {
80    base( "xmind-8", _( "XMind 8" ), { ".xmind" }, true, true );
81  }
82
83  /* Exports the given drawing area to the file of the given name */
84  public override bool export( string fname, DrawArea da ) {
85
86    /* Create temporary directory to place contents in */
87    var dir = DirUtils.mkdtemp( "minderXXXXXX" );
88
89    var styles    = new Array<Xml.Node*>();
90    var file_list = new FileItems();
91
92    /* Export the meta file */
93    export_meta( da, dir, file_list );
94
95    /* Export the content file */
96    export_content( da, dir, file_list, styles );
97
98    if( styles.length > 0 ) {
99      export_styles( dir, file_list, styles );
100    }
101
102    /* Export manifest file */
103    export_manifest( dir, file_list );
104
105    /* Archive the contents */
106    archive_contents( dir, fname, file_list );
107
108    return( true );
109
110  }
111
112  /* Generates the manifest file */
113  private bool export_manifest( string dir, FileItems file_list ) {
114
115    Xml.Doc* doc = new Xml.Doc( "1.0" );
116    Xml.Node* manifest = new Xml.Node( null, "manifest" );
117
118    manifest->set_prop( "xmlns", "urn:xmind:xmap:xmlns:manifest:1.0" );
119    manifest->set_prop( "password-hint", "" );
120
121    manifest->add_child( manifest_file_entry( "META-INF", "" ) );
122    manifest->add_child( manifest_file_entry( "META-INF/manifest.xml", "text/xml" ) );
123
124    for( int i=0; i<file_list.items.length; i++ ) {
125      var mfile = file_list.items.index( i );
126      manifest->add_child( manifest_file_entry( mfile.name, mfile.type ) );
127    }
128
129    doc->set_root_element( manifest );
130
131    var meta_dir = Path.build_filename( dir, "META-INF" );
132    DirUtils.create( meta_dir, 0755 );
133    doc->save_format_file( Path.build_filename( meta_dir, "manifest.xml" ), 1 );
134
135    file_list.add( "META-INF", "" );
136    file_list.add( Path.build_filename( "META-INF", "manifest.xml" ), "text/xml" );
137
138    delete doc;
139
140    return( false );
141
142  }
143
144  /* Creates the file-entry node within the manifest */
145  private Xml.Node* manifest_file_entry( string path, string type ) {
146    Xml.Node* n = new Xml.Node( null, "file-entry" );
147    n->set_prop( "full-path", path );
148    n->set_prop( "media-type", type );
149    return( n );
150  }
151
152  /* Generate the main content file from  */
153  private bool export_content( DrawArea da, string dir, FileItems file_list, Array<Xml.Node*> styles ) {
154
155    Xml.Doc*  doc       = new Xml.Doc( "1.0" );
156    Xml.Node* xmap      = new Xml.Node( null, "xmap-content" );
157    Xml.Node* sheet     = new Xml.Node( null, "sheet" );
158    Xml.Node* title     = new Xml.Node( null, "title" );
159    var       timestamp = new DateTime.now().to_unix().to_string();
160
161    xmap->set_prop( "xmlns", "urn:xmind:xmap:xmlns:content:2.0" );
162    xmap->set_prop( "xmlns:fo", "http://www.w3.org/1999/XSL/Format" );
163    xmap->set_prop( "xmlns:svg", "http://www.w3.org/2000/svg" );
164    xmap->set_prop( "xmlns:xhtml", "http://www.w3.org/1999/xhtml" );
165    xmap->set_prop( "xmlns:xlink", "http://www.w3.org/1999/xlink" );
166    xmap->set_prop( "timestamp", timestamp );
167    xmap->set_prop( "version", "2.0" );
168    sheet->set_prop( "id", "1" );
169    sheet->set_prop( "timestamp", timestamp );
170
171    export_map( da, sheet, timestamp, dir, file_list, styles );
172
173    title->add_content( "Sheet 1" );
174    sheet->add_child( title );
175    xmap->add_child( sheet );
176
177    doc->set_root_element( xmap );
178    doc->save_format_file( Path.build_filename( dir, "content.xml" ), 1 );
179
180    file_list.add( "content.xml", "text/xml" );
181
182    delete doc;
183
184    return( false );
185
186  }
187
188  /* Exports the map contents */
189  private void export_map( DrawArea da, Xml.Node* sheet, string timestamp, string dir, FileItems file_list, Array<Xml.Node*> styles ) {
190    var nodes = da.get_nodes();
191    var conns = da.get_connections().connections;
192    Xml.Node* top = export_node( da, nodes.index( 0 ), timestamp, true, dir, file_list, styles );
193    if( nodes.length > 1 ) {
194      for( Xml.Node* it=top->children; it!=null; it=it->next ) {
195        if( (it->type == ElementType.ELEMENT_NODE) && (it->name == "children") ) {
196          Xml.Node* topics = new Xml.Node( null, "topics" );
197          topics->set_prop( "type", "detached" );
198          for( int i=1; i<nodes.length; i++ ) {
199            Xml.Node* topic = export_node( da, nodes.index( i ), timestamp, false, dir, file_list, styles );
200            Xml.Node* pos   = new Xml.Node( null, "position" );
201            var       x     = (int)(nodes.index( i ).posx - nodes.index( 0 ).posx);
202            var       y     = (int)(nodes.index( i ).posy - nodes.index( 0 ).posy);
203            pos->set_prop( "svg:x", x.to_string() );
204            pos->set_prop( "svg:y", y.to_string() );
205            topic->add_child( pos );
206            topics->add_child( topic );
207          }
208          it->add_child( topics );
209        }
210      }
211    }
212    sheet->add_child( top );
213    export_connections( da, sheet, timestamp, styles );
214  }
215
216  private Xml.Node* export_node( DrawArea da, Node node, string timestamp, bool top, string dir, FileItems file_list, Array<Xml.Node*> styles ) {
217
218    Xml.Node* topic = new Xml.Node( null, "topic" );
219    Xml.Node* title = new Xml.Node( null, "title" );
220    var       sid   = ids++;
221
222    topic->set_prop( "id", node.id().to_string() );
223    topic->set_prop( "style-id", sid.to_string() );
224    // topic->set_prop( "modified-by", TBD );
225    topic->set_prop( "timestamp", timestamp );
226    if( top ) {
227      topic->set_prop( "structure-class", "org.xmind.ui.map.unbalanced" );
228    }
229
230    title->add_content( node.name.text.text );
231    topic->add_child( title );
232
233    /* Add note, if needed */
234    if( node.note != "" ) {
235      topic->add_child( export_node_note( node, styles ) );
236    }
237
238    /* Add styling information */
239    Xml.Node* nstyle = new Xml.Node( null, "style" );
240    Xml.Node* nprops = new Xml.Node( null, "topic-properties" );
241    nstyle->set_prop( "id", sid.to_string() );
242    nstyle->set_prop( "type", "topic" );
243    export_node_style( node, nprops );
244    nstyle->add_child( nprops );
245    styles.append_val( nstyle );
246
247    /* Add image, if needed */
248    if( node.image != null ) {
249      export_image( da, node, dir, file_list, topic );
250    }
251
252    if( node.children().length > 0 ) {
253
254      var groups = new Array<int>();
255
256      Xml.Node* children = new Xml.Node( null, "children" );
257      Xml.Node* topics   = new Xml.Node( null, "topics" );
258      topics->set_prop( "type", "attached" );
259
260      for( int i=0; i<node.children().length; i++ ) {
261        var child = node.children().index( i );
262        topics->add_child( export_node( da, child, timestamp, false, dir, file_list, styles ) );
263        if( child.group ) {
264          groups.append_val( i );
265        }
266      }
267
268      children->add_child( topics );
269      topic->add_child( children );
270
271      /* Add boundaries, if found */
272      if( groups.length > 0 ) {
273        Xml.Node* boundaries = new Xml.Node( null, "boundaries" );
274        for( int i=0; i<groups.length; i++ ) {
275
276          Xml.Node* boundary = new Xml.Node( null, "boundary" );
277          Xml.Node* style    = new Xml.Node( null, "style" );
278          Xml.Node* props    = new Xml.Node( null, "boundary-properties" );
279          int       id       = ids++;
280          int       stid     = ids++;
281
282          /* Create boundary */
283          boundary->set_prop( "id", id.to_string() );
284          boundary->set_prop( "style-id", stid.to_string() );
285          boundary->set_prop( "range", "(%d,%d)".printf( groups.index( i ), groups.index( i ) ) );
286          boundary->set_prop( "timestamp", timestamp );
287          boundaries->add_child( boundary );
288
289          /* Create styling node */
290          style->set_prop( "id", stid.to_string() );
291          style->set_prop( "type", "boundary" );
292          props->set_prop( "svg:fill", Utils.color_from_rgba( node.children().index( groups.index( i ) ).link_color ) );
293          style->add_child( props );
294          styles.append_val( style );
295
296        }
297        topic->add_child( boundaries );
298      }
299
300    }
301
302    return( topic );
303
304  }
305
306  /* Exports the given node's image */
307  private void export_image( DrawArea da, Node node, string dir, FileItems file_list, Xml.Node* topic ) {
308
309    var img_name  = da.image_manager.get_file( node.image.id );
310    var mime_type = da.image_manager.get_mime_type( node.image.id );
311    var src       = Path.build_filename( "attachments", Filename.display_basename( img_name ) );
312    var parts     = src.split( "." );
313    Xml.Node* img = new Xml.Node( null, "xhtml:img" );
314
315    /* XMind doesn't support SVG images so cut short if we have this type of image */
316    if( mime_type == "image/svg" ) return;
317
318    /* Copy the image file to the XMind bundle */
319    DirUtils.create( Path.build_filename( dir, "attachments" ), 0755 );
320    var lfile = File.new_for_path( Path.build_filename( dir, src ) );
321    var rfile = File.new_for_path( img_name );
322    try {
323      rfile.copy( lfile, FileCopyFlags.OVERWRITE );
324    } catch( GLib.Error e ) {
325      return;
326    }
327
328    img->set_prop( "style-id", (ids++).to_string() );  /* TBD - We need to store this for later use */
329    img->set_prop( "svg:height", node.image.height.to_string() );
330    img->set_prop( "svg:width",  node.image.width.to_string() );
331    img->set_prop( "xhtml:src",  "xap:%s".printf( src ) );
332    topic->add_child( img );
333
334    file_list.add( "attachments", "" );
335    file_list.add( src, mime_type );
336
337  }
338
339  /* Exports node styling information */
340  private void export_node_style( Node node, Xml.Node* n ) {
341
342    /* Node border shape */
343    switch( node.style.node_border.name() ) {
344      case "rounded"    :  n->set_prop( "shape-class", "org.xmind.topicShape.roundedRect" );  break;
345      case "underlined" :  n->set_prop( "shape-class", "org.xmind.topicShape.underline" );    break;
346      default           :  n->set_prop( "shape-class", "org.xmind.topicShape.rect" );         break;
347    }
348
349    n->set_prop( "border-line-color", Utils.color_from_rgba( node.link_color ) );
350    n->set_prop( "border-line-width", "%dpt".printf( node.style.node_borderwidth ) );
351    n->set_prop( "line-color",        Utils.color_from_rgba( node.link_color ) );
352    n->set_prop( "line-width",        "%dpt".printf( node.style.link_width ) );
353
354    if( node.style.node_fill ) {
355      n->set_prop( "svg:fill", Utils.color_from_rgba( node.link_color ) );
356    }
357
358    switch( node.style.link_type.name() ) {
359      case "curved"   :  n->set_prop( "line-class", "org.xmind.branchConnection.%s".printf( node.style.link_arrow ? "arrowedCurve" : "curve" ) );  break;
360      case "straight" :  n->set_prop( "line-class", "org.xmind.branchConnection.straight" );  break;
361      case "squared"  :  n->set_prop( "line-class", "org.xmind.branchConnection.elbow" );     break;
362    }
363
364  }
365
366  /* Exports a node note */
367  private Xml.Node* export_node_note( Node node, Array<Xml.Node*> styles ) {
368
369    Xml.Node* note  = new Xml.Node( null, "notes" );
370    Xml.Node* plain = new Xml.Node( null, "plain" );
371
372    plain->add_content( node.note );
373
374    var note_html = replace_formatting( Utils.markdown_to_html( node.note, "html" ), styles );
375    var note_doc  = Xml.Parser.parse_memory( note_html, note_html.length );
376    var html      = note_doc->get_root_element()->copy( 1 );
377
378    note->add_child( plain );
379    note->add_child( html );
380
381    return( note );
382
383  }
384
385  /* Converts the given HTML string into the XHTML equivalent */
386  private string replace_formatting( string str, Array<Xml.Node*> styles ) {
387
388    var bold_id = ids++;
389    var em_id   = ids++;
390
391    Xml.Node* bold_style = new Xml.Node( null, "style" );
392    Xml.Node* em_style   = new Xml.Node( null, "style" );
393    Xml.Node* bold_props = new Xml.Node( null, "text-properties" );
394    Xml.Node* em_props   = new Xml.Node( null, "text-properties" );
395
396    bold_props->set_prop( "id", bold_id.to_string() );
397    bold_props->set_prop( "fo:font-weight", "bold" );
398    bold_style->add_child( bold_props );
399
400    em_props->set_prop( "id", em_id.to_string() );
401    em_props->set_prop( "fo:font-style", "italic" );
402    em_style->add_child( em_props );
403
404    styles.append_val( bold_style );
405    styles.append_val( em_style );
406
407    /* Perform simple one-for-one replacements */
408    return( str.replace( "<p>",       "<xhtml:p>" )
409               .replace( "</p>",      "</xhtml:p>" )
410               .replace( "<a href",   "<xhtml:a xlink:href" )
411               .replace( "</a>",      "</xhtml:a>" )
412               .replace( "<strong>",  "<xhtml:span style-id=\"%d\">".printf( bold_id ) )
413               .replace( "</strong>", "</xhtml:span>" )
414               .replace( "<em>",      "<xhtml:span style-id=\"%d\">".printf( em_id ) )
415               .replace( "</em>",     "</xhtml:span>" ) );
416
417  }
418
419  private void export_connections( DrawArea da, Xml.Node* sheet, string timestamp, Array<Xml.Node*> styles ) {
420
421    var conns = da.get_connections().connections;
422
423    if( conns.length > 0 ) {
424
425      Xml.Node* relations = new Xml.Node( null, "relationships" );
426
427      for( int i=0; i<conns.length; i++ ) {
428        var conn     = conns.index( i );
429        var conn_id  = ids++;
430        var style_id = ids++;
431        var color    = (conn.color == null) ? da.get_theme().get_color( "connection_background" ) : conn.color;
432        var dash     = "dash";
433
434        switch( conn.style.connection_dash.name ) {
435          case "solid"  :  dash = "solid";  break;
436          case "dotted" :  dash = "dot";    break;
437        }
438
439        Xml.Node* relation = new Xml.Node( null, "relationship" );
440        relation->set_prop( "end1", conn.from_node.id().to_string() );
441        relation->set_prop( "end2", conn.to_node.id().to_string() );
442        relation->set_prop( "id", conn_id.to_string() );
443        relation->set_prop( "style-id", style_id.to_string() );
444        // relation->set_prop( "modified-by", TBD );
445        relation->set_prop( "timestamp", timestamp );
446
447        if( conn.title != null ) {
448          Xml.Node* title = new Xml.Node( null, "title" );
449          title->add_content( conn.title.text.text );
450          relation->add_child( title );
451        }
452
453        relations->add_child( relation );
454
455        /* Create style */
456        Xml.Node* style = new Xml.Node( null, "style" );
457        Xml.Node* props = new Xml.Node( null, "relationship-properties" );
458        var from_arrow = (conn.style.connection_arrow == "tofrom") || (conn.style.connection_arrow == "both");
459        var to_arrow   = (conn.style.connection_arrow == "fromto") || (conn.style.connection_arrow == "both");
460        props->set_prop( "arrow-begin-class", "org.xmind.arrowShape.%s".printf( from_arrow ? "triangle" : "none" ) );
461        props->set_prop( "arrow-end-class",   "org.xmind.arrowShape.%s".printf( to_arrow   ? "triangle" : "none" ) );
462        props->set_prop( "line-color", Utils.color_from_rgba( color ) );
463        props->set_prop( "line-pattern", dash );
464        props->set_prop( "line-width", "%dpt".printf( conn.style.connection_line_width ) );
465        props->set_prop( "shape-class", "org.xmind.relationshipShape.curved" );
466        style->add_child( props );
467        styles.append_val( style );
468
469      }
470
471      sheet->add_child( relations );
472
473    }
474
475  }
476
477  /* Creates the styles.xml file in the main directory */
478  private void export_styles( string dir, FileItems file_list, Array<Xml.Node*> nodes ) {
479
480    Xml.Doc*  doc    = new Xml.Doc( "1.0" );
481    Xml.Node* xmap   = new Xml.Node( null, "xmap-styles" );
482    Xml.Node* styles = new Xml.Node( null, "styles" );
483
484    xmap->set_prop( "xmlns", "urn:xmind:xmap:xmlns:content:2.0" );
485    xmap->set_prop( "xmlns:fo", "http://www.w3.org/1999/XSL/Format" );
486    xmap->set_prop( "xmlns:svg", "http://www.w3.org/2000/svg" );
487    xmap->set_prop( "version", "2.0" );
488
489    for( int i=0; i<nodes.length; i++ ) {
490      styles->add_child( nodes.index( i ) );
491    }
492
493    xmap->add_child( styles );
494
495    doc->set_root_element( xmap );
496    doc->save_format_file( Path.build_filename( dir, "styles.xml" ), 1 );
497
498    file_list.add( "styles.xml", "text/xml" );
499
500    delete doc;
501
502  }
503
504  /* Exports the contents of the meta file */
505  private void export_meta( DrawArea da, string dir, FileItems file_list ) {
506
507    Xml.Doc*  doc       = new Xml.Doc( "1.0" );
508    Xml.Node* meta      = new Xml.Node( null, "meta" );
509    Xml.Node* create    = new Xml.Node( null, "Create" );
510    Xml.Node* time      = new Xml.Node( null, "Time" );
511    Xml.Node* creator   = new Xml.Node( null, "Creator" );
512    Xml.Node* name      = new Xml.Node( null, "Name" );
513    Xml.Node* version   = new Xml.Node( null, "Version" );
514    var       timestamp = new DateTime.now().to_string();
515
516    meta->set_prop( "xmlns", "urn:xmind:xmap:xmlns:meta:2.0" );
517    meta->set_prop( "version", "2.0" );
518
519    time->set_content( timestamp );
520    name->set_content( "Minder" );
521    version->set_content( Minder.version );
522
523    create->add_child( time );
524    meta->add_child( create );
525
526    creator->add_child( name );
527    creator->add_child( version );
528    meta->add_child( creator );
529
530    doc->set_root_element( meta );
531    doc->save_format_file( Path.build_filename( dir, "meta.xml" ), 1 );
532
533    file_list.add( "meta.xml", "text/xml" );
534
535    delete doc;
536
537  }
538
539  /* Write the contents as a zip file */
540  private void archive_contents( string dir, string outname, FileItems files ) {
541
542    GLib.File pwd = GLib.File.new_for_path( dir );
543
544    // Create the tar.gz archive named according the the first argument.
545    Archive.Write archive = new Archive.Write();
546    archive.add_filter_none();
547    archive.set_format_zip();
548    archive.open_filename( outname );
549
550    // Add all the other arguments into the archive
551    for( int i=0; i<files.items.length; i++ ) {
552      if( files.items.index( i ).type == "" ) continue;
553      GLib.File file = GLib.File.new_for_path( Path.build_filename( dir, files.items.index( i ).name ) );
554      try {
555        GLib.FileInfo file_info = file.query_info( GLib.FileAttribute.STANDARD_SIZE, GLib.FileQueryInfoFlags.NONE );
556        FileInputStream input_stream = file.read();
557        DataInputStream data_input_stream = new DataInputStream( input_stream );
558
559        // Add an entry to the archive
560        Archive.Entry entry = new Archive.Entry();
561        entry.set_pathname( pwd.get_relative_path( file ) );
562#if VALAC048
563        entry.set_size( (Archive.int64_t)file_info.get_size() );
564        entry.set_filetype( Archive.FileType.IFREG );
565        entry.set_perm( (Archive.FileMode)0644 );
566#else
567        entry.set_size( file_info.get_size() );
568        entry.set_filetype( (uint)Posix.S_IFREG );
569        entry.set_perm( 0644 );
570#endif
571        if( archive.write_header( entry ) != Archive.Result.OK ) {
572          critical( "Error writing '%s': %s (%d)", file.get_path(), archive.error_string(), archive.errno() );
573          continue;
574        }
575
576        // Add the actual content of the file
577        size_t bytes_read;
578        uint8[] buffer = new uint8[64];
579        while( data_input_stream.read_all( buffer, out bytes_read ) ) {
580          if( bytes_read <= 0 ) {
581            break;
582          }
583#if VALAC048
584          archive.write_data( buffer );
585#else
586          archive.write_data( buffer, bytes_read );
587#endif
588        }
589      } catch( GLib.Error e ) {
590        critical( e.message );
591      }
592    }
593
594    if( archive.close() != Archive.Result.OK ) {
595      error( "Error : %s (%d)", archive.error_string(), archive.errno() );
596    }
597
598  }
599
600  // --------------------------------------------------------------------------------------
601
602  /* Main method used to import an XMind mind-map into Minder */
603  public override bool import( string fname, DrawArea da ) {
604
605    /* Create temporary directory to place contents in */
606    var dir = DirUtils.mkdtemp( "minderXXXXXX" );
607
608    /* Unarchive the files */
609    unarchive_contents( fname, dir );
610
611    var content = Path.build_filename( dir, "content.xml" );
612    var id_map  = new HashMap<string,IdObject>();
613    import_content( da, content, dir, id_map );
614
615    var styles = Path.build_filename( dir, "styles.xml" );
616    import_styles( da, styles, id_map );
617
618    /* Update the drawing area and save the result */
619    da.queue_draw();
620    da.auto_save();
621
622    return( true );
623
624  }
625
626  /* Import the content file */
627  private bool import_content( DrawArea da, string fname, string dir, HashMap<string,IdObject> id_map ) {
628
629    /* Read in the contents of the Freemind file */
630    var doc = Xml.Parser.read_file( fname, null, Xml.ParserOption.HUGE );
631    if( doc == null ) {
632      return( false );
633    }
634
635    /* Load the contents of the file */
636    import_map( da, doc->get_root_element(), dir, id_map );
637
638    /* Delete the OPML document */
639    delete doc;
640
641    return( true );
642
643  }
644
645  /* Import the xmind map */
646  private void import_map( DrawArea da, Xml.Node* n, string dir, HashMap<string,IdObject> id_map ) {
647
648    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
649      if( (it->type == ElementType.ELEMENT_NODE) && (it->name == "sheet") ) {
650        import_sheet( da, it, dir, id_map );
651        return;
652      }
653    }
654  }
655
656  /* Import a sheet */
657  private void import_sheet( DrawArea da, Xml.Node* n, string dir, HashMap<string,IdObject> id_map ) {
658
659    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
660      if( it->type == ElementType.ELEMENT_NODE ) {
661        switch( it->name ) {
662          case "topic"         :  import_topic( da, null, it, false, dir, id_map );  break;
663          case "relationships" :  import_relationships( da, it, id_map );  break;
664        }
665      }
666    }
667
668  }
669
670  /* Imports an XMind topic (this is a node in Minder) */
671  private void import_topic( DrawArea da, Node? parent, Xml.Node* n, bool attached, string dir, HashMap<string,IdObject> id_map ) {
672
673    Node node;
674
675    string? sclass = n->get_prop( "structure-class" );
676    if( sclass != null ) {
677      node = da.create_root_node();
678      if( sclass == "org.xmind.ui.map.unbalanced" ) {
679        node.layout = da.layouts.get_layout( _( "Horizontal" ) );
680      } else {
681        node.layout = da.layouts.get_layout( _( "To right" ) );
682      }
683    } else if( !attached ) {
684      node = da.create_root_node();
685    } else {
686      node = da.create_child_node( parent );
687    }
688
689    /* Handle the ID */
690    string? id = n->get_prop( "id" );
691    if( id != null ) {
692      id_map.set( id, new IdObject.for_node( node ) );
693    }
694
695    string? sid = n->get_prop( "style-id" );
696    if( sid != null ) {
697      id_map.set( sid, new IdObject.for_node( node ) );
698    }
699
700    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
701      if( it->type == ElementType.ELEMENT_NODE ) {
702        switch( it->name ) {
703          case "title"      :  import_node_name( node, it );               break;
704          case "notes"      :  import_node_notes( node, it );              break;
705          case "img"        :  import_image( da, node, it, dir, id_map );     break;
706          case "children"   :  import_children( da, node, it, dir, id_map );  break;
707          case "boundaries" :  import_boundaries( da, node, it, id_map );  break;
708        }
709      }
710    }
711
712  }
713
714  /* Returns the string stored in a <title> node */
715  private string get_title( Xml.Node* n ) {
716    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
717      if( it->type == ElementType.TEXT_NODE ) {
718        return( it->content );
719      }
720    }
721    return( "" );
722  }
723
724  /* Imports the node name information */
725  private void import_node_name( Node node, Xml.Node* n ) {
726    node.name.text.insert_text( 0, get_title( n ) );
727  }
728
729  /* Imports the node note */
730  private void import_node_notes( Node node, Xml.Node* n ) {
731
732    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
733      if( it->type == ElementType.ELEMENT_NODE ) {
734        switch( it->name ) {
735          case "plain" :  import_note_plain( node, it );  break;
736        }
737      }
738    }
739
740  }
741
742  /* Imports the node note as plain text */
743  private void import_note_plain( Node node, Xml.Node* n ) {
744    node.note = get_title( n );
745  }
746
747  /* Imports an image from a file */
748  private void import_image( DrawArea da, Node node, Xml.Node* n, string dir, HashMap<string,IdObject> id_map ) {
749
750    int height = 1;
751    int width  = 1;
752
753    string? sid = n->get_prop( "style-id" );
754    if( sid != null ) {
755      // TBD - We need to associate styles to things that are not just nodes
756    }
757
758    string? h = n->get_prop( "height" );
759    if( h != null ) {
760      height = int.parse( h );
761    }
762
763    string? w = n->get_prop( "width" );
764    if( w != null ) {
765      width = int.parse( w );
766    }
767
768    string? src = n->get_prop( "src" );
769    if( src != null ) {
770      var img_file = File.new_for_path( Path.build_filename( dir, src.substring( 4 ) ) );
771      node.set_image( da.image_manager, new NodeImage.from_uri( da.image_manager, img_file.get_uri(), width ) );
772    }
773
774  }
775
776  /* Importa child nodes */
777  private void import_children( DrawArea da, Node node, Xml.Node* n, string dir, HashMap<string,IdObject> id_map ) {
778
779    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
780      if( (it->type == ElementType.ELEMENT_NODE) && (it->name == "topics") ) {
781        string? t = it->get_prop( "type" );
782        var     attached = (t != null) && (t == "attached");
783        for( Xml.Node* it2=it->children; it2!=null; it2=it2->next ) {
784          if( (it2->type == ElementType.ELEMENT_NODE) && (it2->name == "topic") ) {
785            import_topic( da, node, it2, attached, dir, id_map );
786          }
787        }
788      }
789    }
790
791  }
792
793  /* Imports boundary information */
794  private void import_boundaries( DrawArea da, Node node, Xml.Node* n, HashMap<string,IdObject> id_map ) {
795
796    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
797      if( (it->type == ElementType.ELEMENT_NODE) && (it->name == "boundary") ) {
798        string? sid = it->get_prop( "style-id" );
799        string? r   = it->get_prop( "range" );
800        if( r != null ) {
801          int start   = -1;
802          int end     = -1;
803          if( r.scanf( "(%d,%d)", &start, &end ) == 2 ) {
804            var nodes = new Array<Node>();
805            for( int i=start; i<=end; i++ ) {
806              var child = node.children().index( i );
807              nodes.append_val( child );
808            }
809            var group = new NodeGroup.array( da, nodes );
810            da.groups.add_group( group );
811            if( sid != null ) {
812              id_map.set( sid, new IdObject.for_boundary( group ) );
813            }
814          }
815        }
816      }
817    }
818
819  }
820
821  /* Import connections */
822  private void import_relationships( DrawArea da, Xml.Node* n, HashMap<string,IdObject> id_map ) {
823
824    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
825      if( (it->type == ElementType.ELEMENT_NODE) && (it->name == "relationship") ) {
826
827        Node? from_node = null;
828        Node? to_node   = null;
829
830        string? sid = it->get_prop( "style-id" );
831
832        string? sp = it->get_prop( "end1" );
833        if( sp != null ) {
834          var obj = id_map.get( sp );
835          if( obj.typ == IdObjectType.NODE ) {
836            from_node = obj.node;
837          }
838        }
839
840        string? ep = it->get_prop( "end2" );
841        if( ep != null ) {
842          var obj = id_map.get( ep );
843          if( obj.typ == IdObjectType.NODE ) {
844            to_node = obj.node;
845          }
846        }
847
848        string title = "";
849        for( Xml.Node* it2=it->children; it2!=null; it2=it2->next ) {
850          if( (it2->type == ElementType.ELEMENT_NODE) && (it2->name == "title") ) {
851            title = get_title( it2 );
852          }
853        }
854
855        if( (from_node != null) && (to_node != null) ) {
856
857          var conn = new Connection( da, from_node );
858          conn.change_title( da, title );
859          conn.connect_to( to_node );
860          da.get_connections().add_connection( conn );
861
862          if( sid != null ) {
863            id_map.set( sid, new IdObject.for_connection( conn ) );
864          }
865
866        }
867
868      }
869    }
870
871  }
872
873  /* Imports and applies styling information */
874  private bool import_styles( DrawArea da, string fname, HashMap<string,IdObject> id_map ) {
875
876    /* Read in the contents of the Freemind file */
877    var doc = Xml.Parser.read_file( fname, null, Xml.ParserOption.HUGE );
878    if( doc == null ) {
879      return( false );
880    }
881
882    /* Load the contents of the file */
883    import_styles_content( da, doc->get_root_element(), id_map );
884
885    /* Update the drawing area */
886    da.queue_draw();
887
888    /* Delete the OPML document */
889    delete doc;
890
891    return( true );
892
893  }
894
895  /* Imports tha main styles XML node */
896  private void import_styles_content( DrawArea da, Xml.Node* n, HashMap<string,IdObject> id_map ) {
897
898    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
899      if( (it->type == ElementType.ELEMENT_NODE) && (it->name == "styles") ) {
900        for( Xml.Node* it2=it->children; it2!=null; it2=it2->next ) {
901          if( (it->type == ElementType.ELEMENT_NODE) && (it2->name == "style") ) {
902            import_styles_style( da, it2, id_map );
903          }
904        }
905      }
906    }
907
908  }
909
910  /* Imports the style information for one of the supported objects */
911  private void import_styles_style( DrawArea da, Xml.Node* n, HashMap<string,IdObject> id_map ) {
912
913    string? id = n->get_prop( "id" );
914    if( (id == null) || !id_map.has_key( id ) ) return;
915
916    for( Xml.Node* it=n->children; it!=null; it=it->next ) {
917      if( it->type == ElementType.ELEMENT_NODE ) {
918        switch( it->name ) {
919          case "topic-properties"        :   import_styles_topic( da, it, id_map.get( id ).node );       break;
920          case "relationship-properties" :   import_styles_connection( da, it, id_map.get( id ).conn );  break;
921          case "boundary-properties"     :   import_styles_boundary( da, it, id_map.get( id ).group );   break;
922        }
923      }
924    }
925
926  }
927
928  /* Imports the style information for a given node */
929  private void import_styles_topic( DrawArea da, Xml.Node* n, Node node ) {
930
931    string? sc = n->get_prop( "shape-class" );
932    if( sc != null ) {
933      var border = "squared";
934      switch( sc ) {
935        case "org.xmind.topicShape.roundedRect" :  border = "rounded";     break;
936        case "org.xmind.topicShape.rect"        :  border = "squared";     break;
937        case "org.xmind.topicShape.underline"   :  border = "underlined";  break;
938      }
939      node.style.node_border = StyleInspector.styles.get_node_border( border );
940    }
941
942    string? blc = n->get_prop( "border-line-color" );
943    if( blc != null ) {
944      RGBA c = {1.0, 1.0, 1.0, 1.0};
945      c.parse( blc );
946      node.link_color = c;
947    }
948
949    string? blw = n->get_prop( "border-line-width" );
950    if( blw != null ) {
951      int width = 1;
952      if( blw.scanf( "%dpt", &width ) == 1 ) {
953        node.style.node_borderwidth = width;
954      }
955    }
956
957    string? f = n->get_prop( "fill" );
958    if( f != null ) {
959      RGBA c = {1.0, 1.0, 1.0, 1.0};
960      c.parse( f );
961      node.link_color = c;
962      node.style.node_fill = true;
963    }
964
965    string? lc = n->get_prop( "line-color" );
966    if( lc != null ) {
967      RGBA c = {1.0, 1.0, 1.0, 1.0};
968      c.parse( lc );
969      node.link_color = c;
970    }
971
972    string? lw = n->get_prop( "line-width" );
973    if( lw != null ) {
974      int width = 1;
975      if( lw.scanf( "^dpt", &width ) == 1 ) {
976        node.style.link_width = width;
977      }
978    }
979
980    string? lcl = n->get_prop( "line-class" );
981    if( lcl != null ) {
982      var type = "straight";
983      switch( lcl ) {
984        case "org.xmind.branchConnection.curve"        :  type = "curved";    break;
985        case "org.xmind.branchConnection.straight"     :  type = "straight";  break;
986        case "org.xmind.branchConnection.elbow"        :
987        case "org.xmind.branchConnection.roundedElbow" :  type = "squared";   break;
988        case "org.xmind.branchConnection.arrowedCurve" :
989          type = "curved";
990          node.style.link_arrow = true;
991          break;
992      }
993      node.style.link_type = StyleInspector.styles.get_link_type( type );
994    }
995
996  }
997
998  /* Imports connection styling information */
999  private void import_styles_connection( DrawArea da, Xml.Node* n, Connection conn ) {
1000
1001    string? arrow_start = n->get_prop( "arrow-begin-class" );
1002    if( arrow_start != null ) {
1003      switch( arrow_start ) {
1004        case "org.xmind.arrowShape.triangle"  :
1005        case "org.xmind.arrowShape.spearhead" :  conn.style.connection_arrow = "tofrom";  break;
1006        default                               :  conn.style.connection_arrow = "none";    break;
1007      }
1008    }
1009
1010    string? arrow_end = n->get_prop( "arrow-end-class" );
1011    if( arrow_end != null ) {
1012      switch( arrow_end ) {
1013        case "org.xmind.arrowShape.triangle"  :
1014        case "org.xmind.arrowShape.spearhead" :
1015          conn.style.connection_arrow = (conn.style.connection_arrow == "tofrom") ? "both" : "fromto";
1016          break;
1017      }
1018    }
1019
1020    string? lc = n->get_prop( "line-color" );
1021    if( lc != null ) {
1022      RGBA c = {1.0, 1.0, 1.0, 1.0};
1023      c.parse( lc );
1024      conn.color = c;
1025    }
1026
1027    string? lp = n->get_prop( "line-pattern" );
1028    if( lp != null ) {
1029      switch( lp ) {
1030        case "solid" :  conn.style.connection_dash = StyleInspector.styles.get_link_dash( "solid" );   break;
1031        case "dot"   :  conn.style.connection_dash = StyleInspector.styles.get_link_dash( "dotted" );  break;
1032        default      :  conn.style.connection_dash = StyleInspector.styles.get_link_dash( "dash" );    break;
1033      }
1034    }
1035
1036    string? lw = n->get_prop( "line-width" );
1037    if( lw != null ) {
1038      int width = 1;
1039      if( lw.scanf( "%dpt", &width ) == 1 ) {
1040        conn.style.connection_line_width = width;
1041      }
1042    }
1043
1044  }
1045
1046  /* Imports styling information for a node group */
1047  private void import_styles_boundary( DrawArea da, Xml.Node* n, NodeGroup group ) {
1048
1049    string? f = n->get_prop( "fill" );
1050    if( f != null ) {
1051      RGBA c = {1.0, 1.0, 1.0, 1.0};
1052      c.parse( f );
1053      group.color = c;
1054    }
1055
1056  }
1057
1058  /* Unarchives all of the files within the given XMind 8 file */
1059  private void unarchive_contents( string fname, string dir ) {
1060
1061    Archive.Read archive = new Archive.Read();
1062    archive.support_filter_none();
1063    archive.support_format_zip();
1064
1065    Archive.ExtractFlags flags;
1066    flags  = Archive.ExtractFlags.TIME;
1067    flags |= Archive.ExtractFlags.PERM;
1068    flags |= Archive.ExtractFlags.ACL;
1069    flags |= Archive.ExtractFlags.FFLAGS;
1070
1071    Archive.WriteDisk extractor = new Archive.WriteDisk();
1072    extractor.set_options( flags );
1073    extractor.set_standard_lookup();
1074
1075    /* Open the file for reading */
1076    if( archive.open_filename( fname, 16384 ) != Archive.Result.OK ) {
1077      error( "Error: %s (%d)", archive.error_string(), archive.errno() );
1078    }
1079
1080    unowned Archive.Entry entry;
1081
1082    while( archive.next_header( out entry ) == Archive.Result.OK ) {
1083
1084      var file = File.new_for_path( Path.build_filename( dir, entry.pathname() ) );
1085      entry.set_pathname( file.get_path() );
1086
1087      /* Read from the archive and write the files to disk */
1088      if( extractor.write_header( entry ) != Archive.Result.OK ) {
1089        continue;
1090      }
1091
1092#if VALAC048
1093      uint8[]         buffer;
1094      Archive.int64_t offset;
1095
1096      while( archive.read_data_block( out buffer, out offset ) == Archive.Result.OK ) {
1097        if( extractor.write_data_block( buffer, offset ) != Archive.Result.OK ) {
1098          break;
1099        }
1100      }
1101#else
1102      void*       buffer = null;
1103      size_t      buffer_length;
1104      Posix.off_t offset;
1105
1106      while( archive.read_data_block( out buffer, out buffer_length, out offset ) == Archive.Result.OK ) {
1107        if( extractor.write_data_block( buffer, buffer_length, offset ) != Archive.Result.OK ) {
1108          break;
1109        }
1110      }
1111#endif
1112
1113    }
1114
1115    /* Close the archive */
1116    if( archive.close () != Archive.Result.OK) {
1117      error( "Error: %s (%d)", archive.error_string(), archive.errno() );
1118    }
1119
1120  }
1121
1122}
1123