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