TreeLine/0000755000175000017500000000000011656633566011264 5ustar dougdougTreeLine/doc/0000755000175000017500000000000011656365232012021 5ustar dougdougTreeLine/doc/sample_conditional_todo.trl0000644000175000017500000000363311651514477017444 0ustar dougdoug Conditional Task List Home Tasks Mow lawn false false Patch wall false false Vacuum false false Walk dog false true Watch TV true false Work Tasks Play solitaire true false Write documents false false Eat lunch true false Compliment boss false true TreeLine/doc/LICENSE0000644000175000017500000004310311651514477013031 0ustar dougdoug GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. TreeLine/doc/sample_other_fields.trl0000644000175000017500000000262511651514477016563 0ustar dougdoug Other Field References File Information Parent Info Child Info First child Next child Third child TreeLine/doc/sample_intern_links.trl0000644000175000017500000000273311651514477016613 0ustar dougdoug Main Separate references and links Group 1 Item 1.1 Item 2.2 Item 1.2 Item 2.1 Group 2 Item 2.1 Item 1.2 Item 2.2 Item 1.1 Combined into keyword lists Apples red large Banannas yellow large Cherries red small Pears yellow large Raspberries red small TreeLine/doc/doc_output.png0000644000175000017500000002564011651514477014725 0ustar dougdougPNG  IHDRlb#PLTE!!#!"%'&(%'+-*,))07.0..24$((+}+5A04@48919J1E3AQ/BW<@B7AL?A>57BCA3F[?DF:EP9D[>EL9FWDFCCGI9K`B)2MF'ݳ7g^ B$3fG/yE^J{r |1w6ߍom)72"HG䏷z)DZoY-ԋo2y;fÅ7vJWV .ﳉuIwNp-ND_:бqIJeF閽]9׼kGXҎ/9cGsP;:vцm8m=ziءC&>=z콫~ᣓ/<5=vI-a߹c?iu[ύy[??α59asDoz5w]Afl.n`$ Wi˱C9g6 ɱɜCE}ث^áC'׀ç'N#K ݛ5vzur,v7WocfܵVߏW7n|t"K>/7\9pwrpo8{M|g,gɻhshz WsN3['OޢxNNə|u;g/>zɣӇͺouopo'ye7^A"}K/_c}9 g^:8vp쥆39W'cNZ4z4mywrg'nyg򽜱{8xa,u}c Lc'sU:Ckr8Qx{a ߈x<ήFl۸J'PVecnk+\_zK_|=v;ds7bZy<{e=la w+kri|}J"XM16jD~饃4XLy9:hr\W򈰏mqcmK;6:Vu+1n.?='9Y__זȬLuon&7b;]ίo$e (HjD` KXB,!`B 6,w~N|Ͱ o肛Y.Zށ_OQ &Ff䗟>*બԊt՘O˯<:ϴ~lZ~Ho5ʏTϪ"bT;QF+ل#:}ȠN`ALAYF:@Z-V8*/b^d9]MP=MȲO צ%jG[GH : " @?y^@kR^n1[8!FOǍ\ 7)f <wmMBB]DCǧAtǥb[0pT l\n2XWZ+nIs6:Ӛh|Ιܞs@J|~>{^4e|[of#W{iOۗWPyCfnwx?)iK<plL;Ss_f}s>_` Dס>'|e2o;*´aƒ%MSpywři4ȏ8OQr*f21V9hmXZOН_oF9Kg'y6>R*b~snpe333ss Z;\-oAмֹ>?Fc[?x[͠3f疟=V0K$.Vcv$V~`|4bmf!lCeiNLmv18]:XvɔKhI&=k-I K%,!` sc׹I ]Yv)O9xb8uwF(|C71Y,)_>(a,Xpm鸣U q[zܹc-/ty!\♲9ݶtѝ?Xr%y|]G,C`L,й,}`")n` druR+;rc,׳?dZ<`hKKKKKKKKKKK,w#38XƱ twr<#c,7x't.`±4-< AnjR6jNHб4Fe|KhZ[̐׏;Ge\pφˈ|zOwDoý0XƳd< g/!5s5$XƷ$NYḰ&\: A,[gbԃ<`2e`';rCk91.eKow'gjeI<ijL%X%X%X%X%X%X%X%X%X>+5Ԓ0p umku<K~x[A`TzJ9-A@.@Qm;m9gD[:*I]iL-&= 4{y'jyx3_Ӥz#V:U[92w{=f204{l![wױO)}AQ"E︥$ܖk5JXT&|˺GygZSE죺[Sr>觃ہ -[JZںw!?k50cϠ$gD}lekBbMa)@}h: G:dK@@V,{m;Β9g[U- 3Yjm 8|GK=N[nG IactaOd"Ikzۡ1ܒ47]6cO7$i˹ / TVtx&ƙgt8vnK*8k؜e҅ {0DDY\6oY+$%ImpYLvI]eo3C"2y-炞` /`_$J,9LbOςe[ X&pT`I%wG,=Kbؐ"$f4^fz<^{bҠ"&dy۳nsxz^g}+"v;AORuX7,Էu}'p(˘EZ0+Obi/F}AnTS[qmRn0rՅ2vڑk)pמ+nEc啲@g̒ro4/涄3G.Ff뭲uKqa7\5_0\RҒ Jm}!h_YY6+ΐOx'Y]6K72pC2ȺKIRJMmޫKN#HAO php !B`% |C '%Fϋ=˖mwY>[COϓh8:՟">Js6Qko9mɞG,0xp:},mo|neQ+-<-v/ۢo?x~'.cwImg_җhW7O2fFCJo_}?x:)'KKL˞XƵvkטoڒэ1~=썹ՏeH4>]oI1uCԴ?5iUFpBؖ3b/Ƭ>̢QY1eVej5Ӫ&{,tUFmQi4,jjs.wZd-]Jʼ_/8kguQieR~4L /fRIJY$Seӗ**n,*%OH/db2} [ B)d,v3nMɼKUfynOzϨnME2sc tv ]xK7[Z0{.akZ|,='G=>6d5 8Um z)nR:SU7u;ukoHΠ;wzXwP\T|TT1dg5Q+Ly,^SY'e0l<_(㨏=OUOMgULbyW(;s~Nc}nhnkB-dτLkq s-*,h;H]IYd:$Q$ 2䫖ZaizɎ9'-1K27E%-gKK|6,]r|J3S:kY 핀ӱnJ>FTe+y؄T1kG\R#lVZ.T6cXJQϒQeUYޟޯ/nW4.+XVSORuê.hrE&oK]|H"{BiWI@VX>1QIMMeV6EtBܧJ3%*uD&35K z;vTninbҧRvkUV̥RhҺ'jhntS%-SkR7*ߥ `72 -Z[1f4Иt9L!WXSҪl(Pi?f"%ڧM{vPۿW)L>vMRR!)k$mf$P1[[Q+%w3kJY5ƞ3!KU]w[GKvT^}&\Z]kYvvZ;E-5%nQh.*-iʰZq-*=ju=}KǽQ͘Z$g'jJT*mF-HW1}l:?KJyZY$^ֻ4tXvnO`%X%X%:JQy߹ܲ=ϥM3Yy&/Z{PJa_>¡-ur"2<)ψԕOR/-4[;Ve7Ko鞇,$`un.>~4K ’R,D2#Vv$rFۯ2EgrmZٹz͕[Q]7?eKE]*Mh*-VP%ݼliYcYڏQ5b,'Z"Y6KWhKo2Baj/4UbR =%Xjj0]~&T GL^ؾ#,e2dP)X}ayV"ϚM kr1y]T]%g$}OR.VL.P^ѭEӖr},YzEߑlɑ &a-Yvcs5 OroҳY.#^-X̹[nN|:j&+WcG-"* %%U+zTI~z=ҝD&N7ZK=z5d掦\ڒ]<>$qew|7ޒh Y*>iU2ۊ3Lӱav1M2J}C+?PL%.ZuK]\JOṶu5&Lb-%Kv$DחhըW鳫dXUF~aD omvą}ڳV$/UUQg aFqvTs?w]=xhIܼGu XX%Xe7aR\:_l**[/j\1oi}͉Cz;Q&Lƭ͖f)K%ˢȭ"YyQLKart4ei+rڥr_.]kɯ7i/s죪呖*Z'y"ռ={%Ƿ/b)ɨ3DnԊLYX3?[)x"{?_f-J~y+ߊfxr.ۥX9#AS3!c |OUӞKv..%kGe ]BIz=VːwV{xI\[[h.X/Xܪ]`9Z{HmeeOݪ6-1>$wj|ם?:*IwiQ9vWZᕸ5R^-? 3t;G%#Yh\@0,s*yؑn]2bJq,K둇I/ӝkQ6٭}"K֊%ff/SYl[8kb{a5c#.isB7. oJ3s%}grQsJk29k颏!Kn.%h=L}ގ%Oce'q-x'ۙ'L"״.7%hVca=|a~\_ȝ%c-45RQOB-WإGͽ.ԋ3r=e(؏eYQR+l8PjK,Ym}':gP_\;xiN|Ӫ3'Zk lYRݘykd\jՄikTr'fI$bతjy'YRz$I}XArYntwL6K|>G+Y\CT{?X@_->? M[%w$;KI+xX>}K$}۸|eYm.MvL.R`7wE/QifOӒߞgq-ե+{w?giUL<6MJ]vDO;#J(-% X>5˹n&Di.*Ke:P_.VxqHk6gHt9|Z}ܗ3]Gm^4gR٭-.ϖ#&s]IV V'IDAT|jǝY_rpa,J*֟qa֗vS3+옵jtgrӖ>X>p ,7;X&qˤyws*,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,` õE/=-E$|d˧n {N 7$2MxFF}F\}@e2;3/>h`&( qL{8NPħ!z sX&e2 Id9LZKLVˠA/X9"` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` 2H\7t)@BTNrdA|ːNܴe%e,,,,ois$Zoލ&ǒ2߳hHp$ X%,!` ٚILutvv@15dX%,!OiNɱOM}9¨'c`>I3ZQ2M K#Z0ПE1gB ѵ‰ 4~|$ZF2e` R+ⶒ, 2Zeb(2Z1, /e[X/,-$ܰm-2b-7Q덨˒= ۙJy.Vpd1q,х.7X.FO$׮bN`-j_wou{clP!ޔݮԽ;3wgP »dݙNsp8O=)]e7N۶Rlh[{6I\7:? zgaw $u~8k]Oq fg CXZ]ճ7ƩˇX{]f"22DwBFxKOqM|l5Ij ok JxݡPX.QMl{k瑬%=uck:BS3xhj~1k]q3~gwZed-yuuա:cl-V,K 7 IUS'֙I򢸑|y9i֋V) ܟq./x$չQ$ Cڙe4b;?CgF~a,YeXf{;<f$FZB$)0vEԊa&8OplFzKo :ņ+yx+-FTA^ :t4Ěxf<\9 vi /1?xh~8n<$#Kѵ> >>͟85 D/A,72y̅N?tYk%kx-> nU&_pœ_xSn`* ފqz(Lppmޠ#8R_= g-CQia#~+<1ټ|y>v,Fb-|n@c-p~]!Vw K|[%e(ZZ8T''?'o j-ħ_ Features Store information Stores almost any type of information, including plain text, HTML, numbers, dates, times, booleans, URLs, etc. Tree structure The tree structure helps keep things organized. Fields Each node can have several fields that form a mini-database. Node types Several node types, with different sets of fields, can be included in one file. Output format The output format can be defined for each node type. Right-hand view The right-hand view can show one of three views - for showing output, editing node data and editing node titles. URL links Clicking on URLs in the output view opens the link in an external web browser. Search commands There are two available search commands. Undo/redo Undo and redo commands are available for all modifying operations. Printing The formatted output can be printed with parent/child lines and headers and footers. HTML export The data can be exported to HTML. XSLT export An XSLT file can be exported to work with the XML TreeLine files. Text import and export Tab-delimited tables and tab-indented text files can be imported and exported. Plain text files and Treepad files can be imported. Bookmark import/export Mozilla and XBEL format bookmark files can be imported and exported. Sorting There are several sorting options. Filtering The nodes can be filtered. Spell check Text data can be spell checked (requires an external program - see the System Requirements section). Arranging data Data can be automatically arranged using either parent references or categories from data fields. Numbering There is an outline numbering feature. Customization There are many other options for customization, including user-defined keyboard shortcuts. TreeLine/doc/readme.trl0000644000175000017500000065725311656365022014020 0ustar dougdoug TreeLine ReadMe ReadMe File for TreeLine an information storage program by Doug Bell Version 1.4.1 (stable release) November 11, 2011 Contents Background background Features features Legal Issues legal System Requirements sysreq Linux linux-sys Windows win-sys Installation install Linux linux-inst Windows win-inst Using TreeLine using Revision History revs Questions, Comments, Criticisms? contact Background background Introduction Do you have lots of sticky notes lying around with various useful information jotted down? Or many lists of books, movies, links, website logins, personal contacts, or things to do? Can you find them when you need them? Well, I often couldn't. So here's my answer. Overview Some would call TreeLine an Outliner, others would call it a PIM. Basically, it just stores almost any kind of information. A tree structure makes it easy to keep things organized. And each node in the tree can contain several fields, forming a mini-database. The output format for each node can be defined, and the output can be shown on the screen, printed, or exported to HTML. It's free Since I'm not in the software business, I'm making this program free for anyone to use, distribute and modify, as long as it is not incorporated into any proprietary programs. If you like the software, feel free to let others know about it. And let me know what you think - see the <a href="#contact">Questions, Comments, Criticisms?</a> section for contact information. Features features General Store information Stores almost any type of information, including plain text, HTML, numbers, dates, times, booleans, URLs, etc. Tree structure The tree structure helps keep things organized. Fields Each node can have several fields that form a mini-database. Node types Several node types, with different sets of fields, can be included in one file. Output format The node format, including fields, output lines and tree-view icon, can be defined for each node type. Views Left-hand view The left-hand view defaults to a tree view but can show a flat list of descendants of the current node. Filtering The left flat view also shows the results of filtering operations. Right-hand view The right-hand view can show one of three views - for showing output, editing node data and editing node titles. Show parent and children The right-hand view is normally split to show data from the parent node and its children. Show multiple selection If multiple nodes are selected, the right-hand view shows all of their data. Show descendant output The output view can be set to show indented output from all descendant nodes. Navigation Incremental search There is a quick incremental search command to find a matching node title. Find command There is a find command that searches through all node data. Toggle selections Previous and next selection commands toggle selections to quickly move between parts of the tree. Formatting Format dialog The dialog for data type configuration has several tabs to easily set all type, field and output parameters. Format copies Formatting information can be copied from another TreeLine file. File Handling Undo/redo Undo and redo commands are available for all modifying operations. File formats TreeLine files are XML by default, but there are options for automatically compressing or encrypting the files. Document templates Document templates for new files are preformatted to cover basic needs. Printing The formatted output can be printed with parent/child lines and headers and footers. File Import and Export HTML export The data can be exported to HTML. XSLT export An XSLT file can be exported to work with the XML TreeLine files. Text import/export Tab-delimited tables and tab-indented text files can be imported and exported. Plain text files and Treepad files can be imported. Bookmark import/export Mozilla and XBEL format bookmark files can be imported and exported. Generic XML import/export Generic XML files can be imported and exported, allowing TreeLine to function as a crude XML editor. ODF import/export ODF text documents can be imported and exported as outlines. Batch conversion Batch file conversions can be done from a command line interface. Linking Objects URL links Clicking on URL fields in the output view opens the link in an external web browser. Internal links There are internal link fields that select another node based on a reference or a keyword. Executable links An executable link can run an external program or open a file when clicked. Data Manipulation Sorting There are several sorting options. Filtering The nodes can be filtered. Conditional types A node's icon and output format can be changed conditionally based on its data. Spell check Text data can be spell checked (requires an external program - see the <a href="#sysreq">System Requirements</a> section). Arranging data Data can be automatically arranged using either parent references or categories from data fields. Numbering There is an outline numbering feature. Customization Customization There are many options for customizing both general and file-based attributes. Shortcuts and toolbars There are editors for keyboard shortcuts and toolbar commands. Internationalization The user interface and documentation are available in English, French and German. Legal Issues legal Free software TreeLine is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either Version 2 of the License, or (at your option) any later version. No warranty This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the LICENSE file provided with this program for more information. System Requirements sysreq Linux linux-sys Libraries TreeLine requires the following libraries and programs: Qt Qt (Version 4.4 or higher) Python Python (Version 2.4, 2.5 or 2.6) PyQt PyQt (Version 4.4.0 or higher - see <a href="http://www.riverbankcomputing.co.uk"> www.riverbankcomputing.co.uk</a> for more information) XML parser An XML parser is required, such as the expat library or the PyXML package Spell check If spell checking capability is desired, either the aspell (preferred) or ispell programs are required (these are packaged with most distributions) Windows win-sys Binary Using the files provided in the installer, TreeLine should run on any computer running Windows XP, Vista or 7. Spell check If spell checking capability is desired, an external program is required. Either aspell (preferred) or ispell must be installed. See <a href="http://aspell.net/win32/"> aspell.net/win32/</a> or <a href="http://www.luziusschneider.com/Speller/English/"> www.luziusschneider.com/Speller/English/</a> to download one of the programs and its dictionaries for any desired languages. Installation install Linux linux-inst Basic install Extract the source files from the treeline tar file, then change to the TreeLine directory in a terminal. For a basic installation, simply execute the following command as root: "python install.py" Install options To see all install options, use: "python install.py -h"<br /><br /> To install TreeLine with a different prefix (the default is /usr/local), use: "python install.py -p /prefix/path" Internationalization To install language translations for TreeLine, download the "treeline-i18n-..." tar file that matches the version number of the main TreeLine file (ignore the letter suffix on the translation version number). To install the program and translation files simultaneously, extract the translation file from the same directory where the main treeline tar file was extracted and run the install command as described above. If TreeLine is already installed, extract the translation file to any directory and run the translation's install command as described above. Windows win-inst Basic install Simply execute the downloaded installation file (treeline-x.x.x-install.exe). It will install the program with its libraries and optionally create file associations and shortcuts. Spell checker To use TreeLine's spell checker, an external program (aspell or ispell) must be installed (see the <a href="#sysreq">System Requirements</a> section). Internationalization To install language translations for TreeLine, download the "treeline-i18n-..." exe file that matches the version number of the main TreeLine file (ignore the letter suffix on the translation version number). To install the translation files, simply execute the translation file. Modify source code If you wish to modify the source code or write your own PyQt programs for Windows, do not use the above procedure. Instead, you need to install Python (see <a href="http://www.python.org"> www.python.org</a>), Qt (see <a href="http://www.trolltech.com"> www.trolltech.com</a>), and PyQt (see <a href="http://www.riverbankcomputing.co.uk"> www.riverbankcomputing.co.uk</a>). Then extract the source code files from the Linux version (treeline tar file) to a directory of your choice and execute the treeline.py file. Using TreeLine using Contents Getting Started start Introduction start-intro Templates start-templates Example Files start-example Basics basics Left Pane Views basics-left Right Pane Views basics-right Commands basics-cmds Multiple Windows basics-wins Tree Navigation nav Keyboard Shortcuts nav-keys Selection nav-select Searching nav-search Tree Editing edit Edit Menu edit-menu Shortcuts edit-shortcuts Right Pane Views edit-view Spell Check edit-spell Formatting and Node Types format Setting Nodes to a Type format-set Type List format-type Type Config format-config Field List format-fields Field Config format-fieldconf Output format-output Title Formatting format-title Skipped Lines format-skip HTML Tags format-html Formatting Examples format-example Other Field References format-refs Sibling Prefix and Suffix format-sibling-text Link Reference Field format-link-ref Generic and Derived Types format-derived Conditional Types format-condition Copying Formats format-copy Field Types fields Field Options field-options Text Type field-text Choice, Combination and Boolean Types field-choice Number Type field-number Date and Time Types field-date Linking Types field-link Unique ID Type field-unique Advanced Options field-advanced Tree Data Manipulation data Category-based Commands data-category Reference-based Commands data-ref Sorting data-sort Filtering data-filter Numbering data-number Change Selections data-change Printing print Print Options print-options Page Setup print-setup Printer Font print-font Header and Footer print-header Print Preview print-preview Printing Problems print-problems File Handling file File Compression file-comp File Encryption file-enc Auto Save file-auto-save Saved Tree States file-states Command Line file-cmdline File Import import General Information import-general Delimited Text import-delim Plain Text import-plain Treepad Files import-treepad Bookmark Files import-bookmarks Generic XML import-xml ODF Text Document import-odf File Export export General Information export-general HTML export-html Delimited Text export-delim Bookmark Files export-bookmarks Generic XML export-xml XSLT export-xslt ODF Text Document export-odf Customizations custom Options custom-options Fonts custom-fonts Keyboard Shortcuts custom-keys Toolbars custom-tools Colors custom-color Tree Icons custom-icons Plugins plugins Description plugins-intro Installation plugins-install Interface plugins-intrf Common Problems errors Closed Panes errors-panes XML Error errors-xml Printing errors-print Getting Started start Introduction TreeLine is a more complex program than it appears at first glance. Reading much of this documentation is highly recommended in order to learn to use it effectively.<br /><br /> The <a href="#basics">Basics</a> section describes views and selections. To learn how to move around and change the tree, refer to the <a href="#nav">Tree Navigation</a> and <a href="#edit">Tree Editing</a> sections. Next, its time to go beyond using TreeLine with only simple text for each node. To learn about setting up custom data for each node, refer to the <a href="#format">Formatting and Node Types</a> and the <a href="#fields">Field Types</a> sections. start-intro Templates When starting a new file, the templates dialog gives a choice of nodes with a single line of text or with long text. The single line template only has a node title, while the long text options have a title and a multiple line text field in each node. For long text, you can also choose between plain text, which preserves line breaks, and HTML text, which allows more complex formatting.<br /><br /> There are a few more specific options, including canned setups for personal contact information, a book list and a to-do list. start-templates Example Files Various TreeLine sample files can be opened by using the "File-&gt;Open Sample" command. They include (roughly in order of increasing complexity): start-example sample_basic_contacts The "sample_basic_contacts" file is a format for storing contact information. Several fields are used for various pieces of information. sample_basic_booklist The "sample_basic_booklist" file is a list of books, providing an example of configuring nodes to contain data. Several different field types are used to create author and book data types. sample_basic_longtext The "sample_basic_longtext" file contains a long text field under each node, similar to the Treepad program. It also shows the difference between plain text and HTML text fields. sample_color_items The "sample_color_items" file uses HTML tags in data type formats to provide color output. See also the <a href="#format-html">HTML Tags</a> section. sample_char_format The "sample_char_format" file is an example of using HTML tags in the data to control font formatting. See also the <a href="#format-html">HTML Tags</a> and the <a href="#field-options">Field Options</a> sections. sample_bookmarks The "sample_bookmarks" file is an example of using external HTML links. This is the file setup that is used when importing or exporting bookmark files. sample_intern_links The "sample_intern_links" file shows how to do links that lead to elsewhere in the TreeLine file. See also the <a href="#field-link">Linking Types</a> section. sample_table_booklist The "sample_table_booklist" file shows how to use HTML tags and sibling prefixes and suffixes to create tables in the output. See also the <a href="#format-advanced">Advanced Settings</a> section. sample_other_fields The "sample_other_fields" file is an example of alternate field references, including references to ancestor nodes, child nodes and file information. See also the <a href="#format-refs">Other Field References</a> section. sample_conditional_todo The "sample_conditional_todo" shows how to use conditional types to change node output based on node content. See also the <a href="#format-derived">Derived</a> and <a href="#format-condition">Conditional Types</a> sections. readme The "readme" file itself shows many types of formatting, including headings, bullets, internal links and pictures. Basics basics Left Pane Views The default view in the left pane is the "Tree View". Parent nodes can be opened and closed to display or hide their indented descendant nodes.<br /><br /> The other tab in the left pane is the "Flat View". It displays all of the descendants of the nodes that are selected in the "Tree View" in a flat list. It also shows the results of filtering operations (see the <a href="#data-filter">Filtering</a> section). basics-left Right Pane Views The right pane is tabbed to show one of three different views of the data. The "Data Output" view shows the formatted text for each node and is read-only (see <a href="#fig-1">Figure 1</a>). The "Data Editor" view shows a text edit box for each data field within a node (see <a href="#fig-2">Figure 2</a>). The "Title List" view shows a list of node titles that can be modified using typical text editor methods.<br /><br /> When a parent node is selected in the tree, the right view will default to showing information about the selected node in an upper pane and information about the selected node's children in the lower pane. The "View-&gt;Show Child Pane" command will toggle the display of the child nodes. If the selected node has no children, the view will show a single pane with information about the selected node only.<br /><br /> When multiple nodes are selected in the tree, the right view will not display any child node information. It will instead show information about every selected node.<br /><br /> When the "View-&gt;Show Output Descendants" command is toggled, the "Data Output" tab will show an indented list with information about every descendant of a single selected node.<br /><br /> The initial state of the views can be controlled by changing the startup conditions in "Tools-&gt;General Options".<br /><br /> By default, the number of nodes displayed simultaneously in the "Data Editor" view is limited. Buttons in a heading allow the next or previous set of nodes to be displayed. This provides a convenient way to page through the node information without slowing the program by trying to display too many fields. The number of pages to display can be set in "Tools-&gt;General Options". basics-right
Output pane picture Figure 1: Output Pane doc_main.png fig-1
Edit pane picture Figure 2: Edit Pane doc_edit.png fig-2
Commands Most of the menu and tool-bar commands apply to the items selected in the left view. In general, they perform an operation on the nodes themselves or on the descendants of the nodes. Multiple nodes can be selected by holding down the "Ctrl" and "Shift" buttons when clicking with the mouse.<br /><br /> To add information to a new TreeLine document, use the "Edit-&gt;Add Child" command to create a new node. Then combinations of the add and insert commands may be used for additional nodes. Alternatively, new node titles may be typed into the "Title List" view in the right pane. basics-cmds Multiple Windows Multiple TreeLine windows with different files can be opened within the same TreeLine session. The "Open files in new windows" option in "Tools->General Options" controls whether new windows are used when opening or creating files.<br /><br /> The "Window->New Window" command will create a new window with views of the same TreeLine file. Changes in either window are saved in the same document. This can be used to work in different sections of the file simultaneously. For performance reasons, the non-active window does not show the changes made in the active window until either the non-active window is focused or the "Window->Update Other Window" command is issued.<br /><br /> Starting TreeLine (from a shortcut, file association or command line) will activate an existing TreeLine session if it is already running. A new window will be opened if an unopened file was specified by file association or in the command line. basics-wins
Tree Navigation nav Keyboard Shortcuts There are several keyboard commands that can be used for tree navigation. The up and down arrow keys move the selection. The left and right arrows open and close the current node. Holding the CTRL and SHIFT keys with "J" or "K" moves between siblings, skipping children. The CTRL key and "U" moves to an item's parent. The "Home", "End", "Page Up" and "Page Down" keys can be used to move quickly through the tree. The CTRL and SHIFT keys can be held with the "Page Up" and "Page Down" keys to move the tree view even when a different pane is focused.<br /><br /> All of these keys and the keyboard shortcuts for pull-down menu commands can be customized by using the "Tools-&gt;Set Keyboard Shortcuts" command. The shortcut editor has tabs for menu and non-menu based commands. Simply type the new key sequence with the appropriate field selected.<br /><br /> Another way to move through the tree is to type the first letter of a visible node title. Hitting the letter again moves to the next possibility. nav-keys Selection Multiple nodes can be selected by holding down the CTRL or the SHIFT keys when changing the active node. Individual nodes are added or removed from the selection when the CTRL key is held. The selection of all nodes between the old and new active nodes are toggled when SHIFT is held. The active node can be changed by using the mouse or by using any of the keyboard navigation methods.<br /><br /> The "View-&gt;Previous Selection" and "View-&gt;Next Selection" commands can be used to toggle through a history of selections, allowing faster navigation through the tree.<br /><br /> By default, the selection sequence doesn't matter. However, if the general option for multiple selection is changed to "Selection Order", nodes selected with CTRL key held are output in the order selected. nav-select Searching There are two ways to search for nodes. These methods can find nodes that are buried in the tree structure. The first is the "Tools-&gt;Find" command. Keywords can be entered in a modeless dialog box. A node is found if the keywords are matched in any of the node's fields. The matching text will highlight in the "Data Output" pane.<br /><br /> The next method is an incremental search, started by typing CTRL + "/". Then type the search string, which shows up in the lower status bar area. The search will progress as the string is being typed. The incremental search only finds text in the node's titles. The previous search can be repeated with the "F3" key and backward with "Shift-F3".<br /><br /> By default, parent nodes will automatically open and close when found with the search methods, by typing the first letter, and with the "next sibling" keyboard command. This behavior can be disabled in "Tools-&gt;General Options". nav-search Tree Editing edit Edit Menu The commands in the "Edit" menu (except for undo and redo) operate on the selected nodes in the left tree view. The cut, copy and paste commands can be an exception to this, since they operate on the right view when something is selected there. And keep in mind that, in general, the descendants of the selected nodes are also affected.<br /><br /> Paste will add a copied nodes as the last child of the selected node. If more than one node is selected, additional copies are added under each selected node.<br /><br /> The "Edit-&gt;Paste Node Text" command renames the selection based on either the text in the clipboard or the title of the top node in the clipboard. edit-menu Shortcuts There are several shortcuts for use in tree editing. Drag and drop will move or copy nodes to become a children of the destination node. Clicking on a selected node will rename it. Pressing the enter key will insert a new node, and pressing the delete key will remove the selected nodes. If desired, these shortcuts can be disabled in "Tools-&gt;General Options". edit-shortcuts Right Pane Views In the right pane, the "Data Editor" view provides the most direct way to edit the data within a node. If the edited field is used in the title formatting, the node title in the tree will show the changes. The field editor will scroll, allowing multiple lines of text to be entered.<br /><br /> There are items in the "Data Editor" box context menus to add HTML font tags around selected text. These tags include bold, italics, underline, size and color. Note that the fields must be set to display HTML content (see the <a href="#fields">Field Types</a> section) for this to be effective.<br /><br /> Another edit box context menu item will add an internal link in the text. The status bar will then prompt the user to click on the link destination in the tree view. This will add an internal link to that destination to the text in the edit box. Of course, the field must be set to display HTML content for this to be effective.<br /><br /> An external editor can also be invoked from a "Data Editor" box context menu. After the text editor saves changes and is closed, the changed text will be in the text box. The EDITOR environment variable can be used to specify the editor to start, or, if the variable doesn't exist, TreeLine will prompt for an executable to set as the default.<br /><br /> Also in the right pane, the "Title List" view is useful to quickly rename child titles or to add new child nodes. A text list of new nodes can even be pasted directly into this view. edit-view Spell Check There is a spell check command in the "Tools" menu. Use of this command requires an external program to be installed (either aspell or ispell - see the <a href="#sysreq">System Requirements</a> section). If there are any misspelled words in the selected branch, a dialog will allow the word to be ignored, added to the dictionary, replaced with a suggestion or edited. This will spell check the text in all data fields of each node.<br /><br /> By default, the spell check will use dictionaries for the current operating system language. If using aspell, there is an option under "Tools-&gt;File Options" to specify an alternate two-letter language code for the current TreeLine file. Note that the appropriate aspell dictionary files must be installed and that this option does not work with ispell. edit-spell Formatting and Node Types format Setting Nodes to a Type By default, a new TreeLine document contains two node types: "ROOT" and "DEFAULT". The type is shown at the top of each node box in the "Data Editor" right-hand view. The creation of new types and the customization of types is described below. To set the selected nodes to a specific type, use the "Data-&gt;Set Item Type" menu. Alternately, to set a series of child and descendant nodes to a specific type, use the "Data-&gt;Set Descendant Types" command. The resulting dialog box allows the selected nodes, their children, all descendants, or descendants matching logical conditions to be set to the highlighted type. The dialog box can be left open while the tree selection is changed to set more nodes. format-set Type List The "Type List" is the first tab of the "Data-&gt;Configure Types Dialog". The list of data types can be modified by the buttons on the right. New types can be added, and existing types can be copied, renamed or deleted. format-type Type Config "Type Config" is the second tab of the "Data-&gt;Configure Types Dialog". It contains a selection for the default child type. If set, this will be the initial type used for new children with this type of parent. If set to "[None]", children will default to either the type of their siblings or their parent.<br /><br /> The "Change Icon" button allows the selection of a custom tree icon for this data type. The "Clear Select" button on the icon dialog can be used to set the icon to "None", so that no icon will be displayed for this type. To avoid showing any tree icons, the "Show icons in the tree view" general option can be unset. format-config Field List The "Field List" is the third tab of the "Data-&gt;Configure Types Dialog". The list of fields within a data type can be modified by using the buttons on the right. New fields can be added, and existing fields can be moved up and down in the list, renamed or deleted. format-fields Field Config "Field Config" is the fourth tab of the "Data-&gt;Configure Types Dialog" (see <a href="#fig-3">Figure 3</a>). The field type and its output format string can be set (see the <a href="#fields">Field Types</a> section for details). Extra prefix and suffix text to be output with the field can also be set, and a default field value for new nodes can be entered. There is a choice between plain text, which preserves line breaks, and HTML text, which allows various formatting options. Finally, the number of lines displayed in the editor for the field can be specified. format-fieldconf
Field config picture Figure 3: Field Config Tab doc_fieldconf.png fig-3
Output "Output" is the last tab of the "Data-&gt;Configure Types Dialog" (see <a href="#fig-4">Figure 4</a>). The left half of the dialog shows the fields. The right half shows the formatting for the title (used for the node text in the tree view) and the node output. The formatting consists of text lines with embedded fields. The fields are shown as "{*field_name*}". The field that is selected in the list can be added to a format at the cursor position with the "&gt;&gt;" keys. The format field reference at the cursor can be removed with the "&lt;&lt;" keys. format-output
Output tab picture Figure 4: Output Tab doc_output.png fig-4
Title Formatting When a node in the tree is renamed, the program attempts to match the title formatting pattern to set the appropriate fields. If the title formatting is too complex, it may not correctly guess the intent. Things like adjacent fields with no characters separating them should be avoided unless you do not wish to rename nodes from the tree.<br /><br /> If the text data used for a tree view title has multiple lines, only the first line will be used as the title. format-title Skipped Output Lines If a line in the output formatting contains one or more fields and all of those fields for a given node are empty, the line is skipped. No blank line or embedded text will be output for that line. Note that this does not apply to a line without any fields (only embedded text). Also, when a line ending with a &lt;br/&gt; or an &lt;hr/&gt; tag is skipped, the ending tag is retained. format-skip HTML Tags Simple HTML formatting tags can be used in node output formats. Commonly used tags include "&lt;b&gt;bold&lt;/b&gt;", "&lt;u&gt;underline&lt;/u&gt;", "line break&lt;br/&gt;", "&lt;hr/&gt;horizontal line", and various font tags. Complex block tags should generally be avoided. When the "Allow HTML rich text in formats" file option is disabled, formatting tags are treated as plain text. For an example of tag use, see the "sample_color_items" file (by using the "File-&gt;Open Sample" command).<br /><br /> A line break is automatically output after each formatting line, unless the "Add line breaks after each line" file option is unset. In addition, the "Add blank lines between nodes" file option determines whether there is also an automatic blank line between node outputs. A line break tag ("&lt;br&gt;") can be used at the end of the formatting to get the same effect, or a horizontal line tag ("&lt;hr&gt;") may be used instead to separate the nodes. format-html Formatting Examples Here is an example of output formatting for a book list:<br /><br /> &lt;hr/&gt;"{*Title*}"<br /> (c) {*Copyright*}, Rating: {*Rating*}<br /> {*PlotDescription*}<br /><br /> Sample files with various kinds of formatting are included in the program distribution. They can be opened by using the "File-&gt;Open Sample" command. They are also listed in the <a href="#start-example">Example Files</a> section. format-example Other Field References References to fields that are not contained within the node can be added to the output. Pushing the "Show Advanced" button on the "Output" tab of the configure dialog makes a reference level selection become visible.<br /><br /> If the reference level is changed to "File Info Reference", fields containing file meta-data can be added to the output. These include the file name, path, size, and modified time. These are shown as "{*!field_name*}" in the title and output format editors.<br /><br /> There are field references to various ancestor nodes (parents, grandparents, etc.). These require the data type of the reference to be specified. This selection determines the field names that are available, but the data from any type with a matching field name will be shown in the output. References to fields from parent and grandparent nodes are shown as "{**field_name*}" and "{***field_name*}", respectively. There are also general ancestor references, shown as "{*?field_name*}", that take data from the closest ancestor with a matching field.<br /><br /> References to child nodes can also be added. These also require that the child data type be specified. The child data becomes embedded in the parent output. The child data is delimited with a separator string defined as a file option. The separator defaults to a comma and a space, but can be set to &lt;br&gt; or anything else.<br /><br /> Finally, a "Child Count" reference can be added. This field will show the number of children ("Level1" field) or grandchildren ("Level2" field) of a node. These are shown as {*#Level1*} in the format editors.<br /><br /> For examples of these fields, see the "sample_other_fields" file (by using the "File-&gt;Open Sample" command). format-refs Sibling Prefix and Suffix Pushing the "Show Advanced" button on the "Type Config" tab of the configure dialog makes a sibling text section become visible, with settings for sibling prefix and suffix tags. These tags can often be left blank, but are useful for creating tables or bulleted lists. These tags will be placed before and after sibling groups of the proper type. For example, to create an output table, the sibling prefix tag could be set to "&lt;table border="1"&gt;" and the suffix tag could be set to "&lt;/table&gt;". Then, the output format could be set to:<br /><br /> &lt;tr&gt;&lt;td&gt;"{*Title*}"&lt;/td&gt;&lt;td&gt;(c) {*Copyright*}&lt;/td&gt;&lt;/tr&gt;<br /><br /> Also see the "sample_table_booklist" example file (by using the "File-&gt;Open Sample" command).<br /><br /> Siblings should generally be of the same type, or at least have the same prefix and suffix tags. format-sibling-text Link Reference Field One of the fields for each type is designated as the link reference field. By default, it is the first field in the list. Pushing the "Show Advanced" button on the "Type Config" tab of the configure dialog shows the selector for this field.<br /><br /> The link reference field is used for internal link references and for the arrange and flatten by reference commands. The contents of this field should generally be unique if usage of these functions is planned. format-link-ref Generic and Derived Types Data types can be set to derive their field settings from a generic type. This allows types with different output formatting to always use the same set of fields. Any changes to the generic's list of fields and field types are automatically reflected in the fields of all derived types. This does not apply to a field's output formatting, which can still be set independently.<br /><br /> There are two methods for creating derived types. First, a derived option can be selected when copying a type on the "Type List" tab of the "Data-&gt;Configure Types Dialog". Alternately, a generic type can be specified from the derived type's "Type Config" tab of the dialog if the advanced functions are shown. format-derived Conditional Types Conditional expressions can be used to automatically assign a data type based on each node's content. Conditions can be assigned only to a generic type and its associated derived types. This allows the automatic assignment of different output formatting or different icons depending on each node's field data.<br /><br /> The conditional dialog box is accessed from a button on the "Type Config" tab of the "Data-&gt;Configure Types Dialog" if the advanced functions are shown. Each line of the condition includes a field, an operator and a comparison value (see <a href="#fig-5">Figure 5</a>). The operators include equality, greater than, less than, starts with, ends with, and contains. There are also True and False operators that will toggle the type of all nodes simultaneously.<br /><br /> For special field types such as dates, times, and booleans, the comparison value should be entered in the same format that is used in the Data Editor window. In general, the starts with, ends with, and contains operators should not be used for these special fields, since the comparison is done using an internal data representation. Dates and times also support a special comparison value of "now", which is always interpreted as the current date and time.<br /><br /> The "Add New Rule" button is used to add additional condition lines. The lines can be joined with "and" or "or" operators. The "Remove Rule" button deletes the last condition line. If only a single line is present, the "Remove Rule" button completely removes the condition.<br /><br /> Conditions do not have to be set for all types in a family. If no conditions are true for a node, the program will select a blank condition over a false one.<br /><br /> For an example, see the "sample_conditional_todo" file (by using the "File-&gt;Open Sample" command). format-condition
Conditional dialog picture Figure 5: Conditional Dialog doc_condition.png fig-5
Copying Formats Another method for changing data type formatting is to copy the formats from another TreeLine file. This is done with the "Data-&gt;Copy Types from File" command. All types from the chosen file are copied. Any types in the current file with matching names are overwritten, but types with unique names are retained. format-copy
Field Types fields Field Options The field type and options are set in the "Field Config" tab of the "Data-&gt;Configure Types Dialog" (see <a href="#fig-3">Figure 3</a>). The field type may be set to text, number, choice, combination, auto choice, date, time, boolean, URL, path, internal link, executable link, unique ID, email, or picture. Prefix and suffix text can be entered and will show up whenever the data is not blank.<br /><br /> There are also settings for text content handling that can be set to allow HTML rich text in the field data or to preserve line breaks (ignoring HTML code). If HTML rich text is used, carriage returns are ignored and non-escaped "&lt;", "&gt;" and "&amp;" symbols do not display. There is also a general option available that makes new fields default to HTML content. For an example of rich text use, see the "sample_char_format" file (by using the "File-&gt;Open Sample" command).<br /><br /> Several of the field types use a formatting string to define their output. For a list of available formatting characters, use the "Format Help" button. Entries in the data editor which do not match the format will cause the field name label to show in bold, and the output for that field will be replaced by "#####". field-options Text Type The default field type is a text field. These fields are edited using edit boxes in the data editor view. The edit box height expands when redisplayed after adding several lines of text. The edit box height can also be set explicitly in the "Field Config" tab. field-text Choice, Combination and Boolean Types The choice and combination field types allow for the selection of text items from a pull-down edit list. The formatting strings for these types list the items separated with the "/" character (use "//" to get a literal "/" in an item). Choice is used to select a single item and combination to select multiple items. The pull-down edit list for combination allows items to be added or removed. Also, the initial text of an item can be typed and auto-completed.<br /><br /> The boolean field type is similar to choice, but defaults to options such as "True/False", "yes/no" and "1/0".<br /><br /> There is also an AutoChoice field type. Like Choice, it provides pull-down selection items, but it does not have a formatting string. The pull-down is automatically populated with all previously used entries. field-choice Number Type In the number type, special characters in the format define the display of the numbers. The format uses a string of "#" (optional digit) and "0" (required digit) characters to define the output formatting. For example, pi formatted with "#.#" is "3.1" and formatted with "00.00" is "03.14". Regardless of the formatting, digits to the left of the decimal point are not truncated, since that would display an incorrect result. But use care to show enough decimal places (either optional or required) to avoid problems with round-off error.<br /><br /> The radix character can be specified as either "." or "," to handle internationalization. For use as a thousands separator, use "\," or "\.". For example, a large number may be formatted as "#\,###\,###.##" or as "#\.###\.###,##". Press the "Format Help" button from the field format dialog for more formatting details. <br /><br /> Unlike most other formats, the number type also uses the output format for display in the Data Editor. Of course, any new entry with a reasonable format is correctly interpreted (but the correct radix character must be used). field-number Date and Time Types In the date, and time field types, special characters in the formats are replaced by elements of the data, similar to number fields. Press the "Format Help" button from the field format dialog for formatting details. <br /><br /> There are also formats for these types under "Tools-&gt;General Options-&gt;Data Editor Formats". These control how these fields are displayed in the Data Editor view. Generally, entries in the data editor with various formats will be correctly interpreted regardless of this setting, but dates must use the correct day-month-year sequence. Also note that the date editor format does not support days of the week. Entries which cannot be interpreted will cause the field name to show in bold.<br /><br /> A default initial field value of "Now" can be used to get a time-stamp of node creation. field-date Linking Types The URL, path, and email field types are used to create links in the output. URL is for a standard web link (defaults to http:// unless otherwise specified), path is for a local file link (defaults to file:///), and email is for a mail link (defaults to mailto:). When clicked in the output window. these links open an external browser or email program. In exported HTML, they act as regular links. Simply enter the desired full path (such as "www.bellz.org/treeline/index.html") in the data editor. In Linux, setting the "BROWSER" environment variable to a string like "mozilla %s" will result in the desired browser being used.<br /><br /> There is also an internal link field type. It creates a clickable link in the Data Output window that selects the next node with matching text in its reference field. If link or reference fields contain multiple lines, each line is treated as a separate link or reference, respectively. If desired, the same field can be used as both the link and reference fields, so that clicking on a keyword in one node's field selects the next node that also has that keyword. If exported to HTML, the internal links function as links to page anchors, but, in cases with duplicate references, they only find the uppermost reference on the page instead of the next one. For an example, see the "sample_intern_links" file (by using the "File-&gt;Open Sample" command).<br /><br /> The ExecuteLink field type runs an external program when its link is clicked. The command to run is given by the text of the field. Or the field's prefix may contain the program name, so that the field's text is used for arguments or file names. A general option can disable these links when security is a concern.<br /><br /> The picture field type will show a referenced picture in the output. Enter the path to a local image file in the data editor. Supported image types include JPEG and PNG. GIF is support is optional - it may be supported depending on build options in the Qt library. Of course, when exporting HTML, the picture will show as long as the browser supports the format. field-link Unique ID Type A Unique ID field type is automatically loaded with unique numbers. This is often useful for ensuring that the link reference field is always unique.<br /><br /> Initially, the field's format string is set to the desired first number in the series, with optional leading zeros and optional leading or trailing characters. The initial number and the extra characters are useful to avoid duplication of numbers in different files. All nodes of the data type will get an automatically assigned ID. The field format string will always show the next available number, which will be given to the next new node of that type. field-unique Advanced Options The "Show Advanced" button in the "Field Config" tab of the "Data-&gt;Configure Types Dialog" brings up additional settings.<br /><br /> Link fields can display alternate text in place of the target URL. This is done by specifying an alternate text field.<br /><br /> There is also a setting to flag fields as required to be filled in. Those fields will be marked with an asterisk in the data edit view.<br /><br /> Fields can also be set as hidden, to prevent them from showing in the data edit view. field-advanced Tree Data Manipulation data Category-based Commands The "Data" menu contains commands for arranging and flattening the data by category and by reference. These methods are used to automatically add and remove levels of nodes below the current node in the tree.<br /><br /> The "Add Category Level" command allows you to select one or more of the fields that the child nodes have in common. These fields are used to create new parent nodes for the children, grouping them by common categories. For example, in a list of books, picking the "author_first_name" and "author_last_name" fields will result in a tree with the books under new nodes for each unique author.<br /><br /> The "Flatten by Category" command is almost the opposite of "Add Category Level". It eliminates any descendant nodes with children, transferring their data fields to their children. It will rename fields instead of overwriting data with the same field names, but this command is most useful when the children and parents are different types with unique field names. data-category Reference-based Commands The "Arrange by Reference" and "Flatten by Reference" commands arrange data nodes by using pointers to the value of their parent's reference data field. These commands rely on nodes having unique values in the reference field, such as an ID number. The "Flatten by Reference" command adds a field containing the parent's ID to each descendant node. It then places all of the nodes under the selected root node. The "Arrange by Reference" command does the opposite, placing each node under the parent with the referenced ID. If there are multiple nodes with the same ID, the nearest node above the child is chosen. Any nodes with lost parents are placed directly under the selected root node.<br /><br /> data-ref Sorting The "Data-&gt;Sort" command brings up the sort dialog. It contains options for sorting the entire tree, the selected branches, the selection's children or the selection's siblings. The sorting can be based on specific fields within specified types or on titles only.<br /><br /> Sorting by types allows the selection of several fields to be used as the first key, second key, etc. Multiple types can be selected in the list on the left. The fields that the selected types have in common are listed on the right. The fields to be used as keys are selected in order with the left mouse button and the direction is changed with the right mouse button. data-sort Filtering The "Data-&gt;Conditional Filter" command limits the display of nodes in the flat view based on user-defined rules. First, the type to be filtered is selected. Next, logical rules may be entered based on any of the type's fields (similar to <a href="#fig-5">Figure 5</a>). Multiple rules can be linked by the "and" and "or" operators (press the "Add Rule" button). The "True" rule can be used to show all of the nodes of a given type. Only descendant nodes that match the rules will be shown in the "Flat View" pane. The filtering stays in effect for the "Flat View" pane until it is cleared using the "Data-&gt;Clear Filtering" command (see <a href="#fig-6">Figure 6</a>).<br /><br /> The "Data-&gt;Text Filter" command works similarly, except it only prompts for a text search string. Only the nodes that have a match for that string somewhere in their field data are displayed in the "Flat View". Again, the filtering stays in effect for the "Flat View" pane until it is cleared using the "Data-&gt;Clear Filtering" command. data-filter
Filtering picture Figure 6: Filter Results doc_filter.png fig-6
Numbering The "Numbering" command is used to add number fields to descendant nodes. The number fields do not automatically update when the tree is modified - the "Numbering" command must be repeated. In the dialog, a new or existing field name is entered, and the root (selected) node may be included in the numbering if desired. One of three styles may be chosen: outline style restarts numbering for each group of children, section style appends the child's number onto the parent's number, and single level style numbers only the first level of children. The default formats can be used ("I, II..., A, B..., 1, 2..." for outlines; "1, 2..., 1.1, 1.2..." for sections), or custom formats may be specified for each level. The custom formats should contain one of the following characters: "1", "A", "a", "I", or "i". The series will continue from there (using numbers, letters or Roman numerals, respectively). The last occurrence of one of these characters in the format string is used - previous ones are assumed to be part of the format. data-number Change Selections Finally, the "Change Selected Data" command allows the values of a data field to be changed simultaneously for all selected nodes. Simply select the desired field and enter the new value. To erase field values, enter a character then delete it - this will indicate that a blank field is desired. data-change
Printing print Print Options The "File-&gt;Print Options dialog's first tab is "General Options". On the left, printing of the entire tree, the selected branches (all descendants) or the selected nodes can be selected. The first two options print all of the descendants with the children indented.<br /><br /> On the right are options for drawing tree structure lines, including the root node, including only open children and keeping the first child with its parent. The tree structure lines are drawn to connect parent and child nodes. They can make parent/child relationships easier to visualize, especially across multiple pages. The lines may not display as desired when using some HTML formatting tags.<br /><br /> By default, TreeLine will avoid breaking pages between a parent and its first child. This behavior can be disabled by disabling the "Keep first child with parent" option. print-options Page Setup The second tab of the "Print Options" dialog box is "Page Setup". On the left are settings for page size, orientation and units. On the right are settings for columns, indent offsets, and margins. print-setup Printer Font The third tab of the "Print Options" dialog box is "Font Selection". The printout will use the same font as the "Data Output" pane if the upper check box is checked. If it is unchecked, a font can be selected specifically for printing. print-font Header and Footer The fourth tab on the "Print Options" dialog is used to set the print header and footer. There are file info fields on the left and six header/footer edit boxes (left, center and right justified) on the right. These fields can be added and combined with other text in the edit boxes, just like in the "Output" tab of the "Data-&gt;Configure Data Types" dialog. The header and footer settings are saved with the TreeLine file. print-header Print Preview The print preview window can be shown by using the "File-&gt;Print Preview" menu or with the button on the "Print Options" dialog. Buttons on the window are used to navigate between pages, open the "Print Options" dialog, or open the print dialog. The print preview will show more detail if its window is made larger. print-preview Printing Problems Some printing problems, especially problems with margins and word-wrapping, can be eliminated by changing the print font to a font that is better supported by the printer. print-problems File Handling file File Compression A TreeLine file is in an XML text format. There are also options to work with compressed files (gzip format) to save space. Individual files can be set to compressed mode from either "Tools-&gt;File Options" or from the save-as dialog. There is also a general option to set the default mode for new files. file-comp File Encryption There is a file encryption option to password protect TreeLine files. Individual files can be set to encrypted mode from either "Tools-&gt;File Options" or from the save-as dialog. There is also a general option to set the default for new files. The encryption uses the SHA hash function as a stream cipher - it should be fairly secure. file-enc Auto-Save An auto-save feature can store unsaved files with a "~" appended to the file name. The backup files are automatically removed when the file is saved or TreeLine exits cleanly. The auto-save time interval is set in the general options. Setting the interval to zero disables this feature. file-auto-save Saved Tree States When opening a recently used file, TreeLine will restore the states of open and selected nodes. This information is stored in the user's TreeLine configuration file. If desired, this feature can be disabled with a general option. file-states Command Line Command line options allow non-interactive file importing and exporting. This allows automated runs to be scheduled. For more details, run "treeline -h" (or "treeline_dos -h" in Windows) from the command line. file-cmdline File Import import General Information A TreeLine file is in an XML text format. Other types of text files can be imported simply by opening them. Opening a file that is not valid TreeLine XML will result in a prompt for the type of import desired. import-general Delimited Text TreeLine will open a text file with a tree structure represented by tabs before each line. In this case, only the node title is imported, without any extra fields.<br /><br /> A tab-delimited table can also be imported. It becomes a single level of children under the root node, with each node containing fields from each table column. The first row of the table is used as field names and each subsequent row becomes a node. import-delim Plain Text There are two types of plain text import. One creates a separate node for each line in the file. The other creates a node for each paragraph, assuming the paragraphs are separated by blank lines. In both cases, the resulting TreeLine file will have all of the text under a single parent, but it is a good starting point. Additional structure can be added later. import-plain Treepad Files There is also a filter to import files from the Treepad shareware program. Only Treepad text nodes are supported. import-treepad Bookmark Files TreeLine will import bookmark files in both the Mozilla HTML format (Mozilla, Firefox and Netscape browsers) and the XBEL format (Konqueror, Galeon and Elinks browsers). Each bookmark becomes a node with a name and a link field. Some information in the files, such as visited dates and icon references, is not imported. For an example, see the "sample_bookmarks" file (by using the "File-&gt;Open Sample" command). import-bookmarks Generic XML TreeLine will import and export generic XML files. These routines do not have much intelligence - each XML element becomes a node and each XML attribute becomes a field. XML text content become fields named "Element_Data". This lets TreeLine function as a crude XML editor. import-xml ODF Text Document TreeLine will import Open Document Format (ODF) text documents, from applications such as OpenOffice.org and KWord. The node structure is formed based on the heading styles assigned in the document. Any text under each heading is assigned to that heading's node. The import filter is intended for simple text outlines only. No formatting is maintained, and objects such as tables and pictures are not imported. import-odf File Export export General Information Files are exported using the "File-&gt;Export" command. This will show a dialog box of available export types and options. export-general HTML Single-file HTML output is similar to printing, with similar options. It can include the print header and footer in the HTML, and the number of columns can be set.<br /><br /> There are also two multiple-file HTML export functions that create directory structures. Directories are named for the content of the reference data field, which must contain legal file names and not have duplicates under the same parent. The first form creates a table in each HTML file that contains the data for a set of siblings, as well as links to the parent and child pages. The second form creates an HTML page for every node, with a navigation pane on the left side of each page that contains links to the node's parent, uncles and children. export-html Delimited Text Data can be exported to tabbed title text and tab-delimited tables. These formats are the same as the corresponding import formats. When exporting to a table, only the first level of children is used, so you may wish to flatten the data before exporting. export-delim Bookmark Files TreeLine will export bookmark files in both the Mozilla HTML format (Mozilla, Firefox and Netscape browsers) and the XBEL format (Konqueror, Galeon and Elinks browsers). export-bookmarks Generic XML TreeLine will import and export generic XML files. These routines do not have much intelligence - each node becomes an XML element and each field becomes an XML attribute, except for fields named "Element_Data" that become the element's text. This lets TreeLine function as a crude XML editor. export-xml XSLT In addition to exporting data, the format for a file can be exported to an XSLT file. This can be used to display the XML data from a native TreeLine file in a compliant browser without exporting the data to HTML. Recent versions of most browsers are XSLT compliant. Note that complex field type formatting will be ignored by the XSLT.<br /><br /> When exporting to XSLT, a link to the XSL file is added to the TreeLine file. Note that the TreeLine file must then be saved. In general, the TreeLine file should have a .xml extension so that the TreeLine file can be opened directly in a compliant browser. After that, the XSL file does not need to be re-exported for data changes (only for formatting changes).<br /><br /> If HTML tags are used in data formats that will be exported to XSLT, they should use xHTML style (&lt;br /&gt; instead of &lt;br&gt;). When exporting, there is a prompt for the name of an optional style sheet (css). This name is stored in the TreeLine file as the default for future exports. Also, the reference to the XSLT file in the TreeLine file may be removed with the "Tools-&gt;Remove XSLT Reference" command. export-xslt ODF Text Document TreeLine will export an outline to an Open Document Format (ODF) text document, compatible with OpenOffice.org and KWord. The title of each node is assigned a heading style at the appropriate level. Any other text in the output of each node becomes normal text under the heading. The export filter is intended for simple text outlines only. Any HTML formatting is stripped, and objects such as tables and pictures are not supported. export-odf Customizations custom Options TreeLine's behavior can be modified with several settings available in "Tools-&gt;General Options". Most of these options are covered elsewhere in this document.<br /><br /> Under Windows, a "treeline.ini" file in the user's application settings directory is used by default to store the settings. For portable use of TreeLine, that file can be manually moved to TreeLine's "lib" directory. Once it exists, a "treeline.ini" file in the "lib" directory will be used and updated automatically if the file isn't also in the user's application settings directory. custom-options Fonts Fonts used in the tree views, the output view and the editor views can be set in the "Tools-&gt;Set Fonts" menu. custom-fonts Keyboard Shortcuts Keyboard shortcuts can be customized by using the "Tools-&gt;Set Keyboard Shortcuts" command. The shortcut editor has tabs for menu and non-menu based commands. Simply type the new key sequence with the appropriate field selected. custom-keys Toolbars An editor to customize the toolbars is available from "Tools-&gt;Customize Toolbars". The number of toolbars can be set and the buttons on each can be defined. custom-tools Colors User interface colors can be set using the last three items in the "Tools menu. If the "Use Default System Colors" command is enabled, the user background and text color settings are overridden. custom-color Tree Icons There is an icons directory in the user configuration directory ("~/.treeline-1.x/icons" on Linux, "Documents and Settings\&lt;user&gt;\Application Data\bellz\treeline-1.x\icons" on Windows). Image files (PNG or BMP) placed into this directory are available for use as tree icons. custom-icons Plugins plugins Description TreeLine has an interface to plugin extension modules. This allows extensions to be written by various coders that provide functionality needed by a few users without adding bloat to the main application. The plugins can add new menu items, access the TreeLine data and read and write TreeLine Files.<br /><br /> Currently available plugins are listed on the "Downloads" page of the TreeLine web site. plugins-intro Installation The plugins are installed by copying their Python file (*.py) to a plugins directory. This can be a directory in the TreeLine installation ("&lt;prefix&gt;/lib/treeline/plugins/" on Linux or "TreeLine\lib\plugins\" on Windows) or in the user configuration directory ("~/.treeline-1.x/plugins/" on Linux, "Documents and Settings\&lt;user&gt;\Application Data\bellz\treeline-1.x\plugins\" on Windows). A list of loaded plugins can be found using the "Help-&gt;About Plugins" command. plugins-install Interface Information about writing new plugins can be found in the "plugininterface.py" file in the TreeLine source code. plugins-intrf Common Problems errors Closed Panes If one of the view panes is not visible when it should contain data, it has probably been collapsed to a size of zero. This can apply to the left pane and to the upper and lower right panes. To fix this, look for a thin stripe along one of the view borders that can be dragged back into its proper position. errors-panes XML Error An error message, "Error loading XML Parser", typically means that TreeLine could not find a necessary XML library. Under Linux, Python uses external libraries for parsing. Installing either the expat library or the PyXML package should fix the problem. Under Windows, Python includes a parser, so this error should not be seen unless files are missing or corrupt. errors-xml Printing Some printing problems, especially problems with margins and word-wrapping, can be eliminated by changing the print font to a font that is better supported by the printer. errors-print
Revision History revs November 11, 2011 - Release 1.4.1 (stable release) Bug Fixes: Multiple encrypted windows Fixed failures due to bad internal references when opening encrypted files in multiple windows. Auto-open imported file Avoid corruption of user option settings after attempting to automatically open an imported file as the last file used. Encrypted imported files Fix problems with imported files when the user sets new files to be encrypted by default. Copy types from an encrypted file Prompt the user for a password when copying types from an encrypted TreeLine file. April 14, 2011 - Release 1.4.0 (new stable release) Notes: Stable release This is a new stable release, ending the 1.3.x development series. Translations This release is available in English, French and German, although the French documentation is not completely up to date. Volunteers to do additional translations are welcome. Updates: File browser path The file browser used for editing path-based fields now remembers the previously used directory. Toolbar icons Added icons for the New Window and Close Window commands so they can be added to toolbars. Bug Fixes: HTML export columns Fixed a bug that could keep the column control from being enabled during HTML export. Major New Features Since 1.2.x: Multiple document interface Different files can be open in multiple TreeLine windows within the same TreeLine session. This is controlled with commands in the new "Window" menu. Multiple views of same file The "Window->New Window" command creates a new window with views of the same TreeLine file. Changes in either window are saved in the same document. Attach to session Starting TreeLine (from a shortcut, file association or command line) will activate an existing TreeLine session if it is already running. Insert inline links An "Add internal link" context menu has been added to text boxes in the data editor. It prompts for another node to be clicked with the mouse, then an inline internal link is added with that node's reference information. HTML directory page export Added an "HTML directory pages" export option. This will export a directory structure with an HTML file for every node. A navigation pane on the left side of each page contains links to nearby nodes. Restore interface defaults New dialog buttons will restore the default configuration when customizing toolbars or shortcuts. Help viewer search function A new text search function has been added to the help viewer. January 5, 2011 - Release 1.3.5 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work. Updates: Code cleanups Incorporated some minor code clean-ups (thanks to Erik Wegner for identifying them). Windows binary The Windows binary is now based on the Python 2.7, Qt 4.7 and PyQt 4.8 libraries. Bug Fixes: Sorting by type Fixed incorrect sorting by type with siblings of mixed types. Print preview Fixed problems showing child lines in print previews if the lines option was changed with the preview open. Rename option Really disable click-to-rename if the general option is unselected. September 22, 2010 - Release 1.3.4 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work. Updates: Icon image formats Added ico and gif as valid tree icon types. French translation The French GUI translation has been updated, but the documentation translation is still out of date. German translation Minor updates have been made to the German GUI translation. Windows binary The Windows binary is now based on the Python 2.7 library. Bug Fixes: Window menu The list of windows in the Window menu is now properly updated after saving a file with a new name. Spell check Fixed a bug that could cause spell checks to hang. June 24, 2010 - Release 1.3.3 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work. German translation only German is the only GUI and documentation translation that has been updated so far. New Features: Help viewer search function A new text search function has been added to the help viewer. Data Editor view paging shortcut The keyboard shortcuts for scrolling the right-hand views (Ctrl+Page Up and Ctrl+Page Down) now cycle through the Data Editor view pages if there is no more room to scroll Updates: System requirements The Linux system requirements have been updated to indicate that Qt 4.4 or higher is now required. Windows installer The Windows installer now sets the sample and readme documents to read-only status. German translation The German GUI and documentation translation has been updated. Bug Fixes: Window cascading The cascading of new windows has been fixed. It now references the current position of the previous window, and it avoids cascading windows off of the screen. Data Editor resizing The Data Editor widgets are now sized correctly when TreeLine is started in Data Editor mode. Depreciation warnings The spell checker and the Linux installation script have been updated to avoid subprocess command depreciation warnings. May 14, 2010 - Release 1.3.2 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work. Translations not updated The GUI and documentation translations have not yet been updated. Updates: Documentation Updated the ReadMe file to cover the new features added during this development cycle. Bug Fixes: Save prompt Fixed a major bug that prevented files from being saved at the "Save Changes?" dialog when closing a modified file. April 8, 2010 - Release 1.3.1 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work. Documentation not updated The documentation has not yet been updated to cover the new features. Translations not updated The GUI and documentation translations have not yet been updated. New Features: Window update command Added a menu command that updates another window containing the same file (without waiting for a focus change). This command also has a keyboard shortcut (Ctrl+W). Hiding the status bar A menu option has been added to hide the status bar. There is also a new general option to hide it at startup. A hidden status bar is automatically shown temporarily for important messages. Toolbar restore defaults A new button on the toolbar editing dialog will restore the default toolbar configuration. Keyboard shortcuts restore defaults A new button on the keyboard shortcuts editing dialog will restore the default shortcut configuration. Updates: Windows portability Portability on Windows has been improved. A "treeline.ini" config file in TreeLine's "lib" directory will be used if it's present and there are no config files in the user's application data directory. Change selected data The Change Selected Data command has been updated to use Data Editor formats for dates and times. DOS TreeLine executable file An alternate TreeLine executable file (treeline_dos.exe) has been added to make batch command-line operations work from Windows. Plugin interface improvements Additional plugin methods have been added for handling files and languages. Bug Fixes: Spell check Fixed a problem spell checking with older versions of aspell when Unicode characters were present. October 14, 2009 - Release 1.3.0 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is not feature complete and probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work. Documentation not updated The documentation has not yet been updated to cover the new features. Translations not updated The GUI and documentation translations have not yet been updated. New Features: Multiple document interface Multiple TreeLine windows with different files can be open within the same TreeLine session. The "Open files in new windows" general option controls whether new windows are used when opening or creating files. Window menu A "Window" menu has been added with commands to create new windows, close windows, or select from a list of current windows. Multiple views of same file The "Window->New Window" command creates a new window with views of the same TreeLine file. Changes in either window are saved in the same document. This can be used to work in different sections of the file simultaneously. For performance reasons, the non-active window does not show the changes made in the active window until the non-active window is focused. Attach to session Starting TreeLine (from a shortcut, file association or command line) will activate an existing TreeLine session if it is already running. A new window will be opened if an unopened file was specified by file association or in the command line. Insert inline links An "Add internal link" context menu has been added to text boxes in the data editor. It prompts for another node to be clicked with the mouse, then an inline internal link is added with that node's reference information. HTML directory page export Added an "HTML directory pages" export option. This will export to a directory structure with an HTML file for every node. A navigation pane on the left side of each page contains links to the node's parent, uncles and children. Updates: General options dialog to 3 columns The general options dialog has been split into three columns for better viewing on small, wide displays. Add exports to plugin interface Additional export commands and export options have been added to the plugin interface. No initial default window position To avoid a problem on the Mac platform, when TreeLine is run for the first time, it lets the OS determine the window position. October 12, 2009 - Release 1.2.4 (stable release) Bug Fixes: PyQt 4.6 workaround Added a work-around for a bug in PyQt 4.6's clipboard support that can prevent TreeLine from starting. Tree horizontal scroll Prevent an uncommanded horizontal scrolling to the left of the tree view when changing the selection. May 5, 2009 - Release 1.2.3 (stable release) Updates: Windows 98 compatibility A new build has been created for use on the Windows 98 operating system. This fixes problems that made TreeLine 1.2.2 unusable on Windows 98. Copy persistence Data copied to the Windows clipboard is now preserved after exiting from TreeLine. Bug Fixes: Copy/paste error Fixed a bug on Windows that showed an error message at program exit if the copy command had been used during the session. HTML directory export In an HTML directories export, characters that are illegal in directory names are now correctly removed. January 8, 2009 - Release 1.2.2 (stable release) Updates: German translation Added German translations of the GUI and the ReadMe file. Translation updates Added a few missing strings to the translation files. Python 2.6 compatibility Substituted some imported modules to avoid deprecation warnings in Python 2.6. Note that Python 2.3 is no longer supported. Windows binary The windows binary has been updated to use Python 2.6, and the installer has been tweaked to reduce DLL conflicts. Bug Fixes: Edit view tabbing Fixed problems using the tab key to change focus from the tree to the data editor. UniqueID fields in titles ID numbers are now properly assigned to new nodes with UniqueID fields in the title. Child count in titles Fixed problems creating nodes with child count fields in the titles. November 4, 2008 - Release 1.2.1 (stable release) Bug Fixes: Rename input loss Fix the loss of entered text when the renaming of a node is ended by a command that changes the selected node. Unicode file names Problems with opening files with Unicode file names from the command line or from a file association have been fixed. Windows JPEG support Fixed problems with JPEG and GIF image support in the Windows binary build. Tree view scrolling When commands change the tree view, it no longer scrolls vertically unless the current node is not visible. Compressed encrypted files Fixed problems with files set to both compressed and encrypted modes. But note that compressing an encrypted file still does not significantly reduce its size. Mac support Problems on the Macintosh platform with detecting the text encoding and with setting the default theme have been fixed. June 17, 2008 - Release 1.2.0 (new stable release) Notes: Stable release This is a new stable release, ending the 1.1.x development series. Translations This release is only available in English and French. The 1.0.2 release is still available, including German, Portuguese, Russian and Spanish translations. Volunteers to update the translations are welcome. Updates: French GUI translation The French translations of the GUI and the ReadMe file have been updated. Bug Fixes: Replace with backup file When restoring an auto-saved backup file, TreeLine no longer replaces a the main file with the backup if the backup is not a valid TreeLine file. Configure dialog Fixed a minor bug in the Configure Data Types dialog caused by unselecting a field in the Output pane. Major New Features Since 1.0.x: Ported to Qt4 TreeLine was extensively rewritten to port it to the Qt4 library (previous versions used Qt3.x on Linux and Qt2.3 on Windows). Benefits include updated widgets and removal of the non-commercial license exception in Windows. Templates Preformatted templates have been added for new TreeLine documents. User-created templates can also be added. Right view content The content of right-hand views varies based on the selection and other options. A single selection still shows panes with the parent and its children, but a multiple selection will show all of the selected nodes in a single pane. Also, the Data Output view can optionally show an indented view of all descendants, and the Data Editor view splits the display into pages to speed up display. Multiple selection use More commands can make use of a multiple selection instead of just using the single active node. Also, an option can set the sequence of showing and exporting multiple selections to either tree order or selection order. Flat view and filtering There is a tab on the left-hand view for a flat node list showing the selected nodes and all of their descendants. It is most useful for showing the results of filtering. There is a conditional filter for specific rules and a text filter that searches all fields for a string. Dialog changes Several dialogs have been reworked for usability. The Configure Data Types and Print Options dialogs use tabs for better organization. The Configure dialog also has initially hidden advanced features and is now modeless, so it can be left open while applying configuration changes. The Sort, Export and Print Preview dialogs have also been updated. Tree navigation Navigation through the tree is simplified by new commands that step through the selection history. Also, search strings are now highlighted in the Data Output view for text searches. Shortcuts and customization New dialogs have been added to customize keyboard shortcuts and tool bar buttons. There is also a directory for user-defined tree icons. ODF New file import and export options convert between an outline and an Open Document Format (ODF) text document, compatible with OpenOffice.org and KWord. Formatting is not maintained. Unique ID fields A Unique ID field type has been added that is automatically loaded with unique numbers. It is useful for link references. January 15, 2008 - Release 1.1.10 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is nearly ready to become a stable release, but it could still contain bugs. Testing and bug reports are appreciated. Missing features The French translation of the GUI is the only one completed so far. None of the documentation translations have been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages may not be available yet for these libraries in some distributions. Delete old config file If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\&lt;login&gt;\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release. Updates: HTML directories export Exporting to HTML directories has been improved. The file names are generated from the reference field (were from the first field). Internal links to nodes now work properly in the exported files. Save as default directory When saving a new file, the Save-As dialog now defaults to the top directory in the recent file list. French GUI translation A translation of the GUI in French is now available. The i18n installation file is a separate download. Documentation The ReadMe documentation file has been updated to clarify certain usage issues. Bug Fixes: Setting files to generic The other controls in the configuration dialog are updated immediately when setting a data type to be derived from a generic. Fix http load plugin A recent file list bug that affected the httpload plugin has been fixed. Also, a library need by the plugin (urllib) has been added to the Windows installation. Status bar error An error message about removing a status bar widget has been eliminated. Mac encoding error An error on a Mac when setting the language encoding has been fixed. Unicode file paths Various issues with file paths containing Unicode characters have been fixed. June 13, 2007 - Release 1.1.9 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It could contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.x) should be used for critical work. Missing features The translations of the GUI and of the documentation have not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. Delete old config file If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\&lt;login&gt;\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release. Updates: Filter result selection Initially select the top filtering result in the flat view to avoid issues with a blank selection. Flat view rename Avoid entering rename mode when clicking on a multiple selection in the flat view. Icon dialog size Maintain the size of the type icon dialog during a session. Print unit options Remove the redundant printing units option from the general options dialog. Documentation Extensively update the ReadMe documentation to match the current feature set. TODO template Add a new file template for TODO lists. Bug Fixes: Paste references Fix incorrect internal references in groups of pasted nodes. Tree rename Abort a tree rename operation if the new name does not match the title format. Add numbering field Correctly add a field to the generic type when numbering a derived type. Update field choices Properly update the pull-down choices for a choice or combination field after a configuration change. Enable conditional type button Enable the conditional type button immediately after defining a type as derived. Remove page number field Remove the page number field from the available file reference fields in the main configuration settings. Filter menu update Properly update the Data menu after starting a filtering operation. Missing language code Fix an error due to a missing system language setting. Unknown field type Properly handle a file open error due to an unknown field type. May 22, 2007 - Release 1.1.8 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.x) should be used for critical work. Missing features The documentation and GUI translations have not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. Delete old config file If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\&lt;login&gt;\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release. Updates: Command line ODF translation Translation of ODF text documents has been added to the command line batch processing options. Use LC_MESSAGES for language The environment variable LC_MESSAGES is now checked before the LANG variable to determine the user's preferred language. Bug Fixes: Problem with Unicode user name (option file) Problems writing the options file when there are Unicode characters in the username have been fixed. Miscellaneous Unicode bug fixes Various minor bugs in Unicode handling have been fixed. May 3, 2007 - Release 1.1.7 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is not feature complete and probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.x) should be used for critical work. Missing features The documentation and GUI translations have not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. Delete old config file If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\&lt;login&gt;\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release. New Features: Templates Preformatted templates have been added. The File-&gt;New command brings up a dialog with a list of available templates. The dialog is also shown at startup if the recent file list is empty. Default templates are installed into TreeLine library directories. New templates can be added to a user templates directory in the user settings location. Template file names consist of a number for sorting, a language code and the name of the template. Updates: Native Save As dialog The Save As dialog in Windows is now the native dialog. Bug Fixes: ODF export Problems exporting to an ODF text document when blank fields are encountered have been fixed. Installer error messages Added appropriate error messages to the Linux installer if some installer files are not found. April 17, 2007 - Release 1.1.6 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is not feature complete and probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.x) should be used for critical work. Missing features The documentation and GUI translations have not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. Delete old config file If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\&lt;login&gt;\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release. New Features: Descendants in output view A new command (View-&gt;Show Output Descendants) toggles the Data Output child view from showing only child nodes to showing an indented view of all descendant nodes. A button was added to the default toolbar for this command. Also, a new general option can make the descendant view the default at startup. ODF export A new file export option converts an outline to an Open Document Format (ODF) text document, compatible with OpenOffice.org and KWord. The title of each node is assigned a heading style at the appropriate level. Any other text in the output of each node becomes normal text under the heading. Any HTML formatting is stripped. ODF import ODF text documents can be imported. The node structure is based on the heading styles assigned in the document. Any text under each heading is assigned to that heading's node. Formatting is not maintained. Updates: Modify config from Data Editor context menu New commands were added to the Data Editor context menus that open the Configure Data Types dialog with the editor's type and/or field already selected. Shortcuts for editor html tag commands Keyboard shortcuts were added for the commands that add HTML tags in the Data Editor. Icons were also added to these commands, so the toolbar editor can be used to place them on a toolbar if desired. Plugin improvements The interface for writing plugin modules was improved by adding interfaces to the keyboard shortcut editor and the toolbar editor. Also, a plugin interface to get the plugin's directory was added. Default field text Any default text for new fields is added when a node's data type is changed (not just to a new node), as long as there isn't already data in a field with the same name. Bug Fixes: Auto open problem Fixed a problem with the automatic opening of the last file used if there are no valid recent files. Crash if user ID not found Fixed a crash at file open if the file's owner is not found in a Linux machine's user password database. Text display issues Issues with the display of text have been fixed if the Qt library being used is version 4.2.3 or greater (the Windows build now uses 4.2.3). These issues showed up when scroll bars were shown and could hide the right edge of the text or the last line. Paste or drag nodes from other session Multiple issues with dragging or pasting nodes between sessions of TreeLine were fixed. Undo updates an open config dialog If the Configure Data Types dialog is open, it is properly updated after the use of the undo command. Find dialog focus The find dialog is properly focused when repeatedly shown. Printing no nodes Fixed problems occurring when attempting to print an empty branch or an empty selection. HTML directory export Fixed problems with the export of HTML directories. It now removes illegal characters from directory names created from field contents. Auto choice field updates Auto choice fields are now properly updated after configuration changes. Unique ID field help A missing help menu was added to the Unique ID field format editor. February 22, 2007 - Release 1.1.5 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is not feature complete and probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for critical work. Missing features The documentation and GUI translations have not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. Delete old config file If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\&lt;login&gt;\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release. New Features: Unique ID fields A Unique ID field type has been added. The field is automatically loaded with unique numbers. Initially, the field's format string is set to the desired first number in the series, with optional leading zeros and optional leading or trailing characters. All nodes of the data type will get an automatically assigned ID. The field format string will always show the next available number, which will be given to the next new node of that type. Sort dialog The node sorting commands have been consolidated into a single new dialog. This provides more options for selecting what to sort (all nodes, branches, children or siblings) and how (by types and fields or by titles). Selective numbering An option has been added to the numbering dialog to only number nodes where the number field already exists. This allows node types in the branch that do not have the number field to be skipped. Updates: Tree view scrolling The tree view no longer scrolls horizontally when the selection is changed. Data Editor focus The focus behavior of the Data Editor view has been tweaked to allow easier tabbing between widgets. No multiple selection rename Clicking on a selected node to rename it is now disabled when multiple nodes are selected. This avoids initiating a rename when the intent was to modify a multiple selection. Insert sibling context menu The Insert Sibling Before command has been added to the node right-click context menu for consistency. Smoother spell checking The Spell Check dialog is no longer hidden and re-shown between each misspelled word, resulting in smoother operation. Configure Data Types dialog The Output tab of the Configure Data Types dialog has been adjusted to avoid changing the dialog size when toggling the advanced options. Print Options font selection The fonts in the Print Options dialog are sorted case insensitively, and the size of the dialog is fixed regardless of the selected font. Bug Fixes: File open from flat view Fixed a crash when opening a file with the flat view shown. No toolbars Fixed problems occurring when the number of toolbars was set to zero. Undo of data commands Fixed undo operations for Numbering, data Add Category Level and Arrange by Reference commands. Commands while renaming Fixed problems with undo and delete commands issued while a node is being renamed. Type and field renames Fixed problems with renaming data types and fields in the Configure Data Types dialog. Reference types Avoid adding duplicate types to Other Field References in the advanced Output tab of the Configure Data Types dialog. Cutting the root node No longer allow the Edit Cut command to operate if the root node is selected. Spell check freeze Fixed a freeze of the spell checker on some platforms. Status line An empty rectangle that was sometimes shown on the status line at the bottom of the main window has been removed. Generic XML import An occasional problem with the import of generic XML was fixed. Import bad Unicode The importing of files with bad Unicode characters has been improved. Tree state restoring The restoring of tree states when opening a file has been made more robust. Miscellaneous tracebacks Fixed several other minor tracebacks that can show up in a terminal or log file. January 18, 2007 - Release 1.1.4 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is not feature complete and is sure to contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for critical work. Missing features The documentation and GUI translations have not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. New Features: Selection history Previous and Next Selection commands have been added to the View menu. These commands step through a history of tree selections for quicker navigation. Search highlighting When using the Find command with the Data Output view visible, the search string occurrences are highlighted in the output. Note: in Linux, this feature requires Qt v4.2 or greater and PyQt v4.1 or greater. User icons An icons directory has been added to the user configuration directory ("~/.treeline-1.1/icons" on Linux, "Documents and Settings\&lt;user&gt;\Application Data\bellz\treeline-1.1\icons" on Windows). Image files (PNG or BMP) placed into this directory are available for use as tree icons. Move commands "Move First" and "Move Last" commands have been added to the Edit menu. These commands move a child node to be the first or last child of its parent. Updates: Tree scrolling Horizontal scrolling of the tree view when the contents are wide is now supported. Command line The non-interactive command line options have been ported from the stable version. These options allow batch processing of file imports and exports. Plugin support Support for plugin extension modules has been ported from the stable version. The interfaces to TreeLine functions are unchanged, but existing plugins will need to be ported to the Qt4 library. An updated version of the httpLoad plugin is available. User plugin directory A plugins directory has been added to the user configuration directory. This allows plugin installation without elevated privileges. Translation support The code required to support GUI translations has been ported from the stable version, but the actual translations have not yet been updated. Bug Fixes: Windows copy/paste The problem with cut/copy/paste operations in Windows that produced an error message at application exit was fixed. Data config undo Fixed several problems with the undo of changes to the Data Type Configuration. Renaming nodes A bug that could sometimes add an extra node during renaming in the Tree View has been fixed. Spell check replace Problems encountered when typing a replacement word during spell check have been fixed. Spell check language A spell check language setting in File Options that is not available on the system is now ignored. December 21, 2006 - Release 1.1.3 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is not feature complete and is sure to contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for any critical work. Missing features Some features from the stable release are not yet implemented here. These include command line options, plugin support and translation support. Also, the documentation has not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. Known bugs One known bug in the Windows version produces an error message at application exit if cut/copy/paste operations were done during the session. New Features: Keyboard shortcut editor A new built-in editor for keyboard shortcuts has been added to the Tools menu. It has two tabs: one for menu commands and one for other commands. Simply press the desired key combination to change the shortcut in the selected entry. In general, printable characters without Ctrl or Alt modifiers should not be used for shortcuts. Although the editor will accept them, they may not work properly when certain widgets are focused. Tool bar customization A dialog to customize the tool bars has been added. It is accessed from the Tools menu. This allows the user to select small or large icons, define the number of tool bars, and define the commands included in each tool bar. Updates: Restoring node states The saving of node open/close states for recently opened files has been ported from the stable version. Restoring split sizes The restoring of splitter sizes and the active right-hand tab from the previous session has been ported from the stable version. Config file version The TreeLine configuration files now include a version number (.treeline-1.1 on Linux, treeline-1.1.ini on Windows) to avoid incompatibility issues with previous versions. New keyboard shortcuts The default non-menu keyboard shortcuts have been changed to control-key sequences that function regardless of the focused widget. Incremental search The incremental node title search has been ported from the stable version. The default keyboard shortcut is now Ctrl+/. Add node on enter The option to add a node when pressing the enter key has been ported from the stable version. Bug Fixes: Flat view searching Searching for nodes with the flat view active now only finds nodes actually present in the flat view. Drag & drop fix Problems with drag &amp; drop of nodes with Unicode content were fixed. Config dialog show icon The Icon Selection dialog under Configure Data Types now scrolls to the currently selected icon when opened. November 22, 2006 - Release 1.1.2 (unstable development snapshot) Notes: Same All of the notes listed for Version 1.1.1 still apply. Bug Fixes: Combo box crash Fixed a serious bug that caused TreeLine to crash when using a pull-down combo box on a field in the Data Editor view. November 21, 2006 - Release 1.1.1 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is not feature complete and is sure to contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for any critical work. Missing features Several features from the stable release are not yet implemented here. These include keyboard shortcuts for non-menu items, command line options, plugin support and translation support. Also, restoring of tree node open/close states, window split locations and active tabs are not yet available. And the documentation has not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. Known bugs One known bug in the Windows version produces an error message at application exit if cut/copy/paste operations were done during the session. New Features: Printing implemented Printing functionality has been added to the development series. It now includes options to print the entire tree, the selected branches, or just the selected nodes. Printing interface A new Print Options dialog box is tabbed to give easier access to all of the options. There are buttons that give a more logical progression from the options to the print preview and to the print dialog. Updates: Configure dialog buttons The buttons at the bottom of the Configure Data Types dialog have been changed to OK/Apply/Reset/Cancel. This can reduce the number of mouse clicks. Initial configure dialog type The Configure Data Types dialog now defaults to the selected node's type when it is shown. Moved .ini file On Windows, the TreeLine.ini file has been moved from the installation directory to a location under the "Documents and Settings" folder. This avoids problems on multi-user systems and for users with limited access rights. Bug Fixes: Configure dialog updates The Configure Data Types dialog now updates properly after copying types from another file, adding numbering, and rearranging by category and reference. Drag & drop fix Various problems with node drag &amp; drop that could result in inadvertent drops were fixed. Splitter height recall A bug that caused the right-hand view splitter to reset to even sizes was fixed. Saving file info field formats Problems with saving changes to the format of file information fields were fixed. Fixed help viewer issues Issues with some internal links and pictures in the help viewer were fixed. September 12, 2006 - Release 1.1.0 (unstable development snapshot) Notes: Development release This is an unstable development snapshot. It is not feature complete and is sure to contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for any critical work. Missing features Several features from the stable release are not yet implemented here. These include printing, keyboard shortcuts for non-menu items, command line options, plugin support and translation support. Also, restoring of tree node open/close states, window split locations and active tabs are not yet available. And the documentation has not been updated. Qt4 required On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions. Known bugs One known bug in the Windows version produces an error message at application exit if cut/copy/paste operations were done during the session. New Features: Ported to Qt4 TreeLine was ported to the Qt4 library. This involved a major rewrite of the code. The previous versions used Qt3.x on Linux and Qt2.3 on Windows. Benefits include updated widgets and removal of the non-commercial license exception in Windows. Tabbed config dialog The Configure Data Types dialog uses tabs for better organization. Many tabs contain advanced features that can be shown if desired. The dialog is also modeless, so it can be left open while applying configuration changes. Right view content The content of the right-hand view varies based on the kinds of nodes selected. With a single parent node selected, it splits the view, showing the parent and its children (same as version 1.0.0). With a multiple selection or with a childless node selected, it shows all of the selected nodes in a single pane. Node selection sequence A general option sets the sequence for multiple selection to either tree order or selected order. When nodes are selected with control-click under the selected order option, they show in the right-hand view in the order they were picked. This sequence also applies to copy and export commands. Multiple selection use More commands make use of a multiple selection instead of just using the single active node. For example, the paste command will add duplicate children under each selected destination node. Export node selection The export command includes options to export the entire tree, the selected branches, or just the selected nodes. Data edit view speedup In order to render the Data Editor right-hand view faster, there is a general option to set the number of "pages" initially shown. By default, only one page is shown. Buttons allow toggling to the next page or all pages. Flat view added There is a tab on the left-hand view to show a flat node list. It shows the initially selected nodes and all of their descendants. It is probably most useful when using filtering commands. Filtering A conditional filter selects nodes based on rules for specific data types and fields, and a text filter selects based on finding the given text in any field. Both filters show their results in the tabbed "Flat View" and remain in effect until removed. Child count field In the advanced mode, a child count field reference can be added to the output configuration. It shows the number of children at a specified descendant level. July 6, 2006 - Release 1.0.0 Updates: Version numbering A new version numbering system is being introduced. This release is 1.0.0, to better indicate its stability. The 1.1.x series will be started soon for less stable development releases. German ReadMe The German translation of the ReadMe file has been updated. Bug Fixes: Multiple selection drag Fixed a problem with duplicate nodes showing up when pasting multiple nodes on Windows. Improper generic types Deriving types from other derived types is no longer allowed, since it did not function properly. Encryption fix A problem with saving an encrypted file in the German version of TreeLine has been fixed. February 16, 2006 - Release 0.14.1 Updates: Improved XSLT export Exported XSLT files now support field prefixes and suffixes. Thanks to Roland Meister for supplying patches. Added plugin functions Added functions to the plugin interface to allow plugins to open and close nodes. Bug Fixes: Unicode in field names Fixed a problem with displaying output from fields with non-ASCII characters in their names. Encrypted auto-save Auto save now works properly with encrypted files. December 20, 2005 - Release 0.14.0 New Features: Type inheritance Data types can be set to derive their field settings from a generic type. This allows types with different output formatting to always use the same field data. Any changes to the generic's list of fields and field types are automatically reflected in the fields of all derived types. A derived option can be selected when copying a type, or a generic type can be specified from the derived type's "Advanced Configuration" dialog box. Conditional types Conditional expressions can be used to automatically assign a data type based on each node's content. Conditions are assigned to a generic type and its associated derived types. This allows the automatic assignment of different output formatting or different icons depending on each node's field data. Conditions are specified from each type's "Advanced Configuration" dialog box. Conditions do not have to be set for all types in a family, since, if no conditions are true for a node, the program will select a blank condition over a false one. In addition, conditions that are always "True" or "False" are available to toggle the output format of all nodes of a certain type simultaneously by modifying the condition. Spell check language By default, the spell check uses dictionaries for the current operating system language. A new option under "Tools-&gt;File Options" can be used to specify an alternate two-letter language code for the current TreeLine file. This only works with aspell and with the appropriate aspell dictionaries available. Open sample menu A new menu item, "File-&gt;Open Sample", is a shortcut to the directory containing sample template files. Help menu A new menu item, "Help-&gt;Help Contents", directly opens the "Using TreeLine" section of the ReadMe file. Updates: Type change title When changing a node's type, if the result would have been a blank title, TreeLine now adjusts the new title fields to maintain the original title text. Picture paths Picture links with relative paths now resolve their reference based on the directory containing the current TreeLine file. Font tag selection When applying a font tag in the editor view, the selection is maintained. This allows multiple tags to be applied. Prefix/suffix tags in titles HTML tags in field prefixes and suffixes are now removed in node titles. HTML output Exported HTML has been made somewhat more compliant with standards. Windows installer option A new option on the windows installer toggles the writing of installation directory and uninstall information to the registry. When this and other options are disabled, TreeLine can be installed without modifying the registry. New sample files Four more sample files have been included. These files provide examples of using TreeLine's features and can be used as templates. ReadMe changes The ReadMe file has been updated with descriptions of the sample files, with more internal links, and with screenshots. Bug Fixes: Reference field save A problem with saving reference field settings for unused node types was fixed. Copied formats Formats copied from another file are now available immediately. Escaped characters in titles Node titles from HTML-enabled fields that contain escaped characters (&lt;, &gt;, &amp;) are now properly displayed. Default new node values Fixed some minor bugs concerning setting the default field value for new nodes. HTML directories output A problem with exporting HTML directories with very recent versions of the PyQt library was fixed. Child separators When displaying child data in node output, extra separators between blank entries are no longer shown. November 1, 2005 - Release 0.13.1 New Features: Recent file option The number of recently used files listed in the File menu can be set using a new general option. Note that this also controls how many files have their tree states (open/closed nodes, etc.) saved. Updates: Language setting TreeLine now sets the interface language using the LANG environment variable (if set) before falling back to other locale settings. This allows the operating system's locale settings to be overridden. Bug Fixes: Locale error Problems starting TreeLine when the locale's LANG variable ends with "@euro" have been fixed. Rename new nodes Problems with the display of the rename edit box when creating new nodes in a long, scrolled tree view have been fixed. August 29, 2005 - Release 0.13.0 New Features: Translation setup The TreeLine user interface and documentation have been internationalized. Currently, translations are available for French and German. Volunteers to translate into additional languages are welcome. Internationalize number formats Number formats can be internationalized. Periods and commas are supported as radix characters, and commas, periods or spaces can be set as thousands separators. Print unit option A new general option can set printing units to either inches or centimeters. External editor An external editor can be invoked from a Data Editor text field context menu. The EDITOR environment variable can be used to specify the editor to start, or, if the variable doesn't exist, TreeLine will prompt for an executable to set as the default. Updates: Number editing The general option for a number editing format has been removed. Instead, the output format is displayed in the editor. Of course, any new entry with a reasonable format is correctly interpreted (but the correct radix character must be used). Spaces in link names Handling of spaces in filenames for the linking fields have been improved. Paths with spaces selected in the file browser are properly quoted (Windows) or escaped (Linux). Output view scrolling Page Up/Down commands in the Data Output view now leave a one-line overlap. Interrupt ignore A Ctrl-C interrupt in a terminal is now ignored. Bug Fixes: Bookmark separators A dummy field has been added to imported bookmark separator formats to avoid configuration problems. XSLT paths Paths in links to XSLT files are now properly handled when not in the same directory as the XML file. Renamed fields Format text is now immediately updated when renaming fields. Undo Problems with undo after complex data changes have been fixed. Control characters Errors due to having ASCII control characters in TreeLine text have been fixed. Spell check Problems with spell check of unicode characters have been fixed. May 4, 2005 - Release 0.12.0 New Features: AutoChoice field type A new AutoChoice field type provides pull-down selection of previously used entries. Any new entries are automatically added to the pull-down. Executable field type A new ExecuteLink field type runs an external program when its link is clicked. The command to run is given by the text of the field. Or the field's prefix may contain the program name, so that the field's text is used for arguments or file names. There is also a general option to disable these links when security is a concern. Path browsing A button that brings up a file browsing dialog has been added to the editor for Path, ExecuteLink and Picture field types. Alternate text for link fields Link fields can display alternate text in place of the target URL. This is done by specifying an alternate text field in the Advanced Field Format dialog. Embedded child fields in output Fields from child nodes can be embedded in their parent's output. Select a child reference from the Other Fields dialog. The child data is delimited with a separator string defined as a file option. No line break file option A new file option allows the removal of line breaks from each output line. This allows other HTML tags to be used to separate the output lines. New contains filter rule A "contains" rule to check for substrings has been added to the rules for node filtering and selective type changes. Open last-used right view The right-hand view that was previously used is now selected automatically on startup. This can be disabled using the general restore window geometry option. Updates: PNG icons Toolbar and tree icons are loaded from individual PNG files instead of from an XPM file. This improves icon quality. Advanced dialogs Some features from the dialogs for data types and fields have been moved to advanced dialogs for simplicity. Non-ascii letter search Unicode characters are accepted for first-letter tree searches. Removed eval() calls All calls to the eval() function have been replaced for improved security. Thanks to Roland Meister for providing patches. Export HTML ReadMe file The HTML version of the ReadMe file is now exported from the TreeLine version. Allow spaces in command line Spaces are permitted in Linux command line arguments. Allow Qt command line options Qt command line options are supported. Plugin improvements There have been several minor improvements to the plugin module interface. Bug Fixes: Delete last child A serious problem with deleting the last child under the root item has been fixed. Incremental search Some keyboard shortcuts for incremental searching have been fixed. Saved tree state Open nodes are restored more consistently when opening files. Font issue A font issue on early versions of Qt3 has been fixed. No dtd file search The search for dtd files linked from XML files has been disabled to eliminate errors. Long line editor visibility Visibility problems with long unbroken text lines in data editors have been fixed in Qt3. February 16, 2005 - Release 0.11.1 Bug Fixes: Sip compatibility Fixed a compatibility issue in the toolbar code that would prevent TreeLine from starting when PyQt was built against the newest versions of the sip library (versions 4.2rc1 and 4.2rc2). Combination field types Problems with editing the data of combination field types have been fixed. Plugin interface A bug in the plugin interface for setting field formats has been fixed. February 4, 2005 - Release 0.11.0 New Features: Tree view icons Icons have been added to the tree view. The icon assigned to each data type can be changed from the Configure Data Types dialog box. There are several generic icons available within TreeLine, and plugin modules can be written to add additional ones. A general option to disable the tree icons has also been added. Copy formats A command has been added to the data menu to copy type formatting from another TreeLine file. All types from the chosen file are copied. Any types in the current file with a matching name are overwritten, but types with unique names are retained. XML import and export Functions to import and export generic XML files have been added. These routines do not have much intelligence - each XML element becomes a node and each XML attribute becomes a field. This lets TreeLine function as a crude XML editor. Data editor height An editor height parameter has been added to text fields. This allows the number of lines in the data editor to be set for each field in each data type. As a result, the LongText field type becomes redundant and has been removed. Required field parameter A field parameter has been added for fields that are required to be filled. Fields with this parameter set are marked with an asterisk in the data editor view. Hidden field parameter Another field parameter has been added to allow certain fields to be hidden in the data editor view. Updates: Code clean-up A fair amount of TreeLine code has been rewritten to improve efficiency and to ease future changes. Users should see improved speed with large files, especially noticeable when repainting during editing. Python version TreeLine now requires Python Version 2.3 or higher on Linux systems. The previous version of TreeLine will remain available for those stuck with older versions of Python. Hide HTML tags in titles HTML tags are now stripped from node titles in the left-hand tree view if the fields are set to output HTML rich text. Plugin Extension Interface The plugin extension module interface has been expanded. Several added functions deal with node and field formatting. There is also a new callback trigger called whenever a node's data is modified. User plugin directory A user plugin directory can now be specified in the configuration file. This allows plugins to be installed without root/administrator access. Linux installer An option has been added to the Linux installer to allow a different documentation directory to be specified. Bug Fixes: Drag-and-drop Fixed a drag-and-drop bug that wouldn't allow nodes to be moved instead of copied in the Linux version. Special field name characters Fixed the output of fields that include dashes and periods in their names. Edit view font setting A problem retaining the font setting for edit views has been fixed. Startup failure A rare startup failure due to problems with the encryption engine has been fixed. Encodings A rare problem with foreign language encodings has been fixed. November 8, 2004 - Release 0.10.2 Bug Fixes: Node creation Fixed a major bug that could prevent new nodes that contain date or time fields from being created. October 28, 2004 - Release 0.10.1 Bug Fixes: Recent files Fixed a major bug that caused TreeLine to hang when attempting to open a recent file that no longer exists. This bug also caused startup failures if automatic opening was enabled with nonexistent files. Plugin interface function A bug in the plugin interface's getRootNode function was fixed. File encryption Fixed a problem with file encryption on 64-bit systems. October 15, 2004 - Release 0.10.0 New Features: Reference fields One of the fields in a data type can be tagged as a reference field in the field-type dialog box. It defaults to the first field. This field is now used as the reference for the arrange and flatten by reference commands. It is also used by the new internal link feature, described below. Internal link fields There is a new internal link field type. It creates a clickable link in the Data Output window that selects the next node with matching text in its reference field. If link or reference fields contain multiple lines, each line is treated as a separate link or reference, respectively. If desired, the same field can be used as both the link and reference fields, so that clicking on a keyword in one node's field selects the next node that also has that keyword. If exported to HTML, the internal links function as links to page anchors, but, in cases with duplicate references, they only find the uppermost reference on the page instead of the next one. File encryption File encryption has been added as an option to password protect TreeLine files. Individual files can be set to encrypted mode from either "Tools-&gt;File Options" or from the save-as dialog. There is also a general option to set the default for new files. The encryption uses the SHA hash function as a stream cipher - it should be fairly secure. Initial field values A default initial field value can now be specified for a field type. Any new nodes get this value for the given data field when they are created. Also, in a date or time field, an initial value of "Now" can be used to get a time-stamp of node creation. Plugins An interface to plugin extension modules has been added to TreeLine. This allows extensions to be written by various coders that provide functionality needed by a few users without adding bloat to the main application. Currently available plugins are listed on the "Downloads" page of the TreeLine web site. The plugins are installed by copying their Python file (*.py) to the plugins directory ("&lt;prefix&gt;/lib/treeline/plugins/" on Linux or "TreeLine\lib\plugins\" on Windows). A list of loaded plugins can be found using the "Help-&gt;About Plugins" command. Information about writing plugins can be found in the "plugininterface.py" file in the TreeLine source code. Tree and editor fonts Options have been added under "Tools-&gt;General Options" to set the fonts used for the tree view and for the right-pane editor views. Page breaks When printing, TreeLine will now avoid breaking pages between a parent and its first child. This behavior can be disabled by changing the "Keep first child with parent" print option. Updates: Unicode Unicode text handling has been improved. Unicode can now be used in TreeLine file names, node data type names and field names. Also, imported and exported text files now use the proper encoding based on the system's locale settings. Internal link sample A new sample TreeLine file with internal link fields has been added to the documentation. Bug Fixes: File-info field dialog An occasional problem bringing up the field-type dialog for file-info fields was fixed. File-info data type Avoid showing an oddly-named internal file-info data type in the dialogs after the file-info formatting is changed. View states The option to restore the view state when opening recently used files now works more consistently. September 16, 2004 - Release 0.9.1 Updates: Unicode update Unicode text is now supported without requiring modifications of the Python sitecustomize file. The sitecustomize change has been eliminated from the installer. Open file filter The compressed ".trl.gz" extension has been added to the file open filter. Mailing list Information about the new TreeLine mailing list has been added to the documentation. Bug Fixes: XSLT export A bug that prevented XSLT file export under Linux has been fixed. XBEL export Special characters in bookmark URL's are now properly escaped during export to XBEL format. September 9, 2004 - New Mailing List List Information A mailing list has been created for users to discuss anything and everything about TreeLine. This is the place for development discussions (from roadmaps to feature suggestions to beta testing), release announcements, bug reports, and general user discussions (from new uses to tips &amp; tricks to configuration samples).<br /><br /> To subscribe, go to &lt;http://bellz.org/mailman/listinfo/treeline&gt;. I expect this to be a low-volume mailing list. September 1, 2004 - Release 0.9.0 New Features: Bookmark import/export Import and export of bookmarks in both the Mozilla HTML format (Mozilla, Firefox and Netscape browsers) and the XBEL format (Konqueror, Galeon and Elinks browsers) have been added. Sub-tree export An new option exports a portion of a tree to another TreeLine file. Command line import/export Command line options have been added to allow non-interactive file importing and exporting. This allows automated runs to be scheduled. For more details, run "treeline -h" from the command line. If using a windows binary, output is supressed, so it must be directed to a log file ("treeline -h &gt; log.txt"). File compression Options have been added to work with compressed TreeLine files. Individual files can be set to compressed mode from either "Tools-&gt;File Options" or from the save-as dialog. There is also a general option to set the default for new files. Thanks to Mathieu Girard for contributing code. Ancestor field references A field reference to show data from any ancestor node has been added. The closest one with a matching field is used. File info fields Fields containing file meta-data have been added. These include file name, path, size, and modified time. Headers and footers Headers and footers for printing have been added. They can contain the file meta-data mentioned above. The headers and footers can also be shown in exported HTML files. Character formatting HTML character formatting tags can be automatically added to text using a new data editor context menu. Available tags include bold, italics, underline, size and color. Note that the fields must be set to display HTML content for this to be effective. HTML content option A new general option makes new fields default to HTML content. This allows for easier display of HTML formatting. This option is not enabled by default, since it does not preserve carriage returns and does not show non-escaped "&lt;", "&gt;" and "&amp;" symbols. Saving tree states When opening a recently used file, TreeLine will now restore the states of open and selected nodes. If desired, this feature can be disabled with a new general option. Thanks to Jan Hustak for contributing this code. Toolbar split The toolbar has been split into two separate bars (general commands and node commands) that can be shown and moved independently. Again, thanks to Jan Hustak for contributing this code. Auto-save A new auto-save feature stores unsaved files with a "~" appended to the name. The backup files are automatically removed when the file is saved or TreeLine exits cleanly. The auto-save time interval is set in the general options. Scroll key bindings New key bindings, set to Shift+Page-Up and Shift+Page-Down by default, scroll the current right-hand child view. XSL style sheet The XSL export command now prompts for the name of an optional style sheet (css). This name is stored in the TreeLine file as the default for future exports. Updates: Import/export improvements There have been some general improvements to the handling of imported and exported files. Undo/redo File option settings can now be restored with undo/redo commands. Documentation The README documentation files have been revised to be more organized and readable. Sample files More sample files are provided. See the "sample_*.trl" files in the "doc" directory of the installation. Linux installer option The Linux install script now includes an option for a temporary build root directory. Bug Fixes: Up key binding The up-arrow key binding did not work as expected in a deeply nested tree. Combo boxes Some problems with auto-completion on data editor combo boxes have been fixed. Spin boxes Problems in windows with some option spin boxes accepting new values have been fixed. Python compatibility Fixed a compatibility issue with older versions of Python. June 23, 2004 - Release 0.8.1 Bug Fixes: Node renaming crash Fixed crashes that sometimes occurred when switching focus or views after renaming a new or existing node. Compatibility with older versions of Qt Fixed a compatibility issue with older versions of Qt (3.0.x). June 3, 2004 - Release 0.8.0 New Features: Undo/redo Added undo and redo commands and an option for the number of undo levels that are stored. Parent field display Added the ability to show parent and grandparent data within a node's formatted output. Updates: Node type menu When changing node types with the menu, check marks now show the current type(s). Bug Fixes: Node renaming Fixed crashes caused by right-clicking in the edit area or using the Alt-Tab key combination when renaming a node title. Data type names Fixed problems occurring when several similar data type names were defined. Installation script A rare problem with the installation script choking on image thumbnail directories was fixed. May 18, 2004 - Release 0.7.3 New Features: Spell check Spell checking of the tree data has been implemented. This feature requires an external program, either aspell or ispell (see the System Requirements section). Updates: Print preview size The size and position of the print preview window are now saved. Export open text Added an option for including only open descendants when exporting tabbed title text. April 1, 2004 - Release 0.7.2 Updates: Keyboard navigation For faster keyboard navigation, the left arrow key now closes the selected node's parent if the selected node has no children or is already closed. Bug Fixes: Drag-and-drop Some tree item drag-and-drop bugs have been fixed. Dialog closing Problems when some dialogs were closed using the escape key have been fixed. Install script Fixed some rare Linux install script problems with some systems. March 9, 2004 - Release 0.7.1 Updates: File open default The open file dialog now uses the directory of the most recently opened file as its initial directory. Right-hand view To improve performance, some unnecessary refreshes of the right-hand view were eliminated. Bug Fixes: Search and change type commands Fixed problems with repeated uses of the search dialog command and the command to set the types of descendants. Data editor view Fixed problems with the data editor view in the mode that does not show children. March 2, 2004 - Release 0.7.0 New Features: Prefix and suffix tags Added prefix and suffix tag formatting for groups of siblings. These tags allow output to be formatted in tables or with bullets. See the "sample_table.trl" file for an example of table formatting. Save window geometry The size and position of the window and its splitters are now saved at exit. A new option will disable this feature if desired. Treepad import filter An import filter has been added to open Treepad files (text nodes only). Windows installer An install program has been added for windows. Bug Fixes: Linux install script Fixed Linux install script problems with certain versions of Python. Export end-of line Fixed some text exports where improper end-of-line characters were used on windows. November 19, 2003 - Release 0.6.2 Updates: Wait cursor Cursor changes were added to indicate time-consuming operations. Bug Fixes: Tree scrolling Eliminated inadvertent scrolling of the tree view when editing in the right-hand view. Install script Fixed problems encountered when the Linux install script attempted to delete old TreeLine directories under certain versions of Python. November 7, 2003 - Release 0.6.1 New Features: Incremental search Incremental searching has been improved by adding keyboard commands to repeat the search forward (F3 by default) and backward (Shift-F3 by default). Sorting The sorting commands now contain options for reverse sorting. Text imports The importing of text files has been improved. Imports of plain text were added, with items for each line or each paragraph. A prompt for the type of text file was also added. Title rename option A new option setting avoids going into title renaming mode when adding a new node. This also keeps the selection unchanged as nodes are added. Drag files Dragging files to the TreeLine window in order to open them is now supported. Updates: Context menu rename The rename command was added to the tree context menu. Right view scrolling The right view now scrolls to the top when the item selected in the tree view changes. Initial configure type When opening the configure types dialog, the current tree item's type is initially selected. ReadMe file A TreeLine formatted version of the ReadMe file has been added. Linux install An install script was added for Linux and Unix systems. Windows build The windows build now uses Python version 2.3 and PyQt version 3.8. Bug Fixes: Rename issues Keyboard commands are disabled during tree rename operations to avoid unpredictable results. View updates The right views now update properly when the parent of the selected tree item is closed. Multiple copies Problems involving copying with multiple nodes selected were fixed. Drag-and-drop Problems with drag-and-drop on some Linux systems were fixed. September 16, 2003 - Release 0.6.0 New Features: Split views The three right views are now divided into upper sections for the selection's data and lower sections for the data from the selection's children. View commands can be used to hide the display of children if desired. Customized shortcut keys Keyboard shortcuts can be customized by editing the TreeLine configuration file ("~/.treeline" on Linux, "treeline.ini" on windows). Any configuration files from previous versions should be deleted when installing TreeLine 0.6.0. Easily set type A new command will quickly set a node's data type. Automatic open on search Parent nodes will automatically open and close when found with the two search methods, by typing the first letter and with the "next sibling" keyboard command. This behavior can be disabled with a new general option. First letter navigation Typing a lowercase letter will move forward through the visible tree items that start with that letter. An uppercase letter will move backward. Incremental search Tree items (including those not visible) can be incrementally searched by typing "/" followed by the search string. Modeless find dialog The Find dialog box is now modeless (the program may be operated while the dialog is open). New choice type A "Choice" field type has been added that allows selection from a user-defined list of strings. New combination type A "Combination" field type has been added that allows selection of several items from a user-defined list of strings. Linking types Several linking field types have been added, including "URL", "Path", "Email", and "Picture". Clicking on a "URL" or "Path" in the output window will open the link in an external browser. The "Picture" fields are shown on in the output window for certain image formats. Pull-down editors Pull-down editors have been added for the "Choice", "Combination", "Date", "Time", and "Boolean" field types. Output font selection A font selection for the data output view has been added to the general options. New printing options New printing options include setting the print font and setting a default page size. Updates: Save unused configurations Configuration information is properly saved for unused node data types. More keyboard friendly TreeLine is now more keyboard-friendly in many ways. Arrange window captions Window captions now put the file name first. Bug Fixes: Data edit resizing Fixed problems with data edit view resizing (on Qt3 only). HTML in prefix/suffix Fixed file corruption problems caused by the use of HTML characters in the field format's prefix and suffix areas. March 14, 2003 - Release 0.5.0 New Features: Default child types When configuring node data types, a default type for that type's new children can be set. Multiple page HTML export An added form of HTML export creates a directory structure with a separate page for each parent node. Updates: New sibling shortcut A shortcut (Ctrl-B) has been added to the command to insert a sibling node. New icon files Icon files have been added to the distribution files. August 26, 2002 - Release 0.4.2 Bug Fixes: Fixed Python 2.1 issue Fixed another (oops) incompatibility problem with Python 2.1.x. This change is also not needed when using Python 2.2 or higher. August 23, 2002 - Release 0.4.1 Bug Fixes: Fixed Python 2.1 issue Fixed an incompatibility problem with Python versions 2.1.x. This change is not needed when using Python 2.2 or higher. August 22, 2002 - Release 0.4.0 New Features: Field types Data fields can be formatted as specific types, including short and long text, numbers, dates, times, and boolean values. HTML or plain text fields Individual fields can be specified as plain text or HTML. Line breaks are preserved in plain text mode. Field prefixes & suffixes Text prefixes and suffixes can be assigned to fields. The extra text does not display for blank fields. Spaces between nodes Options for spaces between nodes and allowing HTML in formats are saved with the file instead of being global options. Truncated node titles Node titles are truncated after the first line of text data. Conditional type setting Node types can be set conditionally. Variable text length A new option allows the number of lines in a long text edit field to be set. Update missing fields Missing fields can be updated from a separate file by matching unique values of the first data field. Bug Fixes: Title rename Clicking on an unselected current item no longer initiates title renaming. Paste fix Rare problems with opening or pasting content with complex text strings were fixed. May 28, 2002 - Release 0.3.4a Bug Fixes: Windows library issue A fix of the Windows binary only. Fixes major problems by upgrading the library version to PyQt 3.2.4. May 15, 2002 - Release 0.3.4 Updates: Ported to Qt 3 TreeLine has been ported to Qt 3.x. It now works with both Qt 2.x and 3.x using the same source code. Binaries updated The binaries for windows have been updated to Python 2.2 and PyQt 3.2 (but are still using Qt 2.3 Non-commercial). Bug Fixes: Selection bug A selection bug that caused problems with the up/down and indent/unindent commands has been fixed. Preference fix Errors in the initial user preference file were fixed. March 6, 2002 - Release 0.3.3 New Features: Multiple selection Multiple nodes in the tree can now be selected using the shift and control keys - most commands will operate on multiple nodes. Copy node text A new command will copy node titles as text, not just as XML. Print and export open Options have been added to print and export only nodes which are open in the tree. Numbering start A new option allows the numbering command to start with a number greater than one. XSLT reference tool A tool to remove the XSLT reference from a file has been added. Updates: Arrange by reference The command to arrange a tree by reference has been made more robust. Opening error handling Errors when opening files are now handled better. Bug Fixes: Export of unicode The export of Unicode characters to HTML has been fixed. Set types dialog A problem with the set data types dialog under Python 2.2 has been fixed. January 7, 2002 - Release 0.3.2 New Features: Unicode support Unicode is now supported in both data and format strings, allowing the entry of foreign text and symbols (note that there is an extra step in the Linux installation to enable this feature). Sort all nodes A new sort command allows all nodes of a given type to be sorted. Right view preference A preference setting now controls the initial state of the right-hand view. Bug Fixes: Tool-bar during rename Fixed crashes caused by tool-bar use while renaming a node. Deleting & renaming nodes Occasional problems with deleting or renaming fields have been fixed. Large edit views Fixed crashes resulting from very large data edit views. December 12, 2001 - Release 0.3.1 New Features: Print lines Parent/child lines can now be added to printouts for better tree visualization. Compress format An option was added to compress the format into a single line of output. Bug Fixes: Sort full branches Problems with sorting full branches were fixed. HTML indenting Fixed improper indentation on an HTML export file. November 27, 2001 - Release 0.3.0 New Features: Filtering Added filtering of descendant nodes. Numbering Added a data node numbering feature with both outline and section styles. Export XSLT Added a feature to export the format to an XSLT file. Help links Added table of contents links to the help file. Updates: Long strings Improved the handling and editing of long data strings. HTML tags Improved the handling of HTML tags in data formats. XML parser Improved XML parser error handling. October 23, 2001 - Release 0.2.1 Bug Fixes: New node types Fixed a major bug that caused errors when new node types were created. October 17, 2001 - Release 0.2.0 New Features: Rewrite TreeLine has been extensively rewritten to include database field information in each node. XML format The file format is now XML. Right-hand views Added additional views in the right-hand pane. Formatting Added options for data formatting and manipulation. Import & export options Added more file import and export options, including HTML export. Questions, Comments, Criticisms? contact Mailing list There is a mailing list for users to discuss anything and everything about TreeLine. This is the place for development discussions (from roadmaps to feature suggestions to beta testing), release announcements, bug reports, and general user discussions (from new uses to tips &amp; tricks to configuration samples).<br /><br /> To subscribe, go to <a href="https://lists.berlios.de/mailman/listinfo/treeline-users"> lists.berlios.de/mailman/listinfo/treeline-users</a>. I expect this to be a low-volume mailing list. Email If you do not wish to subscribe to the mailing list, I can be contacted by email at: doug101 AT bellz DOT org Updates I welcome any feedback, including reports of any bugs you find. Also, you can periodically check back to <a href="http://treeline.bellz.org">treeline.bellz.org</a> for any updates.
TreeLine/doc/sample_bookmarks.trl0000644000175000017500000000534211651514477016103 0ustar dougdoug Bookmarks Linux Debian debian.org http://www.debian.org/ Debian Planet http://www.debianplanet.org/ Links Linux Doc Project http://metalab.unc.edu/mdw/index.html Linux-tips.net http://www.linux-tips.net/index.php MAN Pages http://linux.ctyme.com/ News Linux Today http://linuxtoday.com/ LinuxPlanet http://www.linuxplanet.com/linuxplanet/ NewsForge http://www.newsforge.com/ PCLinuxOnline http://www.pclinuxonline.com/ Searches BigBook http://www.bigbook.com/ Google http://www.google.com/ Usenet http://groups.google.com/ Open Directory http://www.dmoz.org/ Switchboard http://www.switchboard.com/ Yahoo! http://www.yahoo.com/ Weather AviationWeather.com http://www.aviationweather.com/ NWS http://www.nws.noaa.gov/data.html WeatherNet http://cirrus.sprl.umich.edu/wxnet/ Weather Underground http://www.wunderground.com/cgi-bin/findweather/getForecast?query=gai TreeLine/doc/doc_condition.png0000644000175000017500000001214111651514477015343 0ustar dougdougPNG  IHDR<\PLTE #$"$$'()''+-&-3-/,&0;*17)4?'5E)8G564279-8C,;J0:F9:8.=M6=D8=?3>I1?O>?=3BR7BM=AC?A>ACA6EU:EP=EKDFC9HX=HSCHJFHEL]9O^BMXHLN:P_JLI>OZ?N^AO`AS^LPRNPMCRb>UdESdAWfEWaGVfBYhQUXDZiTVTHZeMXdE[kJYiLZ`QX_G]lWYVUZ\H^nE_tL^iY[XR]iJ`oO^o[]ZKaqY^`HcwPbl]_\McsRarNdtLfo\acWbn_a^OeuKf{UduSfpNi}Rhx`egZgmcebVitTjz]htQlghfYlwdikVm}XoalxYpjliVqaouimpWumolYt^tbtgretz\wWznsuqsplt{^x\{cyfylw_}rwypwvwuwyvm|ak~cw|~z|yr}u}|~{fn|~wuizrnzw}{ħƵþ¾ÿחnbKGDH pHYs  tIME ,3)vIDATx\W麥c[{*|X V(/آXZ/`+"*'U[im] =-Fd"4FB"!&7QwL $8_?<3|߼!3Їe/ 1O}XW ~>o^xŋ7ozǓ(82r)+qZg#Jm۶[^}ׇ;H~p~ɯqy/>xwO??f J*'<7"_I Cfp֭[ޖëi;NWw]oy{GD+QO{Wg27@!'P|BR\>&9w.Zv ;.lܸKt \۶ۯĹwv6?@uHyLowts/n .Sq(ah蘘 19w^ZBrX҆VۗፔDS^~zͪ&іӢ1ɪ5E>\} |[8/F x6.W/x.p{ѣ ~9wŝݾo>.HXlS{#qEIνx-B%r?2@$(U 5&zCHihdGh鳯}d|M.ls (j ~Eoj1Kt ^ l>pchQDġϏLEFK" PsA*OP /))I$O}~oMC)r:<0H4QcO1?_g~lE1՛FwNhQxeIxtOjQxx0)""#Ͼ:I(R]]ptGMk֞i"FTr>W^oZ@UIʴAV?1 xt׻(jb_v>$Z{VT M -߻(rHԱgQrSG}&Iu8|FHDtDD|$IEryuBund\4#'K&y9Txv n |Y$\j*'=Al}5-`Ӊ Nn}B4IrW{ lq= ~"ʺ'p'(o~%..u!IvT)PYŒHNԛc vc3Js}#*MT?{DiO܅f;#_'&Dh0f|_Ɨe|9;}(| ׈ _\R b_J&[/DjE`q`}i [ħ4NN_lf*u:`cSKҁ+ ծ{QJg'FM]fSef5%剉0/2/?_(6<"2_Bwm8qT} ugJ]ڿ҃kU }nt؏o[~r׬24ݢ7_cp[]4*o#r_] ȗʏߛGVay% BaMF "O?@ތ?.%գK{|6f #l4|BBaI煄m&`groNHH]%K/~0 *MAm"nV^|IyT / aTdw<>:ѧG˗;*-8EVgpV6hW@ZAPL-Z}[,}m1y@[JVcPk︅T^ы} vL* VwkaZo?~[O*ߩ ?Kt7Ӄwsgo߉,Ҩs 92ۦ ^4m3}E:\\_2;]B}鮵ʷnw\0cķNBs+A;e5CfV1&gЎ#ð"l~ъFcVNun`tA; ֍ %Ӂ  6_ Y`%W'έ/mX!^/)gFA`$n i Xs+AkE 2_Ä/ZݵՉվsZgUYA24PrKD=VSZy#x} vd :^]SľvUs~pQ>< Qh4"AoCk!XamlLoLTlu1 K|ES>zg+Gsҩ8XϮ?Ɯf|_Ɨe|_Ɨ V_M,2h K tdRzPyvM׳Hʏoo~w׈s #у΁ mq]s·M*?W (Hæ;6iəz> U ADĪNkB\\81҂ʻ~O=Ww^EQ6#eqBqzbWIQ5t޼Ǖs>, ـATG Ӣ.?W;+Z2䓚aJ!OOYʅ  |Kc>l1WL~ҐȨw\\~'_q2'K}Uؼ4O4,1ZAit3YSe]O~fGW<|2sߠU~\aXLfAl͇]6ߌ~o/q3H5@쮍4Pyg\g_鸿)y>*W1 [wך紷҃ʋ/#yƑ9WE Ɨe|xVc[mԵ1c@nOt^HOlz)dTCW Iψ.z) tLK/鋕CqL6+vMb@0adɛݤ/͔  hu3&_SIsL9/͔{= (Iׯ)/!ruauKs%}o}||qGܴ1$gθu;pTgK*| M/;n.kZ)saTOz̗/5fr9 Ʋ01'olX5ŷbHѥA,7pcF1L6 bƴL}T KV +/9@E;jC; _ksIGg~v%}=S5,cc/VU6daYf!z͗/p^j*Pn E4Ѽ}ڑlj\ؐ9P!F_N1zEJ+8hVq^f:T͸īlaT(Z\VR\WîXFmN_3L6TZ"{r J׃{ʜػ] 8!GY7B,$jR A&vOjNceV"a~if!lU!k@4>שBJ`}Y7i,Q/#H!w,U_XN_ċ"T2`u0kXaqF l xVo_1)8P[ Mq߅Oe-+Ca0 SF Books Greg Bear www.gregbear.com Darwin's Radio false 2000/10/01 4 Evolution caused by virus begining again Blood Music 1985 true 1998/07/01 2 Smart viruses take over Orson Scott Card www.hatrack.com Pastwatch, The Redemption of Christopher Columbus 1996 Yes 1998/09/01 4 Time travel to change history; discovery of America Enchantment 1999 Yes 2000/08/01 5 Boy travels back to Russian fairy tale Ender's Shadow 1999 Yes 2001/05/01 5 Ender's Game from Bean's perspective TreeLine/doc/sample_basic_contacts.trl0000644000175000017500000000467111651514477017076 0ustar dougdoug Main Friends Bill Smith 1492 Columbus Drive Nina SC 35762 (703) 555-5647 2004/11/30 bill@pinta.com John Johnson 1941 Pearl Street Harbor City HI 86741 (401) 555-8923 (506) 555-7413 (703) 555-2873 1905/05/08 jj@battleship.org Family Jane Doe 101 Dalmation Way Dogbone SD 52782 (654) 555-8527 1901/07/30 jdoe@spots.net John Doe 20000 Leagues Street Undersea NY 12763 (805) 555-7296 (777) 555-9999 (503) 555-8234 1999/08/14 johndoe@subs.com Work Dil Bert (501) 555-5612 Pointy-haired Boss (666) 555-8945 TreeLine/doc/doc_edit.png0000644000175000017500000006270211651514477014312 0ustar dougdougPNG  IHDRwiPLTE X R 2 F!&(V B&(%0-$)+().0!!&<".58):-.4@/94574T$L2:@37N4<=/1;;B44|2?P;?B7AM8CC:BI=G(?B?9H=3L9ECG>JL5IjFHEEIKCJQ>L]?@;Lg?MX@NdVV9[@DRVIPVHUD2c7FVJGUO@[HPZ"4f9lCbRTQoDZFUpDZiKY^MPWUYQX]GQFZrI[fFfGD_s@kFFgPI`{RafX_eOgT\_\T_wPcy\fLTctPfpLg{Xg\gbgZhn`glUjzQkegdUZ\aPyVot#mfw`fWqkmjapvVz_bkhnskqh[t_xfora`|es\wgv|fvouzsur`}`dQR_l{pn~omtep{}zy~cohxyww|w}5w~jq~hsqtƟbw¢GyɨϢİ̴ƭJęѲϿڿ|ε¿ŹŷƳɺŽaъھh0bKGDH pHYs  tIME .IӘ IDATx|ՙM.%NMU($,mUV2BACNdw+]g]k7`C wmn]2"RmHUU;Kse3Ir'zbssy~YwU֞ʙ[r']2U>eZ}vvP[hB\mC7c=Pb^wn##eėKGT{/m]Cf\}s>rfA^hog޽{?}/2ؾxb @۟53>[ʹϲOOm)~/Amc1UWE`-QL{;P {τB':"SyԙIv>p"v|`ccYޱ?9s)(/+jblX1UWVvw m|q;C;|Oe:>?>w1=XÇǦ޽O? =lY\H{wwXߓO>?;=lr}L?~flߗǏ?X!f|{o̷;'!qݻO|w@;O>O½^sc/DǓ>Ə~Z=Wr~}a |PǾcv}gG?| cO<9']rUE2K~;nvMwtC7Euz^ql޽]7ڱ~t|Q#>ܮ]lywq;~w7~~/?6>n뮻>9>r\ 5bnq5׻ݦcn]q_?Rw.fޕ7m#۵{7{[}v1-s]`JE6W&`=(B3@ oױ$mbC[ko_W|ͻՎ}Q/D^U@W`&?_Ͽ\EfBִp muc_v yv=l_`pS֚]pnk r֚թ֭֩NnOUt:Yr> rWp˿&|J_-wRνfgLM7 #ҖۻՀIK*8$p[>2ԐaVCepdV ؊Ցd>V-ӵ!Y#鶉N9n"qw[* iX2 {]rřH{iH#I#n0lMԤ҆N@Q[ȭ#CZ Ш;삟}k[[ks뀽ïg,rjsk UQW<:9-dM# c נ_㑾Xգdds޾QEe.7. pR5$ZR;}NWiև^ِxjFPm՛z 8bϾ/Y\SMT'ɟtZ7mq_UBHܭS#]-TG[pkkg@OcQHu4=*FR󱾾rj`d;ZXiix ңR)9vtt"=2.MPa.ˑxzw Ku}3Hd*X,cTd(TgϜImqW *Mg-.{d1T#)ۀTVvf? )o{]goO™3'w޻K g At:wşe*)_(2\3{ϟ<<F"H(U@5l*u(C+X\>j+u}RN#?P©wϿ{t8]@}Z@v7Gs;g __tɓej\;%Igvns:s͇%TLE2D0Hd؏LEʑb&c<#}W܏J3Ԧc yD߾ޥKoc6eH3}w'޾t-p% KSj'Q@ЙPꥋg޽"Bo~ ksr<{Y$~KO*x<-ɓo? /W㍍M?L9}x7o|/bqyo$G:W#9ΡBw}X!U|'r/P?UL5PU7otoa|I <4x.G,PD5 ӗ.^0}6.GIbOʾ5/wLB\߽t|*z晷~キ:3oU æt1> ZKyq<ճ'z[.Zc;cCo̽/|ghI}dS/>8rHzGT(g}c ]RWo`J~sɤ?r-zSϽ3R cxgTɓ/ߞz}hdbb"uM>?'G2Tk [kk/ƮtpcKˠknzzSU4ZW?MO^z _ݼqgvz…_?ޕ7 C9?~U/{D<~QMg^{.\TSs F巊*\0Z:o_~{ϧg՘y!韥߼ZWÿ~ |}m*~#'O=[.j=uuTkbWrs< {?ϲ=Oɓ/|jv&nCۯWИWC<~??۳gf_; (2~u8ىH֯NOU;cӅ=BI>|OWUd~6ERo7y R!ֿ}:TWSO=>0\W{|d(LA=؛.ja#6O;͇;/'F7?}o<)>s]*tOs ïjhՂv. ߹XD5 I@U?EiOOLO= &\[ Rp_tVk,͜ƚ-@Fk'fޯ>?Syhoooί###MRqJxy|jo&-mZa`^z` {Y'zw9}\X{s_p"]鯲k2]}ҙR2|\[p"I!y&/[7tYmH8 b}ӉD$;NT;wX3 cK?9KfQ~fg[:| ^~T8kHmI8p,9pbq5^L/)SG!B+K7}N/%/jZSgUk";ȎUgtXzҚ[ JР5>\34]}@S@,N"U wR9sX#0 U#mMh!Ur[ʢC z_*lDK4csn阨CgIocZ{ꊢ:3툛#;uS-~7QS]T''WblN:cET DeNuq~ᚫ?@d:ՕE`w隿ݷzgJk$K(XAvzkCe; FWUWh0KTo J$I; Xe9@hH%|@DaaR"@<Y"+Hxn~5!K{nnPY4Cz0kdaƈMp4m/+۪zu(#!:6 zݧPmyHUČgQ߫ItP~^ao̪疊4*]]SubU Jl~G~\VΑ#`@µb|68J( cML3徟nDT1HS%:lsd2LH@kD CZ4Zj0HSDlXڠסyfHD1|ļ"〷aHF ?Yv'y9WҡV-lk{6HuG)X[W2ő鋩U `Sf{#}c|vjBXטsH?|{nlVnՐ8̓f3 ƨCJ4h-6 EOVx: C:<s}ԛ.$"DZ>Qg "/fb;SIN֩.C2z7_i͌x4qUx1(8DYьXCWE?#©W]ՊNp ok-rE!-cKHu - o0Ձ+FjWK92P="+,} j4[h̋ F\Zռ_Ԧ'ws{|~a>ːlamL*wGռ_ IFAB(E @7 DfxvЏTSRTYC9;QlamP-G{>v[ 1&cԚih7 & _NUql&+t~{m"oԐ& koWh̀MQuпf5Y܄ ~ִj0(ꑄl45ܨu e^0`K44< F@6p͂<U6&'e)9ښp%Y'PaԎ9LTjG|bÐlȜ'+3Tq`MMӼvU5skRO?=ܳ{>chېTmQ\5h_%PP(Dm84TXikcDx}`+lyb`$ r2iEidΰNd3M-A b1pmYj*?![kͯWyw[[R+\{v3Tcm mlu[Z;òE4LE\2c?qbpmtVu3)"̐6^*M kͯ-~[0TQ Z4TW_-ϧ_S]~xO~!ޣWw~ڔNuWW3% IDAT:cZ,ZGqM"%z|QpR=:jү*:Ut ժo`+0l^J0 cN)ԩ8C~^nҫU"xH[bPX3cWՁV O(ѯ"_Y_Uu&T:ʇ@ҞjNѺV.7SP_S$FA[=f xC4V+"L"j麷y15~2lTk\<_]njꂩߝNJ.x'rPMKl ޾~"Ey+QmrG.յ\ƍ?1÷>-wlQ]+elbv+7~sK///W1յjVTqJKwtx/ qs;?x?TC0: McYp^gC;|hCׯ;e$#8UL 8 79@ͱznwgXfoT6`ЏP3߹D'v&RE YJ]#'Q6KDw6`N5x}Q ZlmMQH▷6[|ҕC{]{~U4&_lF}50c-élF"[֤=ڴÐktXP qzy#w>e}2磚j||>p$l%'SmLUWhR2UTjA-_ߨO\!0U|(6ÑΛZ;mg|>rQFT@_9yK%9i_җ~*U7"KG5W)_*kkC0sx,`-x҅DAE)[8((Ϛ@'NuyXo`DCѢ46~󠏠fYe<&~ l{VOU,?SEEkRr6ǭ:*ҮBܘvW0 әsS ɱ jː+=p!1q;r`u,A)hpV=LO/=d`<~۪%)9}.-Tɋ:_[?_VGYYvv'@VVOETsT:rNFu:_-??7}->w.}뫦:|5!M~-fTÐjwk" {T*{|¯B_T]Lj(R%9.#vx8߈u}St3^DN9Ẅ́'YM[ i75qp:To,4q87hZ]# pc33p/xl4ڮ=rw[;khfɅI :[5*wg&:f"kRzU͠^<ʍB&kB=Wf+RիT8Q5橒f5>QE2T+Z5~uPOϸ<[i*p?EMӌQt Eݡ dթ^^B;\2һ1huV?ŭSTYYX(~^}L5ā&հJ5gpƆ"[n߂L\TӉ Xn+QARA@'PT5[b()PVWD>pIU}QUs~xN>pXW稿jdԁaA3׳]b: vwl1AH4mbP7 XЧkTWR~I哞IFcFUu\'UvCp:0[u+د_U;hOftgX0"+jxNu1Tg7p?xٚgif&7uZx__Q}QզfGAa !+XT%WWK>ޓ:${xzq7V,%qɨCZ_ n~(|ؐn7)*V3 ٿdT >|4[% {Q# Xhš(tx3&bJKEl"#u+х_AW#zꞫAI0J֯XGmՇڪeGbaiE ]KH :,1|_z[&Zv Xp"V%(MTTcaiT".K1 ΊKeb Zb[|ULOzlrSMK "5:(['Vh+\\[o:&m[H/Dr)ʧgN3ieL49UQ`%<.0zȌp:_rL6 UƓ8Cg|(>LJ EcbKNT n5dDgڛB5Қl U]Ғ?$TaH[y쑤J}} QE2U6cycqt7h9D!"nŃɘΚQEHk¶I7LtuNZ# ա֘&"S$z% Nt2R2OeiՌR_9կy͟{3_UKa+X.hWd9i["*(zWWk@Ŗ:};cF[,6 ֺuYNݩ.zӫJ9<*UxH8JُDha*Z'u*5iIZz-79BQ]rQU3BG4chTWyoiWյ[J%J,CJ52pEuUZZd) TW7-~uҚp)Ch(=<:z<,֯~&[}vep{2ĖޮP8;8_&lByX- xA"*ǡla`;Fbx/d)j ćG.%6jr:Պj~ԯQuMܢξ6ϐx(TZ_-_@u&N #I9ҙǘT_`~U6*:Sfwe4hHuvT֯EUO%ERir1f]tETQl%";' \y;ںSxF[-_xN.uF8Ix<4W<@p8@[z#"%8EEWP$9IGufl UKldɐyܤ<ը_;_mmNFkD ɠeZx&Rݠm•7YŔ}LmVJ0Z8[FFPAOHa{0`Je KUM3kYF̵UhΊ6jmtlƛDaYtа?~8h\G=y4nñ~5կ~Y*UOGyT {`j8?e`<5*Fzs=p>Y.;$v՞*/ѯ28CIzcvB]fWYJM4l~ھ%vR}EW'PL*UxڅcKuWy -~8t:QZuRjU~VՂRdίfS@`axB(ԭ ?4WI5ZݯV3ׯ xQՊ6]k#fkyWWAg6*;hViS3gݠmh$_5M kjAlNlU8(nsl-V z_fl[mVb *pથǖT.կsϞU"lݏ~b5!*oY,UnB}A[J' ή_p U0qћ|62QȯZdS⊌^ѦWVgĖG߭¯*Uޔ[PWTժhuoETeEEV8VdX8<+@3bKJzhW7_]jՙ%Dp|o|Xm]jJwڦ:~ռWՙ%Dpp,TL'T$n K0vGK"9JO Wd[]vjrmo8G\p-沴q~n-Չņv'ZŐ>FWG,έvl񑾵MuNW_4#xn-Q[UHu•l7\v #N;nl]Mc$4\qwCk߯/ 8/q"g|/Ǔ-&\G#-Sư+,Ǎ>0&#pU? ˆ%۵W1a*8epq.ZE*-"NHdTf2eY^ xQ-z<=z DƖ2TvXRϵX"]96Vsz%r3-p_mu{nT*or(3ILipA%DhqmʽX-o=V bSNF?'"ecKlVUW3UHz%2{t@ˣ4T &1GUTZ|L jRL@?EƖTWUDUõV:L)Ak2[ZTՄzqxTNUij[M$TW<<`ڭg+pѮhu`Ʀ#} sGjV9MQO-㑲s z`~ e֯zRmknJ-"-UWgW=˦_-3cKuWdDKjyKdU:~ jS_07L>uX}Eՙ*kjiLl)_ݣW`f4"oR*shO_l3U,M4AD bx;8pŚ8DaY_[נW ¡~)EjU;fo@K:*kRՋ4*͂ΧT)}S0{B94qRl>&68Gw_ȠA~Fx#C9 jA>"YൻqF}?Y A@S8j"A'I8` 8QEźZRj P܋*fv_ůN5BjF21_Ab2.ez-:~ulEKFsQܬF|I-cKW. *U*+ky{:0tt{T;>4~U U]#-˘kOu:ǁǺNJٶf>E)v=Պ[j~VEa!Bk5s ,ռ_=^>Ts*t.XiY9^yWz\nkW=,o)괡- xh:;:B'Pj=ȶ*_-ǖ8p|UW~uH=<[lI`=_-[h뜙X8EŇ~uFn+AZf-qH:6K"XJN‘p8!ayG$:j ꌶW?tM~\6Z&ԉM؝qWKk{Pj kqnl]u:~t|6ԌH*|a?xμ?[maP)[ߑpblHmu J_nZ1U8/:nV? ITC|GoG=V"yZ&4SXk.Ma(Z0]ugѯO4zV)g6xzt6C#v(VVÄqʞ6JR 굒+-͡_`~`jH Q%=0aB hxjd-[]yUOqUY6& T}v)]YF+hQ1\jky|X~3]U[-֯ރ[UѬzRff|:ªVgNݤՆk5[Rs26¯?s(_=ud_yj2TWWկ:|,5+q@'!OT[CR v-:+a@4b 89|5Kr:$'fJUi_TS1*`Vz}U^޷T`$ZWEکoW˝沀C5QA[g-pڧ@+JzeSWWK*sÞk>T_] Oq]֊u WWKl4E*j5 ڙ 06hnܕڪzZDJ[Y[:3 "5d;%jgZ_~uS&7Sl46>Sh>hfiAp8Jg%ݯ^T~T+ꯂ(D'ԫ&=W}9 " )gsUJsSN͢_ŗ:Z3]>0x~xӖKZ3 zbdL]ޒRu8pAṡ*%%Đ-~6~Wsүޚӯ S7k3_-񵜟ݯQCU:m-%W}jU%l,P':0ӁWW8.ܯv3GW SEWfZLu8jрxe6ULf4+^:+:t@:V5zyjCo{4; -j.(ǐG0~д/bqP-{fӯ68DڴfV@bBCsZ-Fz Bo Ly߁V@Zg󫮉Q9;@!)*[LvN lĎ9J*U dp0 XeEf cx.(gV\husW=VB4(Ќz`n4\PбAxV:>D^y+:񫔶}%@khWY.stUh)QUG8AE3 jƈ"4^iO~5REl=~6F!Ree:|ŘмV @+dG񥨶6~55\>0q8_W=j1 8?X]8"Myz&W綦*gׯR4 UVzBh)VaS?h4yP[p6m z9Z_ K7,S՚+>h*}폯u[K˨__ NmMTWK# *ׯ&S#37CЖW%V_]9+-կf)Wb›rפUosӮzf.LJf6^G& P%fFv{ 9ǰ&~uTz1@FmB#vy=~s- Ha$ T9[TGr i z"ix`X  @2򁧌rysׄn[[e8DQ]1L. /(}6Boܑd.`/\4 S-,h!U'=֖Fs5ׯ~4* JT$r0-PRdMR,UUMI,YZe챍vojt6R~dU(V~J.)'a>WzR,p2')9E}6\H<ӯU<ĥTT}Gi+`.+s*]VAz"e#ǘ'M6*'_| ĒtyJ5D9t&VDVDUrӈhC@:v. ܱ]7\M7W׿ \1 RH tP,gdPm X'ұ:V9N& UNTabkZ UjOUU S"Y0ݩ)0*ሪP4atMrMFg fe2$zQtT*PA2ܳ5 @UHjՂ~5/jn73$˷SE"R5idRɵC2sJ&8p,-hP' OpXMɇ T 0R3B%o|%*< *rc-1 J{[ߧ_4[j7Z#1hZQTOajEj=F| uJdB@AJXN?H4+\ ۡ%&3 [Dѻ*A m[ǐj18142N036HuIە.;Y*[w)2rI+tُf>RYV(D$_r28'-+PT)T#n29SQUӉ'Q!JJrƠ"}CTM~1t'_c U'}{O9]%a˅tBiPS0$ c`2PZC3J%`L \B4tyKC57\)"dM'_>x E<\II,l\]aBAUjՔNrjCYB. cqC90үjdݬ1xA-<1ݒ>&UNpjk!]7^]ү~04c>:gIl”@Z* %G"E_iZ7s&CsUϷ~ U&Gx0} ) 4`r Y7o| fs! (%R IѼIuyթ~_%u1OMջQ~USS[(H]92 ; ASu`I@+-UWLV W+WwS:<ցϝWfr{T7^5e؝~k{uχi،~kYQkզq~Kx~`;Fjm4T?WW_u?s{SnE xk*3oϷ֯@ΧDuhb3/}^N<IjRICPYMZ.rDr!Rp#xRtLRHSǭp86{TKE:T%FМ 83s?faE* X}#zBJZ5QFWsWa`ru(s1Cj:{\M ); Bu6&2͸ƖG~~"5PLt 9VCCj1G,H3 Y s`R#$&D?ֿlr#i,Ćm Vy0ИDzM+FqWiS8p-7`Я?A4Zؔ+fDD -}nN6_ݮOȮ{ =Gm~iȊU}@8.w^[r`Gjׯ~QZ`8-:D]zJ@4i ZQUX/={TMf \+BE}ͪdI`vY` ":htrr[DDCBkjVsґBR<_~?hR E#!2WQqfe՛|K/W5_~1]EeAOX2Ho) %( տU3Gvon3Mw+'Fc9~͆Mg6KzvzXLskST;Tҩ +qs6nzG q U Au[68|5w׻oAuPz[d<| QmWJ @ubl֦j߁jSmF]y?KuꕇbT̨t\ҬS oonGbi4v7ի2Q'LBFƸ5Ώ>(oU7+Gx˹v]]2BᖞϩdcҮ(߱+H+3GfxlTFrJuz>4mVqrVsoawu\*O擓R6vCbCoac Y ,\mQ 6 0?NVwiyqTu^4hԣ˞8*5KTuT;F~bv8߶l´ZOq v蓤Lpkּ͍sI7jWd0 O`)PMJY8 312af]=|eGp^^کWʱ|`<*Ձ{tH_V9(%3FrxJ4 7\7{H }ځk'?3M2.h<%g7:ٹldaOF+u5^c|T'jbr|iUD8!C#::sceam6_8LkTk@nL̇p-MKm%V5 T+@uNnM* ?[aF]^&͹'ցWuƳ|DqR $IDATB7!UvND5{*kF(cU3ُ\ʥx?v3r#ΧRXͭRu1͓l+'=(̟z ̩Xu<}#V.cywPmN :sٟbmiƫN0/FΊi:Qڻz㜪FKT*-ۍmt iꃖQmbTr#6Z|^I4PX5`[jowZrчF/| o҇`%*o"}-.*o<ɷS߾yc{ߡ"N˛y.Ucrov_<}o:kyK6|ڕGuyX {ުQP 0yT'=6zT=zQ̣-ΣƁGѥFK/6=.=/1KÞW[z|)uP<{$Vͧ|9V,ڞgӖ(> /z]b]4 Character Format Examples Colored Text Remember to set the field type to <font color="#ff0000">allow HTML rich text content</font> when using formatting tags. This is done in the <font color="#0000ff">"Data->Configure Data Types->Field Type"</font> dialog. Bold Text Font tags can be applied from the <b>right-click context menu</b> in the "Data Editor" view. Font Size <font size="+2">Large text</font> and <font size="-1">small text</font> Italics and Underline Text in <i>italics</i> and even text <u>underlined</u> TreeLine/doc/doc_fieldconf.png0000644000175000017500000002616511651514477015321 0ustar dougdougPNG  IHDRlb#PLTE "#!(&*&(%'+-*+))07.0-(2=/08.24231,6A+9I48:68518I/:E3=I0>N1=S:>@=>M]JLIQfBQaNPMMQSATiESdQSPCVkBXgTUSEXmRVXMX^HZpVXUB]qF\lKZkO\bD_tV[]J]sY[XL_uJapGbvP_p\][Y^`NawJdyUbh^`]\`cOeuKf{Mh}bdaRhx`egPkLlegdSmvVl|Sn[mxhjg^ms]l}fkmUpjliXr{lnkXsjor\vZudroqngr[vVy`viu{rtqptwY|_za|e{h{\vxunykzuy|n}_y{xbsqz}|eu~hiz}l|{nsjqtzw}Ʒ½¾ÿģ9 pHYs  tIME 3#\ 7 IDATxXׁqrŷqZ"b# v i m$\va96ies::.z+ҵ"{8:% OsQ<0H*ꨣ8; !Č~|3y3͏f͛7;vCʅ+(}G-܌?7Jox[˟o7/WE^y넁N O|+/WUu>ϔOVU7W*/r0K/-}i*J+|U߮OX~-8K?_]^W54e}Nk]>8?by3/ _+M\oE/.$*ץ~iaz-Vג/ \v.WR*\s7''ex f2@Jw$oޟxzҺAWmRi%z5{6yet^_dCJP2E.:G^831LSg={#gLۏ٧N>3}m|W}剉o3%ǟ:ǟ97'O>Lg&̓Z^x^]5oVŸ}vB T|37[\%rn#?;utG_~Ϟ|ѳG84yȷ?~ÇOrç ÑsGɓG{??6O}^OyZ?'chy:gKfl>~ޱ{{wood"98t84~pᓯ| |pġ'NL:tdW_9ʡߝաC@OpcϾp㏽sG8X{ro-z)d?|ol?'σ':~?=?=?OrE _=z?Ǔ+*[;v8| >Y{qGXHSSxӯC㏌M:3>~O?Yăٷx<~ŻRBΟ}Ӌ[ R蚤K?xk'>7yW~@8a3fE{cm<ỠlKü Il*.YQ̿ܘ #/wZwyf^>#yJi6[!5^v[HukvF:͌K^5vbRB9DRL Not-R4>ހ^$Ah=s,y%> a'Ņ`Dcna+zeD.mkXuIĚ }C3Mڦh'ʻR.7] 1aRmSv.qilKI:H\dkh~sL:;.͕ %  ;9!崪~w(\uR5~Lr])t&p4!fus!l0ցh6p*ȗ<%L5%y(1UKKry ڔ;Qf.#eFu8i0%7bƑ6#Ie>%tyȌ)%k#e|͎%4߈)]"T8pьTXrd e9Gε0\n.SBڦ7 p-6 βsfm*heͥG81;'[^psڠC؃E.ә y .s3tdh.Kt .!%ty'`TJM\hGuIY*5sLP ڳ], tʟ5F.)^8(EaQɮN8iT 8V:~u٫\C A,ʜWr)O4VTmWh8q.M tZ*aU\F P2b<ApDjA>#-xXDosr.:Nrƻ}pۈX,+n$C5p]:t Eȵl \ t@&E48SNXXHs:*յ9ՀZ19U\HeUTj\[.K.K.K.oK%~%\> ^+J,v鉉?_|f]̋в|2W8$X񭄦KmqTH\ te{ɚ#T7swk )6n3JhkLM]hgD0pF?_B]bB66;d9fOKr۝ѕP˹\NJِ%sA,4HgJ;gu2'9jn| vIQƊJ!OQXc6EE1j(s LtISTiE;e5J ԴctuڷgL2TPLh&ܪ< +yB>ϰT.S&-2O" S&Hn_tJh|zDMUc'%%Aiox 'dA0aṮR.s!I =a%~-08"_p]o̼Mp`8Ha<1<Ńm1|Z tIxGhC+:LL7tcꖙW@Y\ tI?_@Xq"\ϗ`3bű*R2J+v:ğ/GR}dXLOBaW޿K.K,reR"7qI#rer[ S>@%@%f61{4QC^%IĊp~'^ƍg6FviRpYQ.MN&OW'u]"wiF2 p9"ijJ(s}1Y#F4yB5]*\2;kw8~]ѪV5ojƪJkSy7kCݲkKÖ`Q4)c]rv`u˕rѕXNWƜڴAz7&rd^2M \-]4A>`AnsN ]j:aD;r.LUTa0\h#=-󕼉Q_2ױ \.cjDSƙ$=\JYc_TqulN\uH"bl:66z')1D>{^Hj4McG~nx]B%t ]B%t ]B%t ]B1l]n/Q״P>Gx=bŲr.tn0\<1"ʅP-v9m[L?\𮉕 "񄎻uEGqE63-[j4mibLVQGiDY*+̓0@P 7t.Yf=}dK#+:.}v|cuiwVrS+ǿ/_>Oo}.s٬"[.nc*>)qglUjRlVK4ZD-m t]Hkǎ]a2w/tս?bO?/g}.Ti&18{ jRE]UoWw)*"n-Gql̸q;Xos_8DŽz1}_?vso1;=ox7oG2Qd2XBڙ$c55+5L*z$ɴ icA?Qi<Ʀ>CZ)f. ._ 7{^0}.)"JGHf-=q>D0q5=ՍRߟJB=cQbZ.7@޼̆~t@x|ͦ.%IZq7{f<rڭ!SgR @Z=IcFbŹl<;G2:ٹ`qX EDqMBlɻ̏8^JE3$Vb\ p1L R˲ڶ};f _Xp3.8 :<^}DK.K.%  [n0H\b/-݂+YRګE{o˛V}. cԮ\v)tE/ƶuxGC%bIm(Ux Fck!;mN;4` 92FŞ6n[I`ey.K.K.K.KR.{+dу@u)sHyX"X/B"u9Hkzh=m5Bt)6FIYIL m2} KqK8)imκ=֊Btt4!RG{^aKx} ]N^ӯ N. kg/2NVwa4-6??oې|~ c: c+`ts43oV ecfρ5IUf]f<)QHS>.m{̍3seƼ5ŒP6>VA%6Ȯ`:Զ4rUG+rJ\i.+NZ!UEƺ u1_.oKۿdxy)_/ĸlG ݤln˥thwYEUul$'ejQXìaf1sqy4@wW"' ]IJK)s5%SFgMm3-!6ձw:0MOLQSPi2sd]ROjsTt8eSzåD(9z\s}Y.4|M1&)t]]2mxNj)cw\t ]B%tQBCKנơKx/.K.K) iu}?#ݭ/ER9LBF"f|R.uu}3s sL31|R.19XvY6_fX^Ӧ{Dr׾z\ku+ee6ƀWUՒ@X6\F ּѢwG/?|;o٘'|PMyQ>wZjvzBvJhD,AlϻTw(LeVw ;v%Bf$%fBi Moe?ӡ\ȊakO0V_h*Dbc֋ln~cbQog pqGa+]>%t ]B%t ]B%t ]B%t ]Br ,qYK.K.t9冇sG/'vt\b/@ @ɒИh'UA)4ZTu$bF(>p:R@^+Gi;MAz$?:]UIN'Ec|\QOsp>B&F(zc4zua,V:j *?WGԀRɹ$ဆL)mj Mmte7G պ!ಯ%MSoSxQmuKs+jXy/$\ËQL93WSH&fL[AsI-ATRù?`P[ F6Iw;*J"鬂6Z32Zʼ =j멕u%QN+3Zޙ$Rѥ6%g[ɖː~QE`zڪ GMUsyq[ۆX/\Q[M:&M6Nȁ'Bs.{rXhpg/z#Ա`2!^C+=Pgpy_Jɶ3 M4_.(+1m??oKmg7ms J̨FZ]^ăDZ1 :W ndME'Ս!4qwBi>Wj.bZ m2\$UUs8'~ P~ۺJt,m h'[ƥmZ$T ;Λf/O}---ы 4Y[XbLol}CMmtIDAT٬Oof$tKyf͛ j_^ߊS2Gޒ!wKJ.6ZqG'! - fq} 6%D{]5 QJk郀.=é\&F.s}ywKPqJԶ*n,.S7'mK4tZ8k^8ڣzT1~k t; ?a3oT.$fy;%׎dKW_{Cq5z }JXoXTQ*uB\L/f.ut>^6㾃\aN J${o =p޺Q c* s`nuWTetGWRȥ>qMӦa} ^&#Ch ΰH.zʮĆN+o_I: CjUtiCރ5 ˝skR<\ svaz,-.1y4Džyzgn~nF>5GV}$<\_Jeo+p$i1N&!As ˗͂TT9*,4UIU|n _T.a٢8E0*礘T9a(gK-q-¥5KTBSy-R/T\bq)0s)0Rh*ޥT%\^vhZ4.&\nj8w)ˑ"6 s%\6sv?mR{*#VY*Ǻ\DyRq. ".T˂TEW [Ll t6|KV ,e۪Gr)G#a,lþ@wXr1 Y5\V,.rEDXQƋQK0vOcxv:!r^̥T5<, x$JT"D9C\槲\Ѭo:ì\#,T\RdM5NId7NRTT9pSkdmcuP3eMJE%MKRNjבBB0Op8T(1M܈KzOaT˂T3YWw:QiuL 9Lt2Hn Hzd\ۇ˒pņ&n"e TJtqEZk'3qII/#exƢoѷl˂TڂAR(Yx%R\0 KvdIϫp[R1L,AłiiP=|2Hݦ:o.[-}!`[5 j0j˂ +A%QÚ=>[~xu+L࿖ƢY OCPo|!0F24K gRo` '23]Yϫ̏S$ӸYQLwS=/1ILkӳX:hwh xJ':`+APܪhbqMhQ8Ӂ{z*sID %@=8Z"KTTt$3(2s}"Sq.DM$niNj`{iR9}2[KJ`8da\:ES 5<rp>kS:]K"fK$f5KlKB%\fQ[ @x*LŹ,HaR$Xlf,2%2#0_n%Qe6JŹ,HE-n] %\[B,. aٚ] LŹL LŹԭu!K\\Sq.*vun gJ.Qlyn),ܽBepy\XwJ`6#iUh*b2Sa#R}D %@NpYa;/1Q;%#* q|p_ivIENDB`TreeLine/doc/README.html0000644000175000017500000063350111656365177013664 0ustar dougdoug README

ReadMe File for TreeLine

an information storage program

by Doug Bell
Version 1.4.1 (stable release)
November 11, 2011

Contents


Background

Do you have lots of sticky notes lying around with various useful information jotted down? Or many lists of books, movies, links, website logins, personal contacts, or things to do? Can you find them when you need them? Well, I often couldn't. So here's my answer.

Some would call TreeLine an Outliner, others would call it a PIM. Basically, it just stores almost any kind of information. A tree structure makes it easy to keep things organized. And each node in the tree can contain several fields, forming a mini-database. The output format for each node can be defined, and the output can be shown on the screen, printed, or exported to HTML.

Since I'm not in the software business, I'm making this program free for anyone to use, distribute and modify, as long as it is not incorporated into any proprietary programs. If you like the software, feel free to let others know about it. And let me know what you think - see the Questions, Comments, Criticisms? section for contact information.


Features

General

  • Stores almost any type of information, including plain text, HTML, numbers, dates, times, booleans, URLs, etc.
  • The tree structure helps keep things organized.
  • Each node can have several fields that form a mini-database.
  • Several node types, with different sets of fields, can be included in one file.
  • The node format, including fields, output lines and tree-view icon, can be defined for each node type.

Views

  • The left-hand view defaults to a tree view but can show a flat list of descendants of the current node.
  • The left flat view also shows the results of filtering operations.
  • The right-hand view can show one of three views - for showing output, editing node data and editing node titles.
  • The right-hand view is normally split to show data from the parent node and its children.
  • If multiple nodes are selected, the right-hand view shows all of their data.
  • The output view can be set to show indented output from all descendant nodes.

Navigation

  • There is a quick incremental search command to find a matching node title.
  • There is a find command that searches through all node data.
  • Previous and next selection commands toggle selections to quickly move between parts of the tree.

Formatting

  • The dialog for data type configuration has several tabs to easily set all type, field and output parameters.
  • Formatting information can be copied from another TreeLine file.

File Handling

  • Undo and redo commands are available for all modifying operations.
  • TreeLine files are XML by default, but there are options for automatically compressing or encrypting the files.
  • Document templates for new files are preformatted to cover basic needs.
  • The formatted output can be printed with parent/child lines and headers and footers.

File Import and Export

  • The data can be exported to HTML.
  • An XSLT file can be exported to work with the XML TreeLine files.
  • Tab-delimited tables and tab-indented text files can be imported and exported. Plain text files and Treepad files can be imported.
  • Mozilla and XBEL format bookmark files can be imported and exported.
  • Generic XML files can be imported and exported, allowing TreeLine to function as a crude XML editor.
  • ODF text documents can be imported and exported as outlines.
  • Batch file conversions can be done from a command line interface.

Linking Objects

  • Clicking on URL fields in the output view opens the link in an external web browser.
  • There are internal link fields that select another node based on a reference or a keyword.
  • An executable link can run an external program or open a file when clicked.

Data Manipulation

Customization

  • There are many options for customizing both general and file-based attributes.
  • There are editors for keyboard shortcuts and toolbar commands.
  • The user interface and documentation are available in English, French and German.

Legal Issues

TreeLine is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either Version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the LICENSE file provided with this program for more information.


System Requirements

Linux

TreeLine requires the following libraries and programs:

Windows


Installation

Linux

Extract the source files from the treeline tar file, then change to the TreeLine directory in a terminal. For a basic installation, simply execute the following command as root: "python install.py"

To see all install options, use: "python install.py -h"

To install TreeLine with a different prefix (the default is /usr/local), use: "python install.py -p /prefix/path"

To install language translations for TreeLine, download the "treeline-i18n-..." tar file that matches the version number of the main TreeLine file (ignore the letter suffix on the translation version number). To install the program and translation files simultaneously, extract the translation file from the same directory where the main treeline tar file was extracted and run the install command as described above. If TreeLine is already installed, extract the translation file to any directory and run the translation's install command as described above.

Windows

Simply execute the downloaded installation file (treeline-x.x.x-install.exe). It will install the program with its libraries and optionally create file associations and shortcuts.

To use TreeLine's spell checker, an external program (aspell or ispell) must be installed (see the System Requirements section).

To install language translations for TreeLine, download the "treeline-i18n-..." exe file that matches the version number of the main TreeLine file (ignore the letter suffix on the translation version number). To install the translation files, simply execute the translation file.

If you wish to modify the source code or write your own PyQt programs for Windows, do not use the above procedure. Instead, you need to install Python (see www.python.org), Qt (see www.trolltech.com), and PyQt (see www.riverbankcomputing.co.uk). Then extract the source code files from the Linux version (treeline tar file) to a directory of your choice and execute the treeline.py file.


Using TreeLine


Contents


Getting Started
    Introduction
    Templates
    Example Files

Basics
    Left Pane Views
    Right Pane Views
    Commands
    Multiple Windows

Tree Navigation
    Keyboard Shortcuts
    Selection
    Searching

Tree Editing
    Edit Menu
    Shortcuts
    Right Pane Views
    Spell Check

Formatting and Node Types

Field Types

Tree Data Manipulation

Printing
    Print Options
    Page Setup
    Printer Font
    Header and Footer
    Print Preview
    Printing Problems

File Handling
    File Compression
    File Encryption
    Auto Save
    Saved Tree States
    Command Line

File Import

File Export
    General Information
    HTML
    Delimited Text
    Bookmark Files
    Generic XML
    XSLT
    ODF Text Document

Customizations
    Options
    Fonts
    Keyboard Shortcuts
    Toolbars
    Colors
    Tree Icons

Plugins
    Description
    Installation
    Interface

Common Problems
    Closed Panes
    XML Error
    Printing

Getting Started

Introduction

TreeLine is a more complex program than it appears at first glance. Reading much of this documentation is highly recommended in order to learn to use it effectively.

The
Basics section describes views and selections. To learn how to move around and change the tree, refer to the Tree Navigation and Tree Editing sections. Next, its time to go beyond using TreeLine with only simple text for each node. To learn about setting up custom data for each node, refer to the Formatting and Node Types and the Field Types sections.

Templates

When starting a new file, the templates dialog gives a choice of nodes with a single line of text or with long text. The single line template only has a node title, while the long text options have a title and a multiple line text field in each node. For long text, you can also choose between plain text, which preserves line breaks, and HTML text, which allows more complex formatting.

There are a few more specific options, including canned setups for personal contact information, a book list and a to-do list.

Example Files

Various TreeLine sample files can be opened by using the "File->Open Sample" command. They include (roughly in order of increasing complexity):


Basics

Left Pane Views

The default view in the left pane is the "Tree View". Parent nodes can be opened and closed to display or hide their indented descendant nodes.

The other tab in the left pane is the "Flat View". It displays all of the descendants of the nodes that are selected in the "Tree View" in a flat list. It also shows the results of filtering operations (see the
Filtering section).

Right Pane Views

The right pane is tabbed to show one of three different views of the data. The "Data Output" view shows the formatted text for each node and is read-only (see Figure 1). The "Data Editor" view shows a text edit box for each data field within a node (see Figure 2). The "Title List" view shows a list of node titles that can be modified using typical text editor methods.

When a parent node is selected in the tree, the right view will default to showing information about the selected node in an upper pane and information about the selected node's children in the lower pane. The "View->Show Child Pane" command will toggle the display of the child nodes. If the selected node has no children, the view will show a single pane with information about the selected node only.

When multiple nodes are selected in the tree, the right view will not display any child node information. It will instead show information about every selected node.

When the "View->Show Output Descendants" command is toggled, the "Data Output" tab will show an indented list with information about every descendant of a single selected node.

The initial state of the views can be controlled by changing the startup conditions in "Tools->General Options".

By default, the number of nodes displayed simultaneously in the "Data Editor" view is limited. Buttons in a heading allow the next or previous set of nodes to be displayed. This provides a convenient way to page through the node information without slowing the program by trying to display too many fields. The number of pages to display can be set in "Tools->General Options".

Commands

Most of the menu and tool-bar commands apply to the items selected in the left view. In general, they perform an operation on the nodes themselves or on the descendants of the nodes. Multiple nodes can be selected by holding down the "Ctrl" and "Shift" buttons when clicking with the mouse.

To add information to a new TreeLine document, use the "Edit->Add Child" command to create a new node. Then combinations of the add and insert commands may be used for additional nodes. Alternatively, new node titles may be typed into the "Title List" view in the right pane.

Multiple Windows

Multiple TreeLine windows with different files can be opened within the same TreeLine session. The "Open files in new windows" option in "Tools->General Options" controls whether new windows are used when opening or creating files.

The "Window->New Window" command will create a new window with views of the same TreeLine file. Changes in either window are saved in the same document. This can be used to work in different sections of the file simultaneously. For performance reasons, the non-active window does not show the changes made in the active window until either the non-active window is focused or the "Window->Update Other Window" command is issued.

Starting TreeLine (from a shortcut, file association or command line) will activate an existing TreeLine session if it is already running. A new window will be opened if an unopened file was specified by file association or in the command line.


Tree Navigation

Keyboard Shortcuts

There are several keyboard commands that can be used for tree navigation. The up and down arrow keys move the selection. The left and right arrows open and close the current node. Holding the CTRL and SHIFT keys with "J" or "K" moves between siblings, skipping children. The CTRL key and "U" moves to an item's parent. The "Home", "End", "Page Up" and "Page Down" keys can be used to move quickly through the tree. The CTRL and SHIFT keys can be held with the "Page Up" and "Page Down" keys to move the tree view even when a different pane is focused.

All of these keys and the keyboard shortcuts for pull-down menu commands can be customized by using the "Tools->Set Keyboard Shortcuts" command. The shortcut editor has tabs for menu and non-menu based commands. Simply type the new key sequence with the appropriate field selected.

Another way to move through the tree is to type the first letter of a visible node title. Hitting the letter again moves to the next possibility.

Selection

Multiple nodes can be selected by holding down the CTRL or the SHIFT keys when changing the active node. Individual nodes are added or removed from the selection when the CTRL key is held. The selection of all nodes between the old and new active nodes are toggled when SHIFT is held. The active node can be changed by using the mouse or by using any of the keyboard navigation methods.

The "View->Previous Selection" and "View->Next Selection" commands can be used to toggle through a history of selections, allowing faster navigation through the tree.

By default, the selection sequence doesn't matter. However, if the general option for multiple selection is changed to "Selection Order", nodes selected with CTRL key held are output in the order selected.

Searching

There are two ways to search for nodes. These methods can find nodes that are buried in the tree structure. The first is the "Tools->Find" command. Keywords can be entered in a modeless dialog box. A node is found if the keywords are matched in any of the node's fields. The matching text will highlight in the "Data Output" pane.

The next method is an incremental search, started by typing CTRL + "/". Then type the search string, which shows up in the lower status bar area. The search will progress as the string is being typed. The incremental search only finds text in the node's titles. The previous search can be repeated with the "F3" key and backward with "Shift-F3".

By default, parent nodes will automatically open and close when found with the search methods, by typing the first letter, and with the "next sibling" keyboard command. This behavior can be disabled in "Tools->General Options".


Tree Editing

Edit Menu

The commands in the "Edit" menu (except for undo and redo) operate on the selected nodes in the left tree view. The cut, copy and paste commands can be an exception to this, since they operate on the right view when something is selected there. And keep in mind that, in general, the descendants of the selected nodes are also affected.

Paste will add a copied nodes as the last child of the selected node. If more than one node is selected, additional copies are added under each selected node.

The "Edit->Paste Node Text" command renames the selection based on either the text in the clipboard or the title of the top node in the clipboard.

Shortcuts

There are several shortcuts for use in tree editing. Drag and drop will move or copy nodes to become a children of the destination node. Clicking on a selected node will rename it. Pressing the enter key will insert a new node, and pressing the delete key will remove the selected nodes. If desired, these shortcuts can be disabled in "Tools->General Options".

Right Pane Views

In the right pane, the "Data Editor" view provides the most direct way to edit the data within a node. If the edited field is used in the title formatting, the node title in the tree will show the changes. The field editor will scroll, allowing multiple lines of text to be entered.

There are items in the "Data Editor" box context menus to add HTML font tags around selected text. These tags include bold, italics, underline, size and color. Note that the fields must be set to display HTML content (see the
Field Types section) for this to be effective.

Another edit box context menu item will add an internal link in the text. The status bar will then prompt the user to click on the link destination in the tree view. This will add an internal link to that destination to the text in the edit box. Of course, the field must be set to display HTML content for this to be effective.

An external editor can also be invoked from a "Data Editor" box context menu. After the text editor saves changes and is closed, the changed text will be in the text box. The EDITOR environment variable can be used to specify the editor to start, or, if the variable doesn't exist, TreeLine will prompt for an executable to set as the default.

Also in the right pane, the "Title List" view is useful to quickly rename child titles or to add new child nodes. A text list of new nodes can even be pasted directly into this view.

Spell Check

There is a spell check command in the "Tools" menu. Use of this command requires an external program to be installed (either aspell or ispell - see the System Requirements section). If there are any misspelled words in the selected branch, a dialog will allow the word to be ignored, added to the dictionary, replaced with a suggestion or edited. This will spell check the text in all data fields of each node.

By default, the spell check will use dictionaries for the current operating system language. If using aspell, there is an option under "Tools->File Options" to specify an alternate two-letter language code for the current TreeLine file. Note that the appropriate aspell dictionary files must be installed and that this option does not work with ispell.


Formatting and Node Types

Setting Nodes to a Type

By default, a new TreeLine document contains two node types: "ROOT" and "DEFAULT". The type is shown at the top of each node box in the "Data Editor" right-hand view. The creation of new types and the customization of types is described below. To set the selected nodes to a specific type, use the "Data->Set Item Type" menu. Alternately, to set a series of child and descendant nodes to a specific type, use the "Data->Set Descendant Types" command. The resulting dialog box allows the selected nodes, their children, all descendants, or descendants matching logical conditions to be set to the highlighted type. The dialog box can be left open while the tree selection is changed to set more nodes.

Type List

The "Type List" is the first tab of the "Data->Configure Types Dialog". The list of data types can be modified by the buttons on the right. New types can be added, and existing types can be copied, renamed or deleted.

Type Config

"Type Config" is the second tab of the "Data->Configure Types Dialog". It contains a selection for the default child type. If set, this will be the initial type used for new children with this type of parent. If set to "[None]", children will default to either the type of their siblings or their parent.

The "Change Icon" button allows the selection of a custom tree icon for this data type. The "Clear Select" button on the icon dialog can be used to set the icon to "None", so that no icon will be displayed for this type. To avoid showing any tree icons, the "Show icons in the tree view" general option can be unset.

Field List

The "Field List" is the third tab of the "Data->Configure Types Dialog". The list of fields within a data type can be modified by using the buttons on the right. New fields can be added, and existing fields can be moved up and down in the list, renamed or deleted.

Field Config

"Field Config" is the fourth tab of the "Data->Configure Types Dialog" (see Figure 3). The field type and its output format string can be set (see the Field Types section for details). Extra prefix and suffix text to be output with the field can also be set, and a default field value for new nodes can be entered. There is a choice between plain text, which preserves line breaks, and HTML text, which allows various formatting options. Finally, the number of lines displayed in the editor for the field can be specified.

Output

"Output" is the last tab of the "Data->Configure Types Dialog" (see Figure 4). The left half of the dialog shows the fields. The right half shows the formatting for the title (used for the node text in the tree view) and the node output. The formatting consists of text lines with embedded fields. The fields are shown as "{*field_name*}". The field that is selected in the list can be added to a format at the cursor position with the ">>" keys. The format field reference at the cursor can be removed with the "<<" keys.

Title Formatting

When a node in the tree is renamed, the program attempts to match the title formatting pattern to set the appropriate fields. If the title formatting is too complex, it may not correctly guess the intent. Things like adjacent fields with no characters separating them should be avoided unless you do not wish to rename nodes from the tree.

If the text data used for a tree view title has multiple lines, only the first line will be used as the title.

Skipped Output Lines

If a line in the output formatting contains one or more fields and all of those fields for a given node are empty, the line is skipped. No blank line or embedded text will be output for that line. Note that this does not apply to a line without any fields (only embedded text). Also, when a line ending with a <br/> or an <hr/> tag is skipped, the ending tag is retained.

HTML Tags

Simple HTML formatting tags can be used in node output formats. Commonly used tags include "<b>bold</b>", "<u>underline</u>", "line break<br/>", "<hr/>horizontal line", and various font tags. Complex block tags should generally be avoided. When the "Allow HTML rich text in formats" file option is disabled, formatting tags are treated as plain text. For an example of tag use, see the "sample_color_items" file (by using the "File->Open Sample" command).

A line break is automatically output after each formatting line, unless the "Add line breaks after each line" file option is unset. In addition, the "Add blank lines between nodes" file option determines whether there is also an automatic blank line between node outputs. A line break tag ("<br>") can be used at the end of the formatting to get the same effect, or a horizontal line tag ("<hr>") may be used instead to separate the nodes.

Formatting Examples

Here is an example of output formatting for a book list:

<hr/>"{*Title*}"
(c) {*Copyright*}, Rating: {*Rating*}
{*PlotDescription*}

Sample files with various kinds of formatting are included in the program distribution. They can be opened by using the "File->Open Sample" command. They are also listed in the
Example Files section.

Other Field References

References to fields that are not contained within the node can be added to the output. Pushing the "Show Advanced" button on the "Output" tab of the configure dialog makes a reference level selection become visible.

If the reference level is changed to "File Info Reference", fields containing file meta-data can be added to the output. These include the file name, path, size, and modified time. These are shown as "{*!field_name*}" in the title and output format editors.

There are field references to various ancestor nodes (parents, grandparents, etc.). These require the data type of the reference to be specified. This selection determines the field names that are available, but the data from any type with a matching field name will be shown in the output. References to fields from parent and grandparent nodes are shown as "{**field_name*}" and "{***field_name*}", respectively. There are also general ancestor references, shown as "{*?field_name*}", that take data from the closest ancestor with a matching field.

References to child nodes can also be added. These also require that the child data type be specified. The child data becomes embedded in the parent output. The child data is delimited with a separator string defined as a file option. The separator defaults to a comma and a space, but can be set to <br> or anything else.

Finally, a "Child Count" reference can be added. This field will show the number of children ("Level1" field) or grandchildren ("Level2" field) of a node. These are shown as {*#Level1*} in the format editors.

For examples of these fields, see the "sample_other_fields" file (by using the "File->Open Sample" command).

Sibling Prefix and Suffix

Pushing the "Show Advanced" button on the "Type Config" tab of the configure dialog makes a sibling text section become visible, with settings for sibling prefix and suffix tags. These tags can often be left blank, but are useful for creating tables or bulleted lists. These tags will be placed before and after sibling groups of the proper type. For example, to create an output table, the sibling prefix tag could be set to "<table border="1">" and the suffix tag could be set to "</table>". Then, the output format could be set to:

<tr><td>"{*Title*}"</td><td>(c) {*Copyright*}</td></tr>

Also see the "sample_table_booklist" example file (by using the "File->Open Sample" command).

Siblings should generally be of the same type, or at least have the same prefix and suffix tags.

Link Reference Field

One of the fields for each type is designated as the link reference field. By default, it is the first field in the list. Pushing the "Show Advanced" button on the "Type Config" tab of the configure dialog shows the selector for this field.

The link reference field is used for internal link references and for the arrange and flatten by reference commands. The contents of this field should generally be unique if usage of these functions is planned.

Generic and Derived Types

Data types can be set to derive their field settings from a generic type. This allows types with different output formatting to always use the same set of fields. Any changes to the generic's list of fields and field types are automatically reflected in the fields of all derived types. This does not apply to a field's output formatting, which can still be set independently.

There are two methods for creating derived types. First, a derived option can be selected when copying a type on the "Type List" tab of the "Data->Configure Types Dialog". Alternately, a generic type can be specified from the derived type's "Type Config" tab of the dialog if the advanced functions are shown.

Conditional Types

Conditional expressions can be used to automatically assign a data type based on each node's content. Conditions can be assigned only to a generic type and its associated derived types. This allows the automatic assignment of different output formatting or different icons depending on each node's field data.

The conditional dialog box is accessed from a button on the "Type Config" tab of the "Data->Configure Types Dialog" if the advanced functions are shown. Each line of the condition includes a field, an operator and a comparison value (see
Figure 5). The operators include equality, greater than, less than, starts with, ends with, and contains. There are also True and False operators that will toggle the type of all nodes simultaneously.

For special field types such as dates, times, and booleans, the comparison value should be entered in the same format that is used in the Data Editor window. In general, the starts with, ends with, and contains operators should not be used for these special fields, since the comparison is done using an internal data representation. Dates and times also support a special comparison value of "now", which is always interpreted as the current date and time.

The "Add New Rule" button is used to add additional condition lines. The lines can be joined with "and" or "or" operators. The "Remove Rule" button deletes the last condition line. If only a single line is present, the "Remove Rule" button completely removes the condition.

Conditions do not have to be set for all types in a family. If no conditions are true for a node, the program will select a blank condition over a false one.

For an example, see the "sample_conditional_todo" file (by using the "File->Open Sample" command).

Copying Formats

Another method for changing data type formatting is to copy the formats from another TreeLine file. This is done with the "Data->Copy Types from File" command. All types from the chosen file are copied. Any types in the current file with matching names are overwritten, but types with unique names are retained.


Field Types

Field Options

The field type and options are set in the "Field Config" tab of the "Data->Configure Types Dialog" (see Figure 3). The field type may be set to text, number, choice, combination, auto choice, date, time, boolean, URL, path, internal link, executable link, unique ID, email, or picture. Prefix and suffix text can be entered and will show up whenever the data is not blank.

There are also settings for text content handling that can be set to allow HTML rich text in the field data or to preserve line breaks (ignoring HTML code). If HTML rich text is used, carriage returns are ignored and non-escaped "<", ">" and "&" symbols do not display. There is also a general option available that makes new fields default to HTML content. For an example of rich text use, see the "sample_char_format" file (by using the "File->Open Sample" command).

Several of the field types use a formatting string to define their output. For a list of available formatting characters, use the "Format Help" button. Entries in the data editor which do not match the format will cause the field name label to show in bold, and the output for that field will be replaced by "#####".

Text Type

The default field type is a text field. These fields are edited using edit boxes in the data editor view. The edit box height expands when redisplayed after adding several lines of text. The edit box height can also be set explicitly in the "Field Config" tab.

Choice, Combination and Boolean Types

The choice and combination field types allow for the selection of text items from a pull-down edit list. The formatting strings for these types list the items separated with the "/" character (use "//" to get a literal "/" in an item). Choice is used to select a single item and combination to select multiple items. The pull-down edit list for combination allows items to be added or removed. Also, the initial text of an item can be typed and auto-completed.

The boolean field type is similar to choice, but defaults to options such as "True/False", "yes/no" and "1/0".

There is also an AutoChoice field type. Like Choice, it provides pull-down selection items, but it does not have a formatting string. The pull-down is automatically populated with all previously used entries.

Number Type

In the number type, special characters in the format define the display of the numbers. The format uses a string of "#" (optional digit) and "0" (required digit) characters to define the output formatting. For example, pi formatted with "#.#" is "3.1" and formatted with "00.00" is "03.14". Regardless of the formatting, digits to the left of the decimal point are not truncated, since that would display an incorrect result. But use care to show enough decimal places (either optional or required) to avoid problems with round-off error.

The radix character can be specified as either "." or "," to handle internationalization. For use as a thousands separator, use "\," or "\.". For example, a large number may be formatted as "#\,###\,###.##" or as "#\.###\.###,##". Press the "Format Help" button from the field format dialog for more formatting details.

Unlike most other formats, the number type also uses the output format for display in the Data Editor. Of course, any new entry with a reasonable format is correctly interpreted (but the correct radix character must be used).

Date and Time Types

In the date, and time field types, special characters in the formats are replaced by elements of the data, similar to number fields. Press the "Format Help" button from the field format dialog for formatting details.

There are also formats for these types under "Tools->General Options->Data Editor Formats". These control how these fields are displayed in the Data Editor view. Generally, entries in the data editor with various formats will be correctly interpreted regardless of this setting, but dates must use the correct day-month-year sequence. Also note that the date editor format does not support days of the week. Entries which cannot be interpreted will cause the field name to show in bold.

A default initial field value of "Now" can be used to get a time-stamp of node creation.

Linking Types

The URL, path, and email field types are used to create links in the output. URL is for a standard web link (defaults to http:// unless otherwise specified), path is for a local file link (defaults to file:///), and email is for a mail link (defaults to mailto:). When clicked in the output window. these links open an external browser or email program. In exported HTML, they act as regular links. Simply enter the desired full path (such as "www.bellz.org/treeline/index.html") in the data editor. In Linux, setting the "BROWSER" environment variable to a string like "mozilla %s" will result in the desired browser being used.

There is also an internal link field type. It creates a clickable link in the Data Output window that selects the next node with matching text in its reference field. If link or reference fields contain multiple lines, each line is treated as a separate link or reference, respectively. If desired, the same field can be used as both the link and reference fields, so that clicking on a keyword in one node's field selects the next node that also has that keyword. If exported to HTML, the internal links function as links to page anchors, but, in cases with duplicate references, they only find the uppermost reference on the page instead of the next one. For an example, see the "sample_intern_links" file (by using the "File->Open Sample" command).

The ExecuteLink field type runs an external program when its link is clicked. The command to run is given by the text of the field. Or the field's prefix may contain the program name, so that the field's text is used for arguments or file names. A general option can disable these links when security is a concern.

The picture field type will show a referenced picture in the output. Enter the path to a local image file in the data editor. Supported image types include JPEG and PNG. GIF is support is optional - it may be supported depending on build options in the Qt library. Of course, when exporting HTML, the picture will show as long as the browser supports the format.

Unique ID Type

A Unique ID field type is automatically loaded with unique numbers. This is often useful for ensuring that the link reference field is always unique.

Initially, the field's format string is set to the desired first number in the series, with optional leading zeros and optional leading or trailing characters. The initial number and the extra characters are useful to avoid duplication of numbers in different files. All nodes of the data type will get an automatically assigned ID. The field format string will always show the next available number, which will be given to the next new node of that type.

Advanced Options

The "Show Advanced" button in the "Field Config" tab of the "Data->Configure Types Dialog" brings up additional settings.

Link fields can display alternate text in place of the target URL. This is done by specifying an alternate text field.

There is also a setting to flag fields as required to be filled in. Those fields will be marked with an asterisk in the data edit view.

Fields can also be set as hidden, to prevent them from showing in the data edit view.


Tree Data Manipulation

Category-based Commands

The "Data" menu contains commands for arranging and flattening the data by category and by reference. These methods are used to automatically add and remove levels of nodes below the current node in the tree.

The "Add Category Level" command allows you to select one or more of the fields that the child nodes have in common. These fields are used to create new parent nodes for the children, grouping them by common categories. For example, in a list of books, picking the "author_first_name" and "author_last_name" fields will result in a tree with the books under new nodes for each unique author.

The "Flatten by Category" command is almost the opposite of "Add Category Level". It eliminates any descendant nodes with children, transferring their data fields to their children. It will rename fields instead of overwriting data with the same field names, but this command is most useful when the children and parents are different types with unique field names.

Reference-based Commands

The "Arrange by Reference" and "Flatten by Reference" commands arrange data nodes by using pointers to the value of their parent's reference data field. These commands rely on nodes having unique values in the reference field, such as an ID number. The "Flatten by Reference" command adds a field containing the parent's ID to each descendant node. It then places all of the nodes under the selected root node. The "Arrange by Reference" command does the opposite, placing each node under the parent with the referenced ID. If there are multiple nodes with the same ID, the nearest node above the child is chosen. Any nodes with lost parents are placed directly under the selected root node.

Sorting

The "Data->Sort" command brings up the sort dialog. It contains options for sorting the entire tree, the selected branches, the selection's children or the selection's siblings. The sorting can be based on specific fields within specified types or on titles only.

Sorting by types allows the selection of several fields to be used as the first key, second key, etc. Multiple types can be selected in the list on the left. The fields that the selected types have in common are listed on the right. The fields to be used as keys are selected in order with the left mouse button and the direction is changed with the right mouse button.

Filtering

The "Data->Conditional Filter" command limits the display of nodes in the flat view based on user-defined rules. First, the type to be filtered is selected. Next, logical rules may be entered based on any of the type's fields (similar to Figure 5). Multiple rules can be linked by the "and" and "or" operators (press the "Add Rule" button). The "True" rule can be used to show all of the nodes of a given type. Only descendant nodes that match the rules will be shown in the "Flat View" pane. The filtering stays in effect for the "Flat View" pane until it is cleared using the "Data->Clear Filtering" command (see Figure 6).

The "Data->Text Filter" command works similarly, except it only prompts for a text search string. Only the nodes that have a match for that string somewhere in their field data are displayed in the "Flat View". Again, the filtering stays in effect for the "Flat View" pane until it is cleared using the "Data->Clear Filtering" command.

Numbering

The "Numbering" command is used to add number fields to descendant nodes. The number fields do not automatically update when the tree is modified - the "Numbering" command must be repeated. In the dialog, a new or existing field name is entered, and the root (selected) node may be included in the numbering if desired. One of three styles may be chosen: outline style restarts numbering for each group of children, section style appends the child's number onto the parent's number, and single level style numbers only the first level of children. The default formats can be used ("I, II..., A, B..., 1, 2..." for outlines; "1, 2..., 1.1, 1.2..." for sections), or custom formats may be specified for each level. The custom formats should contain one of the following characters: "1", "A", "a", "I", or "i". The series will continue from there (using numbers, letters or Roman numerals, respectively). The last occurrence of one of these characters in the format string is used - previous ones are assumed to be part of the format.

Change Selections

Finally, the "Change Selected Data" command allows the values of a data field to be changed simultaneously for all selected nodes. Simply select the desired field and enter the new value. To erase field values, enter a character then delete it - this will indicate that a blank field is desired.


Printing

Print Options

The "File->Print Options dialog's first tab is "General Options". On the left, printing of the entire tree, the selected branches (all descendants) or the selected nodes can be selected. The first two options print all of the descendants with the children indented.

On the right are options for drawing tree structure lines, including the root node, including only open children and keeping the first child with its parent. The tree structure lines are drawn to connect parent and child nodes. They can make parent/child relationships easier to visualize, especially across multiple pages. The lines may not display as desired when using some HTML formatting tags.

By default, TreeLine will avoid breaking pages between a parent and its first child. This behavior can be disabled by disabling the "Keep first child with parent" option.

Page Setup

The second tab of the "Print Options" dialog box is "Page Setup". On the left are settings for page size, orientation and units. On the right are settings for columns, indent offsets, and margins.

Printer Font

The third tab of the "Print Options" dialog box is "Font Selection". The printout will use the same font as the "Data Output" pane if the upper check box is checked. If it is unchecked, a font can be selected specifically for printing.

Header and Footer

The fourth tab on the "Print Options" dialog is used to set the print header and footer. There are file info fields on the left and six header/footer edit boxes (left, center and right justified) on the right. These fields can be added and combined with other text in the edit boxes, just like in the "Output" tab of the "Data->Configure Data Types" dialog. The header and footer settings are saved with the TreeLine file.

Print Preview

The print preview window can be shown by using the "File->Print Preview" menu or with the button on the "Print Options" dialog. Buttons on the window are used to navigate between pages, open the "Print Options" dialog, or open the print dialog. The print preview will show more detail if its window is made larger.

Printing Problems

Some printing problems, especially problems with margins and word-wrapping, can be eliminated by changing the print font to a font that is better supported by the printer.


File Handling

File Compression

A TreeLine file is in an XML text format. There are also options to work with compressed files (gzip format) to save space. Individual files can be set to compressed mode from either "Tools->File Options" or from the save-as dialog. There is also a general option to set the default mode for new files.

File Encryption

There is a file encryption option to password protect TreeLine files. Individual files can be set to encrypted mode from either "Tools->File Options" or from the save-as dialog. There is also a general option to set the default for new files. The encryption uses the SHA hash function as a stream cipher - it should be fairly secure.

Auto-Save

An auto-save feature can store unsaved files with a "~" appended to the file name. The backup files are automatically removed when the file is saved or TreeLine exits cleanly. The auto-save time interval is set in the general options. Setting the interval to zero disables this feature.

Saved Tree States

When opening a recently used file, TreeLine will restore the states of open and selected nodes. This information is stored in the user's TreeLine configuration file. If desired, this feature can be disabled with a general option.

Command Line

Command line options allow non-interactive file importing and exporting. This allows automated runs to be scheduled. For more details, run "treeline -h" (or "treeline_dos -h" in Windows) from the command line.


File Import

General Information

A TreeLine file is in an XML text format. Other types of text files can be imported simply by opening them. Opening a file that is not valid TreeLine XML will result in a prompt for the type of import desired.

Delimited Text

TreeLine will open a text file with a tree structure represented by tabs before each line. In this case, only the node title is imported, without any extra fields.

A tab-delimited table can also be imported. It becomes a single level of children under the root node, with each node containing fields from each table column. The first row of the table is used as field names and each subsequent row becomes a node.

Plain Text

There are two types of plain text import. One creates a separate node for each line in the file. The other creates a node for each paragraph, assuming the paragraphs are separated by blank lines. In both cases, the resulting TreeLine file will have all of the text under a single parent, but it is a good starting point. Additional structure can be added later.

Treepad Files

There is also a filter to import files from the Treepad shareware program. Only Treepad text nodes are supported.

Bookmark Files

TreeLine will import bookmark files in both the Mozilla HTML format (Mozilla, Firefox and Netscape browsers) and the XBEL format (Konqueror, Galeon and Elinks browsers). Each bookmark becomes a node with a name and a link field. Some information in the files, such as visited dates and icon references, is not imported. For an example, see the "sample_bookmarks" file (by using the "File->Open Sample" command).

Generic XML

TreeLine will import and export generic XML files. These routines do not have much intelligence - each XML element becomes a node and each XML attribute becomes a field. XML text content become fields named "Element_Data". This lets TreeLine function as a crude XML editor.

ODF Text Document

TreeLine will import Open Document Format (ODF) text documents, from applications such as OpenOffice.org and KWord. The node structure is formed based on the heading styles assigned in the document. Any text under each heading is assigned to that heading's node. The import filter is intended for simple text outlines only. No formatting is maintained, and objects such as tables and pictures are not imported.


File Export

General Information

Files are exported using the "File->Export" command. This will show a dialog box of available export types and options.

HTML

Single-file HTML output is similar to printing, with similar options. It can include the print header and footer in the HTML, and the number of columns can be set.

There are also two multiple-file HTML export functions that create directory structures. Directories are named for the content of the reference data field, which must contain legal file names and not have duplicates under the same parent. The first form creates a table in each HTML file that contains the data for a set of siblings, as well as links to the parent and child pages. The second form creates an HTML page for every node, with a navigation pane on the left side of each page that contains links to the node's parent, uncles and children.

Delimited Text

Data can be exported to tabbed title text and tab-delimited tables. These formats are the same as the corresponding import formats. When exporting to a table, only the first level of children is used, so you may wish to flatten the data before exporting.

Bookmark Files

TreeLine will export bookmark files in both the Mozilla HTML format (Mozilla, Firefox and Netscape browsers) and the XBEL format (Konqueror, Galeon and Elinks browsers).

Generic XML

TreeLine will import and export generic XML files. These routines do not have much intelligence - each node becomes an XML element and each field becomes an XML attribute, except for fields named "Element_Data" that become the element's text. This lets TreeLine function as a crude XML editor.

XSLT

In addition to exporting data, the format for a file can be exported to an XSLT file. This can be used to display the XML data from a native TreeLine file in a compliant browser without exporting the data to HTML. Recent versions of most browsers are XSLT compliant. Note that complex field type formatting will be ignored by the XSLT.

When exporting to XSLT, a link to the XSL file is added to the TreeLine file. Note that the TreeLine file must then be saved. In general, the TreeLine file should have a .xml extension so that the TreeLine file can be opened directly in a compliant browser. After that, the XSL file does not need to be re-exported for data changes (only for formatting changes).

If HTML tags are used in data formats that will be exported to XSLT, they should use xHTML style (<br /> instead of <br>). When exporting, there is a prompt for the name of an optional style sheet (css). This name is stored in the TreeLine file as the default for future exports. Also, the reference to the XSLT file in the TreeLine file may be removed with the "Tools->Remove XSLT Reference" command.

ODF Text Document

TreeLine will export an outline to an Open Document Format (ODF) text document, compatible with OpenOffice.org and KWord. The title of each node is assigned a heading style at the appropriate level. Any other text in the output of each node becomes normal text under the heading. The export filter is intended for simple text outlines only. Any HTML formatting is stripped, and objects such as tables and pictures are not supported.


Customizations

Options

TreeLine's behavior can be modified with several settings available in "Tools->General Options". Most of these options are covered elsewhere in this document.

Under Windows, a "treeline.ini" file in the user's application settings directory is used by default to store the settings. For portable use of TreeLine, that file can be manually moved to TreeLine's "lib" directory. Once it exists, a "treeline.ini" file in the "lib" directory will be used and updated automatically if the file isn't also in the user's application settings directory.

Fonts

Fonts used in the tree views, the output view and the editor views can be set in the "Tools->Set Fonts" menu.

Keyboard Shortcuts

Keyboard shortcuts can be customized by using the "Tools->Set Keyboard Shortcuts" command. The shortcut editor has tabs for menu and non-menu based commands. Simply type the new key sequence with the appropriate field selected.

Toolbars

An editor to customize the toolbars is available from "Tools->Customize Toolbars". The number of toolbars can be set and the buttons on each can be defined.

Colors

User interface colors can be set using the last three items in the "Tools menu. If the "Use Default System Colors" command is enabled, the user background and text color settings are overridden.

Tree Icons

There is an icons directory in the user configuration directory ("~/.treeline-1.x/icons" on Linux, "Documents and Settings\<user>\Application Data\bellz\treeline-1.x\icons" on Windows). Image files (PNG or BMP) placed into this directory are available for use as tree icons.


Plugins

Description

TreeLine has an interface to plugin extension modules. This allows extensions to be written by various coders that provide functionality needed by a few users without adding bloat to the main application. The plugins can add new menu items, access the TreeLine data and read and write TreeLine Files.

Currently available plugins are listed on the "Downloads" page of the TreeLine web site.

Installation

The plugins are installed by copying their Python file (*.py) to a plugins directory. This can be a directory in the TreeLine installation ("<prefix>/lib/treeline/plugins/" on Linux or "TreeLine\lib\plugins\" on Windows) or in the user configuration directory ("~/.treeline-1.x/plugins/" on Linux, "Documents and Settings\<user>\Application Data\bellz\treeline-1.x\plugins\" on Windows). A list of loaded plugins can be found using the "Help->About Plugins" command.

Interface

Information about writing new plugins can be found in the "plugininterface.py" file in the TreeLine source code.


Common Problems

Closed Panes

If one of the view panes is not visible when it should contain data, it has probably been collapsed to a size of zero. This can apply to the left pane and to the upper and lower right panes. To fix this, look for a thin stripe along one of the view borders that can be dragged back into its proper position.

XML Error

An error message, "Error loading XML Parser", typically means that TreeLine could not find a necessary XML library. Under Linux, Python uses external libraries for parsing. Installing either the expat library or the PyXML package should fix the problem. Under Windows, Python includes a parser, so this error should not be seen unless files are missing or corrupt.

Printing

Some printing problems, especially problems with margins and word-wrapping, can be eliminated by changing the print font to a font that is better supported by the printer.


Revision History


November 11, 2011 - Release 1.4.1 (stable release)

Bug Fixes:

  • Fixed failures due to bad internal references when opening encrypted files in multiple windows.
  • Avoid corruption of user option settings after attempting to automatically open an imported file as the last file used.
  • Fix problems with imported files when the user sets new files to be encrypted by default.
  • Prompt the user for a password when copying types from an encrypted TreeLine file.

April 14, 2011 - Release 1.4.0 (new stable release)

Notes:

  • This is a new stable release, ending the 1.3.x development series.
  • This release is available in English, French and German, although the French documentation is not completely up to date. Volunteers to do additional translations are welcome.

Updates:

  • The file browser used for editing path-based fields now remembers the previously used directory.
  • Added icons for the New Window and Close Window commands so they can be added to toolbars.

Bug Fixes:

  • Fixed a bug that could keep the column control from being enabled during HTML export.

Major New Features Since 1.2.x:

  • Different files can be open in multiple TreeLine windows within the same TreeLine session. This is controlled with commands in the new "Window" menu.
  • The "Window->New Window" command creates a new window with views of the same TreeLine file. Changes in either window are saved in the same document.
  • Starting TreeLine (from a shortcut, file association or command line) will activate an existing TreeLine session if it is already running.
  • An "Add internal link" context menu has been added to text boxes in the data editor. It prompts for another node to be clicked with the mouse, then an inline internal link is added with that node's reference information.
  • Added an "HTML directory pages" export option. This will export a directory structure with an HTML file for every node. A navigation pane on the left side of each page contains links to nearby nodes.
  • New dialog buttons will restore the default configuration when customizing toolbars or shortcuts.
  • A new text search function has been added to the help viewer.

January 5, 2011 - Release 1.3.5 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work.

Updates:

  • Incorporated some minor code clean-ups (thanks to Erik Wegner for identifying them).
  • The Windows binary is now based on the Python 2.7, Qt 4.7 and PyQt 4.8 libraries.

Bug Fixes:

  • Fixed incorrect sorting by type with siblings of mixed types.
  • Fixed problems showing child lines in print previews if the lines option was changed with the preview open.
  • Really disable click-to-rename if the general option is unselected.

September 22, 2010 - Release 1.3.4 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work.

Updates:

  • Added ico and gif as valid tree icon types.
  • The French GUI translation has been updated, but the documentation translation is still out of date.
  • Minor updates have been made to the German GUI translation.
  • The Windows binary is now based on the Python 2.7 library.

Bug Fixes:

  • The list of windows in the Window menu is now properly updated after saving a file with a new name.
  • Fixed a bug that could cause spell checks to hang.

June 24, 2010 - Release 1.3.3 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work.
  • German is the only GUI and documentation translation that has been updated so far.

New Features:

  • A new text search function has been added to the help viewer.
  • The keyboard shortcuts for scrolling the right-hand views (Ctrl+Page Up and Ctrl+Page Down) now cycle through the Data Editor view pages if there is no more room to scroll

Updates:

  • The Linux system requirements have been updated to indicate that Qt 4.4 or higher is now required.
  • The Windows installer now sets the sample and readme documents to read-only status.
  • The German GUI and documentation translation has been updated.

Bug Fixes:

  • The cascading of new windows has been fixed. It now references the current position of the previous window, and it avoids cascading windows off of the screen.
  • The Data Editor widgets are now sized correctly when TreeLine is started in Data Editor mode.
  • The spell checker and the Linux installation script have been updated to avoid subprocess command depreciation warnings.

May 14, 2010 - Release 1.3.2 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work.
  • The GUI and documentation translations have not yet been updated.

Updates:

  • Updated the ReadMe file to cover the new features added during this development cycle.

Bug Fixes:

  • Fixed a major bug that prevented files from being saved at the "Save Changes?" dialog when closing a modified file.

April 8, 2010 - Release 1.3.1 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It may contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work.
  • The documentation has not yet been updated to cover the new features.
  • The GUI and documentation translations have not yet been updated.

New Features:

  • Added a menu command that updates another window containing the same file (without waiting for a focus change). This command also has a keyboard shortcut (Ctrl+W).
  • A menu option has been added to hide the status bar. There is also a new general option to hide it at startup. A hidden status bar is automatically shown temporarily for important messages.
  • A new button on the toolbar editing dialog will restore the default toolbar configuration.
  • A new button on the keyboard shortcuts editing dialog will restore the default shortcut configuration.

Updates:

  • Portability on Windows has been improved. A "treeline.ini" config file in TreeLine's "lib" directory will be used if it's present and there are no config files in the user's application data directory.
  • The Change Selected Data command has been updated to use Data Editor formats for dates and times.
  • An alternate TreeLine executable file (treeline_dos.exe) has been added to make batch command-line operations work from Windows.
  • Additional plugin methods have been added for handling files and languages.

Bug Fixes:

  • Fixed a problem spell checking with older versions of aspell when Unicode characters were present.

October 14, 2009 - Release 1.3.0 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is not feature complete and probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.2.x) should be used for critical work.
  • The documentation has not yet been updated to cover the new features.
  • The GUI and documentation translations have not yet been updated.

New Features:

  • Multiple TreeLine windows with different files can be open within the same TreeLine session. The "Open files in new windows" general option controls whether new windows are used when opening or creating files.
  • A "Window" menu has been added with commands to create new windows, close windows, or select from a list of current windows.
  • The "Window->New Window" command creates a new window with views of the same TreeLine file. Changes in either window are saved in the same document. This can be used to work in different sections of the file simultaneously. For performance reasons, the non-active window does not show the changes made in the active window until the non-active window is focused.
  • Starting TreeLine (from a shortcut, file association or command line) will activate an existing TreeLine session if it is already running. A new window will be opened if an unopened file was specified by file association or in the command line.
  • An "Add internal link" context menu has been added to text boxes in the data editor. It prompts for another node to be clicked with the mouse, then an inline internal link is added with that node's reference information.
  • Added an "HTML directory pages" export option. This will export to a directory structure with an HTML file for every node. A navigation pane on the left side of each page contains links to the node's parent, uncles and children.

Updates:

  • The general options dialog has been split into three columns for better viewing on small, wide displays.
  • Additional export commands and export options have been added to the plugin interface.
  • To avoid a problem on the Mac platform, when TreeLine is run for the first time, it lets the OS determine the window position.

October 12, 2009 - Release 1.2.4 (stable release)

Bug Fixes:

  • Added a work-around for a bug in PyQt 4.6's clipboard support that can prevent TreeLine from starting.
  • Prevent an uncommanded horizontal scrolling to the left of the tree view when changing the selection.

May 5, 2009 - Release 1.2.3 (stable release)

Updates:

  • A new build has been created for use on the Windows 98 operating system. This fixes problems that made TreeLine 1.2.2 unusable on Windows 98.
  • Data copied to the Windows clipboard is now preserved after exiting from TreeLine.

Bug Fixes:

  • Fixed a bug on Windows that showed an error message at program exit if the copy command had been used during the session.
  • In an HTML directories export, characters that are illegal in directory names are now correctly removed.

January 8, 2009 - Release 1.2.2 (stable release)

Updates:

  • Added German translations of the GUI and the ReadMe file.
  • Added a few missing strings to the translation files.
  • Substituted some imported modules to avoid deprecation warnings in Python 2.6. Note that Python 2.3 is no longer supported.
  • The windows binary has been updated to use Python 2.6, and the installer has been tweaked to reduce DLL conflicts.

Bug Fixes:

  • Fixed problems using the tab key to change focus from the tree to the data editor.
  • ID numbers are now properly assigned to new nodes with UniqueID fields in the title.
  • Fixed problems creating nodes with child count fields in the titles.

November 4, 2008 - Release 1.2.1 (stable release)

Bug Fixes:

  • Fix the loss of entered text when the renaming of a node is ended by a command that changes the selected node.
  • Problems with opening files with Unicode file names from the command line or from a file association have been fixed.
  • Fixed problems with JPEG and GIF image support in the Windows binary build.
  • When commands change the tree view, it no longer scrolls vertically unless the current node is not visible.
  • Fixed problems with files set to both compressed and encrypted modes. But note that compressing an encrypted file still does not significantly reduce its size.
  • Problems on the Macintosh platform with detecting the text encoding and with setting the default theme have been fixed.

June 17, 2008 - Release 1.2.0 (new stable release)

Notes:

  • This is a new stable release, ending the 1.1.x development series.
  • This release is only available in English and French. The 1.0.2 release is still available, including German, Portuguese, Russian and Spanish translations. Volunteers to update the translations are welcome.

Updates:

  • The French translations of the GUI and the ReadMe file have been updated.

Bug Fixes:

  • When restoring an auto-saved backup file, TreeLine no longer replaces a the main file with the backup if the backup is not a valid TreeLine file.
  • Fixed a minor bug in the Configure Data Types dialog caused by unselecting a field in the Output pane.

Major New Features Since 1.0.x:

  • TreeLine was extensively rewritten to port it to the Qt4 library (previous versions used Qt3.x on Linux and Qt2.3 on Windows). Benefits include updated widgets and removal of the non-commercial license exception in Windows.
  • Preformatted templates have been added for new TreeLine documents. User-created templates can also be added.
  • The content of right-hand views varies based on the selection and other options. A single selection still shows panes with the parent and its children, but a multiple selection will show all of the selected nodes in a single pane. Also, the Data Output view can optionally show an indented view of all descendants, and the Data Editor view splits the display into pages to speed up display.
  • More commands can make use of a multiple selection instead of just using the single active node. Also, an option can set the sequence of showing and exporting multiple selections to either tree order or selection order.
  • There is a tab on the left-hand view for a flat node list showing the selected nodes and all of their descendants. It is most useful for showing the results of filtering. There is a conditional filter for specific rules and a text filter that searches all fields for a string.
  • Several dialogs have been reworked for usability. The Configure Data Types and Print Options dialogs use tabs for better organization. The Configure dialog also has initially hidden advanced features and is now modeless, so it can be left open while applying configuration changes. The Sort, Export and Print Preview dialogs have also been updated.
  • Navigation through the tree is simplified by new commands that step through the selection history. Also, search strings are now highlighted in the Data Output view for text searches.
  • New dialogs have been added to customize keyboard shortcuts and tool bar buttons. There is also a directory for user-defined tree icons.
  • New file import and export options convert between an outline and an Open Document Format (ODF) text document, compatible with OpenOffice.org and KWord. Formatting is not maintained.
  • A Unique ID field type has been added that is automatically loaded with unique numbers. It is useful for link references.

January 15, 2008 - Release 1.1.10 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is nearly ready to become a stable release, but it could still contain bugs. Testing and bug reports are appreciated.
  • The French translation of the GUI is the only one completed so far. None of the documentation translations have been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages may not be available yet for these libraries in some distributions.
  • If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\<login>\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release.

Updates:

  • Exporting to HTML directories has been improved. The file names are generated from the reference field (were from the first field). Internal links to nodes now work properly in the exported files.
  • When saving a new file, the Save-As dialog now defaults to the top directory in the recent file list.
  • A translation of the GUI in French is now available. The i18n installation file is a separate download.
  • The ReadMe documentation file has been updated to clarify certain usage issues.

Bug Fixes:

  • The other controls in the configuration dialog are updated immediately when setting a data type to be derived from a generic.
  • A recent file list bug that affected the httpload plugin has been fixed. Also, a library need by the plugin (urllib) has been added to the Windows installation.
  • An error message about removing a status bar widget has been eliminated.
  • An error on a Mac when setting the language encoding has been fixed.
  • Various issues with file paths containing Unicode characters have been fixed.

June 13, 2007 - Release 1.1.9 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It could contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.x) should be used for critical work.
  • The translations of the GUI and of the documentation have not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.
  • If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\<login>\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release.

Updates:

  • Initially select the top filtering result in the flat view to avoid issues with a blank selection.
  • Avoid entering rename mode when clicking on a multiple selection in the flat view.
  • Maintain the size of the type icon dialog during a session.
  • Remove the redundant printing units option from the general options dialog.
  • Extensively update the ReadMe documentation to match the current feature set.
  • Add a new file template for TODO lists.

Bug Fixes:

  • Fix incorrect internal references in groups of pasted nodes.
  • Abort a tree rename operation if the new name does not match the title format.
  • Correctly add a field to the generic type when numbering a derived type.
  • Properly update the pull-down choices for a choice or combination field after a configuration change.
  • Enable the conditional type button immediately after defining a type as derived.
  • Remove the page number field from the available file reference fields in the main configuration settings.
  • Properly update the Data menu after starting a filtering operation.
  • Fix an error due to a missing system language setting.
  • Properly handle a file open error due to an unknown field type.

May 22, 2007 - Release 1.1.8 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.x) should be used for critical work.
  • The documentation and GUI translations have not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.
  • If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\<login>\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release.

Updates:

  • Translation of ODF text documents has been added to the command line batch processing options.
  • The environment variable LC_MESSAGES is now checked before the LANG variable to determine the user's preferred language.

Bug Fixes:

  • Problems writing the options file when there are Unicode characters in the username have been fixed.
  • Various minor bugs in Unicode handling have been fixed.

May 3, 2007 - Release 1.1.7 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is not feature complete and probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.x) should be used for critical work.
  • The documentation and GUI translations have not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.
  • If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\<login>\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release.

New Features:

  • Preformatted templates have been added. The File->New command brings up a dialog with a list of available templates. The dialog is also shown at startup if the recent file list is empty. Default templates are installed into TreeLine library directories. New templates can be added to a user templates directory in the user settings location. Template file names consist of a number for sorting, a language code and the name of the template.

Updates:

  • The Save As dialog in Windows is now the native dialog.

Bug Fixes:

  • Problems exporting to an ODF text document when blank fields are encountered have been fixed.
  • Added appropriate error messages to the Linux installer if some installer files are not found.

April 17, 2007 - Release 1.1.6 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is not feature complete and probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.x) should be used for critical work.
  • The documentation and GUI translations have not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.
  • If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\<login>\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release.

New Features:

  • A new command (View->Show Output Descendants) toggles the Data Output child view from showing only child nodes to showing an indented view of all descendant nodes. A button was added to the default toolbar for this command. Also, a new general option can make the descendant view the default at startup.
  • A new file export option converts an outline to an Open Document Format (ODF) text document, compatible with OpenOffice.org and KWord. The title of each node is assigned a heading style at the appropriate level. Any other text in the output of each node becomes normal text under the heading. Any HTML formatting is stripped.
  • ODF text documents can be imported. The node structure is based on the heading styles assigned in the document. Any text under each heading is assigned to that heading's node. Formatting is not maintained.

Updates:

  • New commands were added to the Data Editor context menus that open the Configure Data Types dialog with the editor's type and/or field already selected.
  • Keyboard shortcuts were added for the commands that add HTML tags in the Data Editor. Icons were also added to these commands, so the toolbar editor can be used to place them on a toolbar if desired.
  • The interface for writing plugin modules was improved by adding interfaces to the keyboard shortcut editor and the toolbar editor. Also, a plugin interface to get the plugin's directory was added.
  • Any default text for new fields is added when a node's data type is changed (not just to a new node), as long as there isn't already data in a field with the same name.

Bug Fixes:

  • Fixed a problem with the automatic opening of the last file used if there are no valid recent files.
  • Fixed a crash at file open if the file's owner is not found in a Linux machine's user password database.
  • Issues with the display of text have been fixed if the Qt library being used is version 4.2.3 or greater (the Windows build now uses 4.2.3). These issues showed up when scroll bars were shown and could hide the right edge of the text or the last line.
  • Multiple issues with dragging or pasting nodes between sessions of TreeLine were fixed.
  • If the Configure Data Types dialog is open, it is properly updated after the use of the undo command.
  • The find dialog is properly focused when repeatedly shown.
  • Fixed problems occurring when attempting to print an empty branch or an empty selection.
  • Fixed problems with the export of HTML directories. It now removes illegal characters from directory names created from field contents.
  • Auto choice fields are now properly updated after configuration changes.
  • A missing help menu was added to the Unique ID field format editor.

February 22, 2007 - Release 1.1.5 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is not feature complete and probably contains bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for critical work.
  • The documentation and GUI translations have not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.
  • If you've installed previous development releases, it is recommended that you delete the old configuration file ("~/.treeline-1.1/treelinerc" on Linux, "Documents and Settings\<login>\Application Data\bellz\treeline-1.1\treeline.ini" on Windows) before running this release.

New Features:

  • A Unique ID field type has been added. The field is automatically loaded with unique numbers. Initially, the field's format string is set to the desired first number in the series, with optional leading zeros and optional leading or trailing characters. All nodes of the data type will get an automatically assigned ID. The field format string will always show the next available number, which will be given to the next new node of that type.
  • The node sorting commands have been consolidated into a single new dialog. This provides more options for selecting what to sort (all nodes, branches, children or siblings) and how (by types and fields or by titles).
  • An option has been added to the numbering dialog to only number nodes where the number field already exists. This allows node types in the branch that do not have the number field to be skipped.

Updates:

  • The tree view no longer scrolls horizontally when the selection is changed.
  • The focus behavior of the Data Editor view has been tweaked to allow easier tabbing between widgets.
  • Clicking on a selected node to rename it is now disabled when multiple nodes are selected. This avoids initiating a rename when the intent was to modify a multiple selection.
  • The Insert Sibling Before command has been added to the node right-click context menu for consistency.
  • The Spell Check dialog is no longer hidden and re-shown between each misspelled word, resulting in smoother operation.
  • The Output tab of the Configure Data Types dialog has been adjusted to avoid changing the dialog size when toggling the advanced options.
  • The fonts in the Print Options dialog are sorted case insensitively, and the size of the dialog is fixed regardless of the selected font.

Bug Fixes:

  • Fixed a crash when opening a file with the flat view shown.
  • Fixed problems occurring when the number of toolbars was set to zero.
  • Fixed undo operations for Numbering, data Add Category Level and Arrange by Reference commands.
  • Fixed problems with undo and delete commands issued while a node is being renamed.
  • Fixed problems with renaming data types and fields in the Configure Data Types dialog.
  • Avoid adding duplicate types to Other Field References in the advanced Output tab of the Configure Data Types dialog.
  • No longer allow the Edit Cut command to operate if the root node is selected.
  • Fixed a freeze of the spell checker on some platforms.
  • An empty rectangle that was sometimes shown on the status line at the bottom of the main window has been removed.
  • An occasional problem with the import of generic XML was fixed.
  • The importing of files with bad Unicode characters has been improved.
  • The restoring of tree states when opening a file has been made more robust.
  • Fixed several other minor tracebacks that can show up in a terminal or log file.

January 18, 2007 - Release 1.1.4 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is not feature complete and is sure to contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for critical work.
  • The documentation and GUI translations have not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.

New Features:

  • Previous and Next Selection commands have been added to the View menu. These commands step through a history of tree selections for quicker navigation.
  • When using the Find command with the Data Output view visible, the search string occurrences are highlighted in the output. Note: in Linux, this feature requires Qt v4.2 or greater and PyQt v4.1 or greater.
  • An icons directory has been added to the user configuration directory ("~/.treeline-1.1/icons" on Linux, "Documents and Settings\<user>\Application Data\bellz\treeline-1.1\icons" on Windows). Image files (PNG or BMP) placed into this directory are available for use as tree icons.
  • "Move First" and "Move Last" commands have been added to the Edit menu. These commands move a child node to be the first or last child of its parent.

Updates:

  • Horizontal scrolling of the tree view when the contents are wide is now supported.
  • The non-interactive command line options have been ported from the stable version. These options allow batch processing of file imports and exports.
  • Support for plugin extension modules has been ported from the stable version. The interfaces to TreeLine functions are unchanged, but existing plugins will need to be ported to the Qt4 library. An updated version of the httpLoad plugin is available.
  • A plugins directory has been added to the user configuration directory. This allows plugin installation without elevated privileges.
  • The code required to support GUI translations has been ported from the stable version, but the actual translations have not yet been updated.

Bug Fixes:

  • The problem with cut/copy/paste operations in Windows that produced an error message at application exit was fixed.
  • Fixed several problems with the undo of changes to the Data Type Configuration.
  • A bug that could sometimes add an extra node during renaming in the Tree View has been fixed.
  • Problems encountered when typing a replacement word during spell check have been fixed.
  • A spell check language setting in File Options that is not available on the system is now ignored.

December 21, 2006 - Release 1.1.3 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is not feature complete and is sure to contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for any critical work.
  • Some features from the stable release are not yet implemented here. These include command line options, plugin support and translation support. Also, the documentation has not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.
  • One known bug in the Windows version produces an error message at application exit if cut/copy/paste operations were done during the session.

New Features:

  • A new built-in editor for keyboard shortcuts has been added to the Tools menu. It has two tabs: one for menu commands and one for other commands. Simply press the desired key combination to change the shortcut in the selected entry. In general, printable characters without Ctrl or Alt modifiers should not be used for shortcuts. Although the editor will accept them, they may not work properly when certain widgets are focused.
  • A dialog to customize the tool bars has been added. It is accessed from the Tools menu. This allows the user to select small or large icons, define the number of tool bars, and define the commands included in each tool bar.

Updates:

  • The saving of node open/close states for recently opened files has been ported from the stable version.
  • The restoring of splitter sizes and the active right-hand tab from the previous session has been ported from the stable version.
  • The TreeLine configuration files now include a version number (.treeline-1.1 on Linux, treeline-1.1.ini on Windows) to avoid incompatibility issues with previous versions.
  • The default non-menu keyboard shortcuts have been changed to control-key sequences that function regardless of the focused widget.
  • The incremental node title search has been ported from the stable version. The default keyboard shortcut is now Ctrl+/.
  • The option to add a node when pressing the enter key has been ported from the stable version.

Bug Fixes:

  • Searching for nodes with the flat view active now only finds nodes actually present in the flat view.
  • Problems with drag & drop of nodes with Unicode content were fixed.
  • The Icon Selection dialog under Configure Data Types now scrolls to the currently selected icon when opened.

November 22, 2006 - Release 1.1.2 (unstable development snapshot)

Notes:

  • All of the notes listed for Version 1.1.1 still apply.

Bug Fixes:

  • Fixed a serious bug that caused TreeLine to crash when using a pull-down combo box on a field in the Data Editor view.

November 21, 2006 - Release 1.1.1 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is not feature complete and is sure to contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for any critical work.
  • Several features from the stable release are not yet implemented here. These include keyboard shortcuts for non-menu items, command line options, plugin support and translation support. Also, restoring of tree node open/close states, window split locations and active tabs are not yet available. And the documentation has not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.
  • One known bug in the Windows version produces an error message at application exit if cut/copy/paste operations were done during the session.

New Features:

  • Printing functionality has been added to the development series. It now includes options to print the entire tree, the selected branches, or just the selected nodes.
  • A new Print Options dialog box is tabbed to give easier access to all of the options. There are buttons that give a more logical progression from the options to the print preview and to the print dialog.

Updates:

  • The buttons at the bottom of the Configure Data Types dialog have been changed to OK/Apply/Reset/Cancel. This can reduce the number of mouse clicks.
  • The Configure Data Types dialog now defaults to the selected node's type when it is shown.
  • On Windows, the TreeLine.ini file has been moved from the installation directory to a location under the "Documents and Settings" folder. This avoids problems on multi-user systems and for users with limited access rights.

Bug Fixes:

  • The Configure Data Types dialog now updates properly after copying types from another file, adding numbering, and rearranging by category and reference.
  • Various problems with node drag & drop that could result in inadvertent drops were fixed.
  • A bug that caused the right-hand view splitter to reset to even sizes was fixed.
  • Problems with saving changes to the format of file information fields were fixed.
  • Issues with some internal links and pictures in the help viewer were fixed.

September 12, 2006 - Release 1.1.0 (unstable development snapshot)

Notes:

  • This is an unstable development snapshot. It is not feature complete and is sure to contain bugs. Testing and bug reports are appreciated, but the stable release (TreeLine 1.0.0) should be used for any critical work.
  • Several features from the stable release are not yet implemented here. These include printing, keyboard shortcuts for non-menu items, command line options, plugin support and translation support. Also, restoring of tree node open/close states, window split locations and active tabs are not yet available. And the documentation has not been updated.
  • On Linux, running TreeLine requires Qt version 4.1 or higher and PyQt version 4.0 or higher. Binary packages are not yet available for these libraries in some distributions.
  • One known bug in the Windows version produces an error message at application exit if cut/copy/paste operations were done during the session.

New Features:

  • TreeLine was ported to the Qt4 library. This involved a major rewrite of the code. The previous versions used Qt3.x on Linux and Qt2.3 on Windows. Benefits include updated widgets and removal of the non-commercial license exception in Windows.
  • The Configure Data Types dialog uses tabs for better organization. Many tabs contain advanced features that can be shown if desired. The dialog is also modeless, so it can be left open while applying configuration changes.
  • The content of the right-hand view varies based on the kinds of nodes selected. With a single parent node selected, it splits the view, showing the parent and its children (same as version 1.0.0). With a multiple selection or with a childless node selected, it shows all of the selected nodes in a single pane.
  • A general option sets the sequence for multiple selection to either tree order or selected order. When nodes are selected with control-click under the selected order option, they show in the right-hand view in the order they were picked. This sequence also applies to copy and export commands.
  • More commands make use of a multiple selection instead of just using the single active node. For example, the paste command will add duplicate children under each selected destination node.
  • The export command includes options to export the entire tree, the selected branches, or just the selected nodes.
  • In order to render the Data Editor right-hand view faster, there is a general option to set the number of "pages" initially shown. By default, only one page is shown. Buttons allow toggling to the next page or all pages.
  • There is a tab on the left-hand view to show a flat node list. It shows the initially selected nodes and all of their descendants. It is probably most useful when using filtering commands.
  • A conditional filter selects nodes based on rules for specific data types and fields, and a text filter selects based on finding the given text in any field. Both filters show their results in the tabbed "Flat View" and remain in effect until removed.
  • In the advanced mode, a child count field reference can be added to the output configuration. It shows the number of children at a specified descendant level.

July 6, 2006 - Release 1.0.0

Updates:

  • A new version numbering system is being introduced. This release is 1.0.0, to better indicate its stability. The 1.1.x series will be started soon for less stable development releases.
  • The German translation of the ReadMe file has been updated.

Bug Fixes:

  • Fixed a problem with duplicate nodes showing up when pasting multiple nodes on Windows.
  • Deriving types from other derived types is no longer allowed, since it did not function properly.
  • A problem with saving an encrypted file in the German version of TreeLine has been fixed.

February 16, 2006 - Release 0.14.1

Updates:

  • Exported XSLT files now support field prefixes and suffixes. Thanks to Roland Meister for supplying patches.
  • Added functions to the plugin interface to allow plugins to open and close nodes.

Bug Fixes:

  • Fixed a problem with displaying output from fields with non-ASCII characters in their names.
  • Auto save now works properly with encrypted files.

December 20, 2005 - Release 0.14.0

New Features:

  • Data types can be set to derive their field settings from a generic type. This allows types with different output formatting to always use the same field data. Any changes to the generic's list of fields and field types are automatically reflected in the fields of all derived types. A derived option can be selected when copying a type, or a generic type can be specified from the derived type's "Advanced Configuration" dialog box.
  • Conditional expressions can be used to automatically assign a data type based on each node's content. Conditions are assigned to a generic type and its associated derived types. This allows the automatic assignment of different output formatting or different icons depending on each node's field data. Conditions are specified from each type's "Advanced Configuration" dialog box. Conditions do not have to be set for all types in a family, since, if no conditions are true for a node, the program will select a blank condition over a false one. In addition, conditions that are always "True" or "False" are available to toggle the output format of all nodes of a certain type simultaneously by modifying the condition.
  • By default, the spell check uses dictionaries for the current operating system language. A new option under "Tools->File Options" can be used to specify an alternate two-letter language code for the current TreeLine file. This only works with aspell and with the appropriate aspell dictionaries available.
  • A new menu item, "File->Open Sample", is a shortcut to the directory containing sample template files.
  • A new menu item, "Help->Help Contents", directly opens the "Using TreeLine" section of the ReadMe file.

Updates:

  • When changing a node's type, if the result would have been a blank title, TreeLine now adjusts the new title fields to maintain the original title text.
  • Picture links with relative paths now resolve their reference based on the directory containing the current TreeLine file.
  • When applying a font tag in the editor view, the selection is maintained. This allows multiple tags to be applied.
  • HTML tags in field prefixes and suffixes are now removed in node titles.
  • Exported HTML has been made somewhat more compliant with standards.
  • A new option on the windows installer toggles the writing of installation directory and uninstall information to the registry. When this and other options are disabled, TreeLine can be installed without modifying the registry.
  • Four more sample files have been included. These files provide examples of using TreeLine's features and can be used as templates.
  • The ReadMe file has been updated with descriptions of the sample files, with more internal links, and with screenshots.

Bug Fixes:

  • A problem with saving reference field settings for unused node types was fixed.
  • Formats copied from another file are now available immediately.
  • Node titles from HTML-enabled fields that contain escaped characters (<, >, &) are now properly displayed.
  • Fixed some minor bugs concerning setting the default field value for new nodes.
  • A problem with exporting HTML directories with very recent versions of the PyQt library was fixed.
  • When displaying child data in node output, extra separators between blank entries are no longer shown.

November 1, 2005 - Release 0.13.1

New Features:

  • The number of recently used files listed in the File menu can be set using a new general option. Note that this also controls how many files have their tree states (open/closed nodes, etc.) saved.

Updates:

  • TreeLine now sets the interface language using the LANG environment variable (if set) before falling back to other locale settings. This allows the operating system's locale settings to be overridden.

Bug Fixes:

  • Problems starting TreeLine when the locale's LANG variable ends with "@euro" have been fixed.
  • Problems with the display of the rename edit box when creating new nodes in a long, scrolled tree view have been fixed.

August 29, 2005 - Release 0.13.0

New Features:

  • The TreeLine user interface and documentation have been internationalized. Currently, translations are available for French and German. Volunteers to translate into additional languages are welcome.
  • Number formats can be internationalized. Periods and commas are supported as radix characters, and commas, periods or spaces can be set as thousands separators.
  • A new general option can set printing units to either inches or centimeters.
  • An external editor can be invoked from a Data Editor text field context menu. The EDITOR environment variable can be used to specify the editor to start, or, if the variable doesn't exist, TreeLine will prompt for an executable to set as the default.

Updates:

  • The general option for a number editing format has been removed. Instead, the output format is displayed in the editor. Of course, any new entry with a reasonable format is correctly interpreted (but the correct radix character must be used).
  • Handling of spaces in filenames for the linking fields have been improved. Paths with spaces selected in the file browser are properly quoted (Windows) or escaped (Linux).
  • Page Up/Down commands in the Data Output view now leave a one-line overlap.
  • A Ctrl-C interrupt in a terminal is now ignored.

Bug Fixes:

  • A dummy field has been added to imported bookmark separator formats to avoid configuration problems.
  • Paths in links to XSLT files are now properly handled when not in the same directory as the XML file.
  • Format text is now immediately updated when renaming fields.
  • Problems with undo after complex data changes have been fixed.
  • Errors due to having ASCII control characters in TreeLine text have been fixed.
  • Problems with spell check of unicode characters have been fixed.

May 4, 2005 - Release 0.12.0

New Features:

  • A new AutoChoice field type provides pull-down selection of previously used entries. Any new entries are automatically added to the pull-down.
  • A new ExecuteLink field type runs an external program when its link is clicked. The command to run is given by the text of the field. Or the field's prefix may contain the program name, so that the field's text is used for arguments or file names. There is also a general option to disable these links when security is a concern.
  • A button that brings up a file browsing dialog has been added to the editor for Path, ExecuteLink and Picture field types.
  • Link fields can display alternate text in place of the target URL. This is done by specifying an alternate text field in the Advanced Field Format dialog.
  • Fields from child nodes can be embedded in their parent's output. Select a child reference from the Other Fields dialog. The child data is delimited with a separator string defined as a file option.
  • A new file option allows the removal of line breaks from each output line. This allows other HTML tags to be used to separate the output lines.
  • A "contains" rule to check for substrings has been added to the rules for node filtering and selective type changes.
  • The right-hand view that was previously used is now selected automatically on startup. This can be disabled using the general restore window geometry option.

Updates:

  • Toolbar and tree icons are loaded from individual PNG files instead of from an XPM file. This improves icon quality.
  • Some features from the dialogs for data types and fields have been moved to advanced dialogs for simplicity.
  • Unicode characters are accepted for first-letter tree searches.
  • All calls to the eval() function have been replaced for improved security. Thanks to Roland Meister for providing patches.
  • The HTML version of the ReadMe file is now exported from the TreeLine version.
  • Spaces are permitted in Linux command line arguments.
  • Qt command line options are supported.
  • There have been several minor improvements to the plugin module interface.

Bug Fixes:

  • A serious problem with deleting the last child under the root item has been fixed.
  • Some keyboard shortcuts for incremental searching have been fixed.
  • Open nodes are restored more consistently when opening files.
  • A font issue on early versions of Qt3 has been fixed.
  • The search for dtd files linked from XML files has been disabled to eliminate errors.
  • Visibility problems with long unbroken text lines in data editors have been fixed in Qt3.

February 16, 2005 - Release 0.11.1

Bug Fixes:

  • Fixed a compatibility issue in the toolbar code that would prevent TreeLine from starting when PyQt was built against the newest versions of the sip library (versions 4.2rc1 and 4.2rc2).
  • Problems with editing the data of combination field types have been fixed.
  • A bug in the plugin interface for setting field formats has been fixed.

February 4, 2005 - Release 0.11.0

New Features:

  • Icons have been added to the tree view. The icon assigned to each data type can be changed from the Configure Data Types dialog box. There are several generic icons available within TreeLine, and plugin modules can be written to add additional ones. A general option to disable the tree icons has also been added.
  • A command has been added to the data menu to copy type formatting from another TreeLine file. All types from the chosen file are copied. Any types in the current file with a matching name are overwritten, but types with unique names are retained.
  • Functions to import and export generic XML files have been added. These routines do not have much intelligence - each XML element becomes a node and each XML attribute becomes a field. This lets TreeLine function as a crude XML editor.
  • An editor height parameter has been added to text fields. This allows the number of lines in the data editor to be set for each field in each data type. As a result, the LongText field type becomes redundant and has been removed.
  • A field parameter has been added for fields that are required to be filled. Fields with this parameter set are marked with an asterisk in the data editor view.
  • Another field parameter has been added to allow certain fields to be hidden in the data editor view.

Updates:

  • A fair amount of TreeLine code has been rewritten to improve efficiency and to ease future changes. Users should see improved speed with large files, especially noticeable when repainting during editing.
  • TreeLine now requires Python Version 2.3 or higher on Linux systems. The previous version of TreeLine will remain available for those stuck with older versions of Python.
  • HTML tags are now stripped from node titles in the left-hand tree view if the fields are set to output HTML rich text.
  • The plugin extension module interface has been expanded. Several added functions deal with node and field formatting. There is also a new callback trigger called whenever a node's data is modified.
  • A user plugin directory can now be specified in the configuration file. This allows plugins to be installed without root/administrator access.
  • An option has been added to the Linux installer to allow a different documentation directory to be specified.

Bug Fixes:

  • Fixed a drag-and-drop bug that wouldn't allow nodes to be moved instead of copied in the Linux version.
  • Fixed the output of fields that include dashes and periods in their names.
  • A problem retaining the font setting for edit views has been fixed.
  • A rare startup failure due to problems with the encryption engine has been fixed.
  • A rare problem with foreign language encodings has been fixed.

November 8, 2004 - Release 0.10.2

Bug Fixes:

  • Fixed a major bug that could prevent new nodes that contain date or time fields from being created.

October 28, 2004 - Release 0.10.1

Bug Fixes:

  • Fixed a major bug that caused TreeLine to hang when attempting to open a recent file that no longer exists. This bug also caused startup failures if automatic opening was enabled with nonexistent files.
  • A bug in the plugin interface's getRootNode function was fixed.
  • Fixed a problem with file encryption on 64-bit systems.

October 15, 2004 - Release 0.10.0

New Features:

  • One of the fields in a data type can be tagged as a reference field in the field-type dialog box. It defaults to the first field. This field is now used as the reference for the arrange and flatten by reference commands. It is also used by the new internal link feature, described below.
  • There is a new internal link field type. It creates a clickable link in the Data Output window that selects the next node with matching text in its reference field. If link or reference fields contain multiple lines, each line is treated as a separate link or reference, respectively. If desired, the same field can be used as both the link and reference fields, so that clicking on a keyword in one node's field selects the next node that also has that keyword. If exported to HTML, the internal links function as links to page anchors, but, in cases with duplicate references, they only find the uppermost reference on the page instead of the next one.
  • File encryption has been added as an option to password protect TreeLine files. Individual files can be set to encrypted mode from either "Tools->File Options" or from the save-as dialog. There is also a general option to set the default for new files. The encryption uses the SHA hash function as a stream cipher - it should be fairly secure.
  • A default initial field value can now be specified for a field type. Any new nodes get this value for the given data field when they are created. Also, in a date or time field, an initial value of "Now" can be used to get a time-stamp of node creation.
  • An interface to plugin extension modules has been added to TreeLine. This allows extensions to be written by various coders that provide functionality needed by a few users without adding bloat to the main application. Currently available plugins are listed on the "Downloads" page of the TreeLine web site. The plugins are installed by copying their Python file (*.py) to the plugins directory ("<prefix>/lib/treeline/plugins/" on Linux or "TreeLine\lib\plugins\" on Windows). A list of loaded plugins can be found using the "Help->About Plugins" command. Information about writing plugins can be found in the "plugininterface.py" file in the TreeLine source code.
  • Options have been added under "Tools->General Options" to set the fonts used for the tree view and for the right-pane editor views.
  • When printing, TreeLine will now avoid breaking pages between a parent and its first child. This behavior can be disabled by changing the "Keep first child with parent" print option.

Updates:

  • Unicode text handling has been improved. Unicode can now be used in TreeLine file names, node data type names and field names. Also, imported and exported text files now use the proper encoding based on the system's locale settings.
  • A new sample TreeLine file with internal link fields has been added to the documentation.

Bug Fixes:

  • An occasional problem bringing up the field-type dialog for file-info fields was fixed.
  • Avoid showing an oddly-named internal file-info data type in the dialogs after the file-info formatting is changed.
  • The option to restore the view state when opening recently used files now works more consistently.

September 16, 2004 - Release 0.9.1

Updates:

  • Unicode text is now supported without requiring modifications of the Python sitecustomize file. The sitecustomize change has been eliminated from the installer.
  • The compressed ".trl.gz" extension has been added to the file open filter.
  • Information about the new TreeLine mailing list has been added to the documentation.

Bug Fixes:

  • A bug that prevented XSLT file export under Linux has been fixed.
  • Special characters in bookmark URL's are now properly escaped during export to XBEL format.

September 9, 2004 - New Mailing List

List Information

A mailing list has been created for users to discuss anything and everything about TreeLine. This is the place for development discussions (from roadmaps to feature suggestions to beta testing), release announcements, bug reports, and general user discussions (from new uses to tips & tricks to configuration samples).

To subscribe, go to <http://bellz.org/mailman/listinfo/treeline>. I expect this to be a low-volume mailing list.


September 1, 2004 - Release 0.9.0

New Features:

  • Import and export of bookmarks in both the Mozilla HTML format (Mozilla, Firefox and Netscape browsers) and the XBEL format (Konqueror, Galeon and Elinks browsers) have been added.
  • An new option exports a portion of a tree to another TreeLine file.
  • Command line options have been added to allow non-interactive file importing and exporting. This allows automated runs to be scheduled. For more details, run "treeline -h" from the command line. If using a windows binary, output is supressed, so it must be directed to a log file ("treeline -h > log.txt").
  • Options have been added to work with compressed TreeLine files. Individual files can be set to compressed mode from either "Tools->File Options" or from the save-as dialog. There is also a general option to set the default for new files. Thanks to Mathieu Girard for contributing code.
  • A field reference to show data from any ancestor node has been added. The closest one with a matching field is used.
  • Fields containing file meta-data have been added. These include file name, path, size, and modified time.
  • Headers and footers for printing have been added. They can contain the file meta-data mentioned above. The headers and footers can also be shown in exported HTML files.
  • HTML character formatting tags can be automatically added to text using a new data editor context menu. Available tags include bold, italics, underline, size and color. Note that the fields must be set to display HTML content for this to be effective.
  • A new general option makes new fields default to HTML content. This allows for easier display of HTML formatting. This option is not enabled by default, since it does not preserve carriage returns and does not show non-escaped "<", ">" and "&" symbols.
  • When opening a recently used file, TreeLine will now restore the states of open and selected nodes. If desired, this feature can be disabled with a new general option. Thanks to Jan Hustak for contributing this code.
  • The toolbar has been split into two separate bars (general commands and node commands) that can be shown and moved independently. Again, thanks to Jan Hustak for contributing this code.
  • A new auto-save feature stores unsaved files with a "~" appended to the name. The backup files are automatically removed when the file is saved or TreeLine exits cleanly. The auto-save time interval is set in the general options.
  • New key bindings, set to Shift+Page-Up and Shift+Page-Down by default, scroll the current right-hand child view.
  • The XSL export command now prompts for the name of an optional style sheet (css). This name is stored in the TreeLine file as the default for future exports.

Updates:

  • There have been some general improvements to the handling of imported and exported files.
  • File option settings can now be restored with undo/redo commands.
  • The README documentation files have been revised to be more organized and readable.
  • More sample files are provided. See the "sample_*.trl" files in the "doc" directory of the installation.
  • The Linux install script now includes an option for a temporary build root directory.

Bug Fixes:

  • The up-arrow key binding did not work as expected in a deeply nested tree.
  • Some problems with auto-completion on data editor combo boxes have been fixed.
  • Problems in windows with some option spin boxes accepting new values have been fixed.
  • Fixed a compatibility issue with older versions of Python.

June 23, 2004 - Release 0.8.1

Bug Fixes:

  • Fixed crashes that sometimes occurred when switching focus or views after renaming a new or existing node.
  • Fixed a compatibility issue with older versions of Qt (3.0.x).

June 3, 2004 - Release 0.8.0

New Features:

  • Added undo and redo commands and an option for the number of undo levels that are stored.
  • Added the ability to show parent and grandparent data within a node's formatted output.

Updates:

  • When changing node types with the menu, check marks now show the current type(s).

Bug Fixes:

  • Fixed crashes caused by right-clicking in the edit area or using the Alt-Tab key combination when renaming a node title.
  • Fixed problems occurring when several similar data type names were defined.
  • A rare problem with the installation script choking on image thumbnail directories was fixed.

May 18, 2004 - Release 0.7.3

New Features:

  • Spell checking of the tree data has been implemented. This feature requires an external program, either aspell or ispell (see the System Requirements section).

Updates:

  • The size and position of the print preview window are now saved.
  • Added an option for including only open descendants when exporting tabbed title text.

April 1, 2004 - Release 0.7.2

Updates:

  • For faster keyboard navigation, the left arrow key now closes the selected node's parent if the selected node has no children or is already closed.

Bug Fixes:

  • Some tree item drag-and-drop bugs have been fixed.
  • Problems when some dialogs were closed using the escape key have been fixed.
  • Fixed some rare Linux install script problems with some systems.

March 9, 2004 - Release 0.7.1

Updates:

  • The open file dialog now uses the directory of the most recently opened file as its initial directory.
  • To improve performance, some unnecessary refreshes of the right-hand view were eliminated.

Bug Fixes:

  • Fixed problems with repeated uses of the search dialog command and the command to set the types of descendants.
  • Fixed problems with the data editor view in the mode that does not show children.

March 2, 2004 - Release 0.7.0

New Features:

  • Added prefix and suffix tag formatting for groups of siblings. These tags allow output to be formatted in tables or with bullets. See the "sample_table.trl" file for an example of table formatting.
  • The size and position of the window and its splitters are now saved at exit. A new option will disable this feature if desired.
  • An import filter has been added to open Treepad files (text nodes only).
  • An install program has been added for windows.

Bug Fixes:

  • Fixed Linux install script problems with certain versions of Python.
  • Fixed some text exports where improper end-of-line characters were used on windows.

November 19, 2003 - Release 0.6.2

Updates:

  • Cursor changes were added to indicate time-consuming operations.

Bug Fixes:

  • Eliminated inadvertent scrolling of the tree view when editing in the right-hand view.
  • Fixed problems encountered when the Linux install script attempted to delete old TreeLine directories under certain versions of Python.

November 7, 2003 - Release 0.6.1

New Features:

  • Incremental searching has been improved by adding keyboard commands to repeat the search forward (F3 by default) and backward (Shift-F3 by default).
  • The sorting commands now contain options for reverse sorting.
  • The importing of text files has been improved. Imports of plain text were added, with items for each line or each paragraph. A prompt for the type of text file was also added.
  • A new option setting avoids going into title renaming mode when adding a new node. This also keeps the selection unchanged as nodes are added.
  • Dragging files to the TreeLine window in order to open them is now supported.

Updates:

  • The rename command was added to the tree context menu.
  • The right view now scrolls to the top when the item selected in the tree view changes.
  • When opening the configure types dialog, the current tree item's type is initially selected.
  • A TreeLine formatted version of the ReadMe file has been added.
  • An install script was added for Linux and Unix systems.
  • The windows build now uses Python version 2.3 and PyQt version 3.8.

Bug Fixes:

  • Keyboard commands are disabled during tree rename operations to avoid unpredictable results.
  • The right views now update properly when the parent of the selected tree item is closed.
  • Problems involving copying with multiple nodes selected were fixed.
  • Problems with drag-and-drop on some Linux systems were fixed.

September 16, 2003 - Release 0.6.0

New Features:

  • The three right views are now divided into upper sections for the selection's data and lower sections for the data from the selection's children. View commands can be used to hide the display of children if desired.
  • Keyboard shortcuts can be customized by editing the TreeLine configuration file ("~/.treeline" on Linux, "treeline.ini" on windows). Any configuration files from previous versions should be deleted when installing TreeLine 0.6.0.
  • A new command will quickly set a node's data type.
  • Parent nodes will automatically open and close when found with the two search methods, by typing the first letter and with the "next sibling" keyboard command. This behavior can be disabled with a new general option.
  • Typing a lowercase letter will move forward through the visible tree items that start with that letter. An uppercase letter will move backward.
  • Tree items (including those not visible) can be incrementally searched by typing "/" followed by the search string.
  • The Find dialog box is now modeless (the program may be operated while the dialog is open).
  • A "Choice" field type has been added that allows selection from a user-defined list of strings.
  • A "Combination" field type has been added that allows selection of several items from a user-defined list of strings.
  • Several linking field types have been added, including "URL", "Path", "Email", and "Picture". Clicking on a "URL" or "Path" in the output window will open the link in an external browser. The "Picture" fields are shown on in the output window for certain image formats.
  • Pull-down editors have been added for the "Choice", "Combination", "Date", "Time", and "Boolean" field types.
  • A font selection for the data output view has been added to the general options.
  • New printing options include setting the print font and setting a default page size.

Updates:

  • Configuration information is properly saved for unused node data types.
  • TreeLine is now more keyboard-friendly in many ways.
  • Window captions now put the file name first.

Bug Fixes:

  • Fixed problems with data edit view resizing (on Qt3 only).
  • Fixed file corruption problems caused by the use of HTML characters in the field format's prefix and suffix areas.

March 14, 2003 - Release 0.5.0

New Features:

  • When configuring node data types, a default type for that type's new children can be set.
  • An added form of HTML export creates a directory structure with a separate page for each parent node.

Updates:

  • A shortcut (Ctrl-B) has been added to the command to insert a sibling node.
  • Icon files have been added to the distribution files.

August 26, 2002 - Release 0.4.2

Bug Fixes:

  • Fixed another (oops) incompatibility problem with Python 2.1.x. This change is also not needed when using Python 2.2 or higher.

August 23, 2002 - Release 0.4.1

Bug Fixes:

  • Fixed an incompatibility problem with Python versions 2.1.x. This change is not needed when using Python 2.2 or higher.

August 22, 2002 - Release 0.4.0

New Features:

  • Data fields can be formatted as specific types, including short and long text, numbers, dates, times, and boolean values.
  • Individual fields can be specified as plain text or HTML. Line breaks are preserved in plain text mode.
  • Text prefixes and suffixes can be assigned to fields. The extra text does not display for blank fields.
  • Options for spaces between nodes and allowing HTML in formats are saved with the file instead of being global options.
  • Node titles are truncated after the first line of text data.
  • Node types can be set conditionally.
  • A new option allows the number of lines in a long text edit field to be set.
  • Missing fields can be updated from a separate file by matching unique values of the first data field.

Bug Fixes:

  • Clicking on an unselected current item no longer initiates title renaming.
  • Rare problems with opening or pasting content with complex text strings were fixed.

May 28, 2002 - Release 0.3.4a

Bug Fixes:

  • A fix of the Windows binary only. Fixes major problems by upgrading the library version to PyQt 3.2.4.

May 15, 2002 - Release 0.3.4

Updates:

  • TreeLine has been ported to Qt 3.x. It now works with both Qt 2.x and 3.x using the same source code.
  • The binaries for windows have been updated to Python 2.2 and PyQt 3.2 (but are still using Qt 2.3 Non-commercial).

Bug Fixes:

  • A selection bug that caused problems with the up/down and indent/unindent commands has been fixed.
  • Errors in the initial user preference file were fixed.

March 6, 2002 - Release 0.3.3

New Features:

  • Multiple nodes in the tree can now be selected using the shift and control keys - most commands will operate on multiple nodes.
  • A new command will copy node titles as text, not just as XML.
  • Options have been added to print and export only nodes which are open in the tree.
  • A new option allows the numbering command to start with a number greater than one.
  • A tool to remove the XSLT reference from a file has been added.

Updates:

  • The command to arrange a tree by reference has been made more robust.
  • Errors when opening files are now handled better.

Bug Fixes:

  • The export of Unicode characters to HTML has been fixed.
  • A problem with the set data types dialog under Python 2.2 has been fixed.

January 7, 2002 - Release 0.3.2

New Features:

  • Unicode is now supported in both data and format strings, allowing the entry of foreign text and symbols (note that there is an extra step in the Linux installation to enable this feature).
  • A new sort command allows all nodes of a given type to be sorted.
  • A preference setting now controls the initial state of the right-hand view.

Bug Fixes:

  • Fixed crashes caused by tool-bar use while renaming a node.
  • Occasional problems with deleting or renaming fields have been fixed.
  • Fixed crashes resulting from very large data edit views.

December 12, 2001 - Release 0.3.1

New Features:

  • Parent/child lines can now be added to printouts for better tree visualization.
  • An option was added to compress the format into a single line of output.

Bug Fixes:

  • Problems with sorting full branches were fixed.
  • Fixed improper indentation on an HTML export file.

November 27, 2001 - Release 0.3.0

New Features:

  • Added filtering of descendant nodes.
  • Added a data node numbering feature with both outline and section styles.
  • Added a feature to export the format to an XSLT file.
  • Added table of contents links to the help file.

Updates:

  • Improved the handling and editing of long data strings.
  • Improved the handling of HTML tags in data formats.
  • Improved XML parser error handling.

October 23, 2001 - Release 0.2.1

Bug Fixes:

  • Fixed a major bug that caused errors when new node types were created.

October 17, 2001 - Release 0.2.0

New Features:

  • TreeLine has been extensively rewritten to include database field information in each node.
  • The file format is now XML.
  • Added additional views in the right-hand pane.
  • Added options for data formatting and manipulation.
  • Added more file import and export options, including HTML export.

Questions, Comments, Criticisms?

There is a mailing list for users to discuss anything and everything about TreeLine. This is the place for development discussions (from roadmaps to feature suggestions to beta testing), release announcements, bug reports, and general user discussions (from new uses to tips & tricks to configuration samples).

To subscribe, go to
lists.berlios.de/mailman/listinfo/treeline-users. I expect this to be a low-volume mailing list.

If you do not wish to subscribe to the mailing list, I can be contacted by email at: doug101 AT bellz DOT org

I welcome any feedback, including reports of any bugs you find. Also, you can periodically check back to treeline.bellz.org for any updates.

TreeLine/doc/INSTALL0000644000175000017500000000217711651514477013063 0ustar dougdougTreeLine Installation Notes Extract the source files from the treeline tar file, then change to the 'TreeLine' directory in a terminal. For a basic installation, simply execute the following command as root: 'python install.py' To see all install options, use: 'python install.py -h' To install TreeLine with a different prefix (the default is '/usr/local'), use: 'python install.py -p /prefix/path' The old '~/.treeline' configuration file should be deleted or renamed if this is a major TreeLine upgrade (such as 0.5.x to 0.6.x) or if editing of new keyboard shortcuts is planned. To install language translations for TreeLine, download the "treeline-i18n-..." tar file that matches the version number of the main TreeLine file (ignore the letter suffix on the translation version number). To install the program and translation files simultaneously, extract the translation file from the same directory where the main treeline tar file was extracted and run the install command as described above. If TreeLine is already installed, extract the translation file to any directory and run the translation's install command as described above. TreeLine/doc/doc_filter.png0000644000175000017500000005152011651514477014646 0ustar dougdougPNG  IHDRwiPLTEER ;U?I 2kb')&y'*-+,*,R+*_/35%'').5;56489&3*1:F6:<3=N11??(:>@7:a/5:;X25?A>;BI>CE@>a7EU*Ie9K@EGD;I`+ >M[?D+*GOVKOQIU:@Sh?UdFQhHFKSZHVaRTQQPjQ[6/diSWZPX_LZ`NWmD\nHZpXVZR`9(i}MORWs9hhORQT=@R`f8]_uZeqRis?jOjZhn_fm[\Z^Viyhm84zgeiegdZ3ee~={jli\oVqapugntnlpef^t~hl(omhshj_U]xX{hnhw}}pjfvnu{Jmtutytvsrv]~i}o~pal{}z)O}wyg{~wy}Jm\[vu}pycǖv}Ɣˡ˨ҿƭԠʮդĪƹ˿¿˾ݻҸ-b pHYs  tIME 4 & IDATx\GxV%!s0$%8 xھ;Z[sV/f;K8g1{sY{;f.i22-muʝ :1[C?[k'~k7 ^m㕯vTw?*u_wn{k![už 4W{8ձk^Wr6ku_GHolƔuC]fЭ!9uW0,+2.nq\#>/"ޖض`w s;!B{ܶnKTZzgע+!KᡎV]|%V2ܰ/_Uگu(Mj]5CM+k]=5,n0,. }Ef9UW6A9{mh=/Gf޷!!dsAyxػa [bS/7!rk>ϼpF]S?sW}aL΢zXi-ȳbǪ4M _]{Ua[u [!ܷvNeʶ{WYkw߰)|ljgTgCoWſk HoaMcݬ;WwB}mWJ><z!\MO/gY޳XyHlmCҽ=л_xYI#3M4UM4,=H$F*l\xp&Wts5l™ʼnsU5B)GR&K'gFv󔓫 K+[f+|˃g~P-DZP=tutoaUk=9{f҉PpC$IuStt&\)kզ݅]Ltd!r#_&9i(CTw:/Z->s9KW_,Dښ@Fvku׮҃k:_NMHg&~mV,}GN<޲/o͠D˦yƾ{UB[Rᝦ&gSvk_pxp276Q,P mulM0: io;y 9yͭ24{%z6qmv+W/--%κqu:}åkHfS4f /]J)~/]p*mhd|<>b>G(-i皾E>wRBLFϦ4^ӗҩh8r{ȮN)on^qtE.\8/&ܸyjp T]NPtr)O/ݸ GS%~R><TX6m#c Ն__rҮ&SooNs'?J@]KP+ 2;ЁPX>' SҥSUDcMS|n7>J~MI KW\S:6}c,T%\Brm<5~ct>K޸6}FOǧSO߾r}+'nё˗oܸ|y"Q?'aBO0Suѩ߼6ծge?]{4#7'N?]O"S5 g&P; =V73tabjo,Pga$oP=w$zOf^Jz&UUaڟ;ObO>X:HP>} KŨF#K4YWoL+u7)&J^͛W.z\Y#7&G"1#{]rD\NR+?rܛo_E[njCS0dN>OZc e N E&T ܑT#}} )?rHÑ}T L΄%jCn=Buzѣ[?jҮ^CZxW oaK=~|kN|ŋZzڍO>~*T#G? %K7y3^_W\w)Tp\f7 RE6T׏=z"EW?z}tt͏'SNՋ璺:q?$ՉT*j@OB~$S$on"^;xp oܜNz(OR޼Z'O^?}{:fT#P]cDKk$?uf2j@iC%w̙3)T#}G~̵HFG|tՌտhN;=~tj gpv&2oǟ\aKOC|?R{>@ %}cuG3_7O?|?DNA)sꗠ" ׿7(#7gvub~#KZ )vퟞ<ɹ'_n GFBПd4iWi*7ɦ1mjkc#- -TgǓ̡C=/?Hi՛~)>x]?$뱰L5UFBڜg6gjܮH$~8=.QŸȬg6ڸ%mܒ*-H&qMnP#Oua  F5I7zG(+Bdc`J'ՖuO>kzr'գWVBmjdX5wWz2t9^~5dL}FI~rc~O<_~q|*Nz{q=q^UJIƕzoOl[3]j5 }$1%_Z)T$2_9~vڂT-GlIR=m/ѵ4TuQ7Ė26ez]u)PnmUrӨfY,+K^MR8+|y&ٻ=U`T7)(JuRآ;]U/S5V7"j z?[Q ^R'1._iv$UoEO,,3`j3: k%*2oHj3-$3ydw4#\/&6ֵ TTU=E9X[G+*n5;ՋU;tMf} QUzTYb]eDI0[Sy{-ALYO'[&Ujwp;A+Aر?BwG?o}+IPRderK d>sK3d=/)2sEhŨힿZPZHA3 =׃m-\FS *jv5gTxt&زZNR|YW_":3uPV)TV d@p< lK2_PJ$Vՙ|GeeRuLBJ%wPfUƨ̗e4<_l0lHJ I,W"rZ*D51c+cq ;M'f ӐvXY*~ $B[l1_r_ϭH?0CŊZ,nlJPJ,a%' rʅM0RAOr{_ŗQwvu8T)RjcI2W+,\zWCU/cij`<ۈퟃ QEiuUZxD5"]AcE`\t }h&t S}VTGET8U5D1΃S}!T?'~oSf(]: wR4IR5Bh΢Lϣi7,*>-U5i9A5HqK!UJM[/)G*XT6h%%'Ws2KN^?]RsEݓ)S)))i?]i@;gѩX8ܜ%9-[R, @@@ T%*|$瑀% <*g>I S5lA<{ ,ؿM?G\t5G}qL+W;j`T׋=)TSMU_EuPhƂ8U}MA% zT蚶ZSSTҝYھ#FR_ǯtEzCcc#J+t5m9R{U}E{e*ʧB_߁+kҢشZJ|p9Zu}KY^Cg+꧗̎_)CZd,: g~ʾC0A  ETSW{pjf|Ւ==NC؁h;S54.ӵ7 <[DzV-6Ǘ!鹨J)Qn~jjjQ9~bf!è.o`erUŦѢ-8u Mf^aiYnsaya1G* ՔVՊ2},vbYAe]N]45pynZ,Zk>aZ;I5I]mj:|SRNb`y"BꁼQ : xj&_=lZjAAg(29:ފrsmC{>X\}F~QRQPb%b+u*OކXD~Ȩ* sU[˦*R.րÞe'`r?dTWՀAϡ )>=t2_mPrӉHtr8$Wr/˖.'/.^$7WS+LWUSQq1PYfK-5֦ <ˡB~qMQ , 'ՀՋH6%\2CNyǮx~IR>; 2KDt?*d,6?(;lI :%lgYN&H#e.p- UԳ $c8 L~bPm+cQ*BVcM9@;9э 8[m4P@nUN9-3  m]XoE=Tߍ剅]so+O:W‚6.[<]M$D+8dX۫>Z" o_TtDZ]lGkl톪R2}KN۝aw6Z̄v/D \QRS݌Lyeex%V]Iuz 碊WI_Mkjoϕ(Q7TsJ;Lu_ccFu1FI_5ώ_qeMz<Аo9|2 9ܐQ]bJju=YnTW~GCx"*[?.$~5CQ]T#oZ˩z^7kmyLZ5]UՉIͮ[R#> -Ib|K8`wV)qszΨ̷pS,FAV@iSI`)BNʜ,ESR-Xb.oIT5ߒjHUFqK@ z|KRU5*/P^Nf6QYʹʘK TZ5˜j̷4@( ,-t,& 01 / $1ch. EѨ.u nQ_Dj9"da|U)[9HY,C@)@WI5KL5[BT'4IP iApXCZ[*8`6eXkTj̷䮅 uǩ|䁏wJ I(3xsJVM,IZ4÷tX)(CbNSx&gAQv |ټBUJRHMuPJ+6B Ee% bܗxi RLW)T"+ۉ 4FVX|ՅRêʫ'uj…)+-&N5]4Ar>.Z龥[j`ޡM?5]]6%(!g1 (}Q)2걫1ߒl2P/7E(bӨX@s9Q: -Sp1HZ(=hI#1MIy}8Un oF{Fn52fv"QdQ"Jl+"[ (i@<P,0Tin^ ͒?]WV 14]] ݀3T:M6h*^Gi>9hSN1A U2@eP]x/y@RW9"ƐzS~L#Hw튮?ey)NCOpxG)b8V`UBZ^ Av&:ryKieV*N?Jr/arCT3걫[ koI4*ڌw*-i&ͷbߒ&ZL}Kʮj1qj՘o闚RR۸%mU6nIoIoIj%MW5aW5ߒ[[tu}Khܒ+[R]|KI[2hW4ߒ [R]|KjoIvU-Ѯ.ȷ>qToI"<xۉ7<bs)`8Ս[r)px-IvN9'b+%4ebZyPn—urL̄Ob̥fUPUݸ%瀃P>Kkܩ<%`V.^@JˆnnOK5Zx Um=ۘ'Sey#Ba]$#Tqpq^S(EWټ1Wj`؁X} UG#]ӷ\.R6 G haR.. ~:6-ݳB:P[J _L]]yD.QѸݿծxҳT!qK~BqKDruؘj]Ս[r"/݉@%G%__̡ëQPUՍ[+uu⒳C? 8 e:&@8TҝQ] G+uH SJћn;XWD䊬]]~T/~yZHXWIM;9QUi*OFwŠ-T;s剭j$(4&{Fd+.86L5 q -yAHl>j,'M8Lj 'TvuX%7󢟓)dJV'Z` S|l-N ijAT:(JAC$:ڭQ LZ UtکR0$ l$ ŮVh%,lE,1oUJZxVR^ULzБD$FQYl9Vq.Dջ/=EIEc mQBoH"*[H iX=J焒OĻ@u̒|ǡUf"!or),]K:lF=v[<OXRT4niJϮThWn"ΚQ=U[R|KF-8Ʌ0-9Hc0ո1F몊 ٠j켳L_v HRl "@g]x2l1b _gz`lF<͛,F\x;@ [V]OL U#LuAleVSvZU̐DB&h@5g@%8%xMFou-qJlAM.>;B]_6KV]p='a~) }:զW$ՇTTgW5|KՇT4UfW5]OTOirJR]UͮjT5QUQhW?_]FuyUYS?fW5ܮ~hjvUenW?fW5]ըjvUUfW5QFUj@ p^IJ_/qۄ"MI;X!=n]7v5:"$bYNg r6G>L$\[Mxl*/x U`3?ە]y<~ DO6}V˻$T&*^K ,8?>_dgjZ5\7fޟ* =? I֏tU}TvuX5T%b*BvBP~Fn#1yfY@] dy0@Z!~MXWx^$2iXJ>%ZZKX[-Y +Guy-hnЁg nS'ۣPeޕVkKdZBˤg@U0Jg<*WUUjCkWbT7L3E~6`Nso%̶Tm xȺXJQ\كުV+--P:qzPt,ʂ۴FH ꟒63/ko8UIw+i'M%S rHus1֕Eg?2g]4J2q+_U*-v4+RD_r7K S|Al ܭn,7. iEs}[pU %ywi!RC-(FLA'%G3b/X}|6$j`^UX}vNbI9qQu y\!"N}v5bqLD6UR|a+%:,'Z-dl6!]sbT9՚LJJҫTn53Y%|&A"ؙnqbtOY@ R1&ɗT_, oBemc#ItH4)s >ORqGk %ͮ.om0}e!ĥX"aK(|KjU~=%mu/vC?*{b"*U3> t}"4L:cu3 > vu#]I #8 vK,R 6:f/}S+*-#CsPr&WipU$4V%[mf"8dD6&l<F-^ ˘a6iKlU6%R*q4z pyViHkaa7)@V}(T,F>Vc33gU6,d("YHSPvUhB-vy+ @/`Du> U2Kf! FL)=2U&h*YaX =Sy\xOl^gvSR#K:H?8y Wq/ 32j̢xӋPB"ۘrkT6&*`'Aj`#\kQڊ*B/*g0Y~vc UEH"nȂZ'o oŸ]?Sd .d%.6_l<}8/1ÚP5rkhTUEOwO5jG|Y-T;yx^_?`?T#Bz!YԅZb Cu&P5҄PN[5.IoJ;ćjgDmTYqm0xlvZãoKJrGT @*0<Zt&ZU騆UԖdhTo#3ᡩKn ~N)>$_oYvS 98NݚR.VS}?ƝI]o9>uT# {Z i{5mw|E'*{AHFR_Ƣ5vepNȱ胐{9|w2CŗBD;Y3]wr-#mgjPYF0z&6+S=^XKBcÛB5rskrFj4`p˦pc(@d}*=`q2A ɞ)Ʀ܂m#3 yC杋Iubkgt<2Ұ Vٙ1 jhA#h;։?D,!<3vp8f&ܿstdH|M#c2ՑPwuPeɨ},-k&'75lF}=y9]ό|cWyh-|l0k3XY#(i)fFFڎ/Å]ҝҖMZ0Q9;7bTfM,>@yeHltE,C֑&[F&Եi(㙖5{[vwɻ,9IM&'7?L7Op"+Zx+ΦaЙP}}}hpg'"}Pؑ'&ZiB' --ĝ z7,2q*2UH-n'G3^<4T MЙUPmNv_*zEB?mỔc3:3yNnvxODJڵЮ FBڵZYT#S>R1Җ|vQ]qT[nmIPvekPK[KFuo13Xl&~cU!z0bSODMՕ#%nzH\W"]qMWWO$EF"M#r0X~V$#R0s\ڷ ר(>D3hӍjTW~_UޤQUCOhCidۚGzCH]#<*h2eUkRE<+T(BXSE<+T)/1)fbIENDB`TreeLine/doc/sample_basic_longtext.trl0000644000175000017500000000216311651514477017116 0ustar dougdoug Text Fields Similar to Treepad This file provides a single long text field for each node. This is similar to how the Treepad program on windows is usually used. Plain text field Text fields can be set to plain text, so all line breaks and other white space are preserved. You can also use characters like <, > and & without escaping them. HTML text field An HTML field allows tags like <i>itallics</> to be used. It does not preserve white space. Characters like &lt;, &gt;, and &amp; must be escaped. TreeLine/doc/doc_main.png0000644000175000017500000006565011651514477014316 0ustar dougdougPNG  IHDR;PLTE Q w ')'&E &' 4+'(&9)**#(,.(4 "@@:3/1-/8; /35';?.6G29@59;+037OGF14~:>@++=E. MS8@Y:BI"NO5DT>CEIICEBVZ;GR0R63U&;HbDHJAIP5Mf37LM(-WK>@RS;="]ZBQGCF)SGOV=QiJOQ@TU:>DT_JSYBUeQSPSUACNEZ[FXnVTXhLWPX^LZ`\Z^K_uS^kR`fLaq;pSIfcX_e\_\Pn/IdxW_s_cWhi2PWOi~EshYhnZgsnocu`gmdgdLrsoflVpVo\pjmj_eapu?|gntbh[elqdpnrgsavutW[xpqiw|hvuswsur[~o}Hen~bz}zz~uXMs}slwh8,v~Ljwwr|znkpw}xA(ĉ}zF9ʘbwǧǤjвĪƲطɿ¿ŸYɹћѲٍ#~ pHYs  tIME 43ֱ/ IDATx|}v/MRZ;$D;u3%6tfM{~q+UIq|8$ew*?=>Q&Z"zDlJo^$ֶ޶6;}y݁r{Lgzig6L{3^ laŪ5̪mm'Vm[*;ઌ1796mc5G̪k2*R&}`7:00Ϭi;@4?ݶ*b5裝uϥ=u|Χӟ˭85㓑ύ'Y˟zZe?1k@k@)c'K>'hk3Krw.9%mmK69;v睹;$x\t9R|4kM*2; ?o]\Dm>9';nb~KϘ!םUwbU,KxtWONVMN jcPc+:}%2OqyY&g-4vj(ג;ǑSK]J¸*һ&!{=gQ~\&: zryFo[#'C/mҞC4#H-LWGct/@G?u$_6--]+*9_?kk >cp{Syd֎ZQ(ޑ==<wbY}=#C:FNttu>);6S+{Q@lji)w|bGJzvNZ'}ǧ\GۺE-pܛg'w.⳪O>e2Õ/e:=ȏK'zxY!Gk:=}~tni+޺pқHWAtxt#!UG/ +SO>sdhb2_tK+5 Eoʎ7 /_;Q3a _FtWgC\CDHѤ>֔{7V$Jw8m-&NL' Ηt\K \&zށ %D/VVyC0:4TAtx{ŋgvŞs]?TA#En*Bo^tLҡ]َ~,=cI 5y}}%#1p7~cEwmmC{_ـk|#vZzhO&.x \rqGDRT(r~hrH+{{*u%phn7Ip(AQʣGDq'f&7 $z:z(D{w[x}u>fg?\6-պ$_FRj/8QEPeHoG!#E r顑2Sz*ßW&ZR9R$z<3GwhFzO9s̎sG_XAth`O  V1s#2K/_lZ]x!x} >7/)i/OG; Lt8u̥7wX-] |TAer=,PUuϯDtn.[IgH}7CˬF׷`P26p-NUx϶ ;Huo/SCs7O9PAt8sͣeo_,N_} D/V-СC/1y֍r;.9TA/c|gkv( ﯪO׷#=oԆ6u%p?}v@gϟ9gG3rGO:rƒ2K}?yD4w4NG n'aFJD|eܿ GϗnؑǪjK*juYÎD_ rgŭQ _'_}Q՞xڎ_mזּ'Vl߹CS-'+eVw2rUֺ#C=[C pB;?u굯-HD+I/I^8}gXvSe/?QmaleGcZAt}G̎\wȉዽ'T=ԺUDq e4k3Z#5hdSR`讆H/e=Cˑ!PSnR. ( 9H^ VuGׯ|iЩnQUぁ^"gdW~}›{_;ReKi[R\"͠b][Fe.h _}s/awDGza#='zO)ĕ>2ڙ+ube}XKw>>\?g]}?K%ylWю׋?('pSx6E#(L2-k-`%=C}=6;@O^+?~I׆=2,Ntꏫ!|vo%ё<*<۶}o`Sv4+ih"vt S`WG&칻 )گ;ZE;:~]ҼQy.&ם#2)O}YKo(O}юΎ ̪[ٽ7>Ѣ~/?^`Cօm$E5I6n P(E\VGiunBYM:DUlmX@dQp;ܡ#}> w#@ Y&fjw ޓT,Y]W'Wߵ(x]w>o^{EO5{j|= o6,ZכϨ&Qim>'*}}K޵w{;g|I /Gw>|ϑIoZկ^=A$G] طﺫkju:qs_%u}] C}oa9 j~=*qg.e<;8{ϓ\{ѓas~sG)yn[ۋFO>K)w~z:?,k'6ڀeܠ-Zux+zOj},i&[h}Ws=(\aU}宻Ͼ>Ix<$ͤ►w߃[ckh+GEG\v:Qx4!0M`v71˨BׁBLTQdU=\i(.fgK^zY+5M> &ybX ɒ$&*D' 7Tx9"(xD d_]Gʎvh[8v~~>ѨZJ[&*S0zp "CIX ⑐0Z@ Khu*|yƇoq(L0h 8rVe20Jg$B N^¨DEDQOg9Ntk]+ njGoݸq}sgn~DCH2<6g#E&c %I:ѹ"E^6Y}6nH{S(tXSR\;g(٨2B]h %1 FnG)*`rr-,Qe辏~{]v_ H1 U}*`"bid3GP${aD˞x?΃/ j,! B:%`B%i.B += OoPpNtQܼ?RȒ:7ֺU8i!^N#2/ND7<{D>334zx$S'z.,xς xr˂9)%G-)/ŭ[nOoP8Y%77勺&FtQQRч_}7|?0mA-e-W}}NmerŭE"6ܶ`ƶ۞nx`uwE]eYJ*lJ+r))8T):Dwˡ s>`ϮP[?/ / l 1ɓ.w e(C 7qбcD_h_{^,eY 8vDgW |qvݖE^Bh(e D& E$ 0 kx݉u䛑h~TCˡJk]uYSHD" D-1uUsA4<;{c=wnhܹscNĎ>Gog?W%k)٦@+)Г$6X/ qK4[ŋGGG;'oGO WFߑwF*ƨ8EG dДd.lLDmG'aGK:z_^{`5t2@=QÎ"Fbl&SxMŽ^d-Q>ǎ_/D+ذpn\GE&ⲱنDT?k=戨.\+?-!7bUlƎgn|ŹwN?uy ^@un_}D$/b/ۢr:J٥MԔGn"x) ,ʹSцRg3^]R(Z`$좘L<.zƙ6Q̨ݴ8C/Nͯ|6Ҩa9Bh /n\S PNc g,17Ϩ7;ZgH3^,AYQ)_kIij)(5q3aG^@F&nG'%򽀿V86m( $IBMOdk k:b=QSOvsГDt(Lݓt+=?_wfH(6QF3ϺfV 2Ck MEt"ѳgw;CCQ w7nBt,-)!d(\a~/6!J##:pbh$Z)?;ڵ2멮Jrn|FEH%Q^G.R(%\N#\%+tsJ'ZaZ>R PD'bG:]{'TMo4.Q<QIiISCxJF\c<h3J!q=/ˏύƿ[/`R<lTkR4),˵U\5xxeŒRexwT$ A6Domu4)-H+͒9OG;gpYT=#4Z晚U'GEhzC opYl ";l;9J"%s6Xz48+,4X5@VR֔DM_E քɤ$:zn3FK(7P/%4FN @ٌgpi3jxV [h}43];;]De]vTMψb~@k)4yҊKGd5#dӚڈ&$Ʒ~ ,Y4c?=^3G eQi`LJ}"(gdRp(ǎ9O -Mj ø&2v@Ta\c 6f 1L#L0xk,KVKPǴ)=^ѯ yj:b<^CYԙT6xK%:b0љvO8 еmD"G0!߻kPt-$8vWheg!$&?Q TCEvo77mIhݎO/UudjJ9.^ q@▼pfGu?=5dg.^;~O+bǵڷetJ-&)\͎:['+\{鞉 ԤJ[$w66 MnIs XGIEEJQ1Ab,'(:@ :8xlFGk&2PDHB$aM<,1k8[autQ6BtX&h%E=4􇃓u;ZKѩj<.xHF bSYCϠ OUR8@I*& pe]&=Kvf L$XNB CDI.iL rŶA1є&DkD-9ZIԍ<@kQ>}Sȳ,\$S!Tÿ=dz}+zBE>r _W|{:Ǝkѳ껃ѿ֦}uS;~!,-s=r^N?ڵ2&*vttUGE׭f?^?jb"$dk3c'Җ]+lG}_̀-珶2o1\ X(7mHׁݨ;>}$Cia4l5M& d#cG>Qk~B40Ǚ GF1zU8AnSy4Q0GxT5Qd/>SW18Zv3 ǂ8rAG*MĎ%䏒15P5 rLFd2aUJbX'Ae=\d6&Uy<1v3 ;?Q2`8( f88&P@U)#J%iXF~I(NGC eh6;hg4v;?WWL5Ǝ}F3 G~ 䓌u GQ:efgcG>vt{*uu욛zѬGؕj/RS:cG>H_@1'%H2~Hg"fG\U82Tb$[TdF3GP!eJ"dʉxL-Z/NdN1v3 ;:&]ĈLJgu2EC٥2 QC%zr+4ed0"Y_56[Ww{Mb2ܼ&ZI-5EM!FJt@Gb=d597\S ({' Y̆"qedh*f2mf6_KI Ҋ"ɖg2PhBa?[%`FB$O':*%4vaE^jh(TdC]JiuOHт(&PH֍Z,7("O9UU1\.-mę1 v>>0t<%BfLCjB1SP#˽F s !^Cz Ÿw!Z%Lv! uZѓ]]\(ٵK뚃nøqF:=D+ b(Q2I$(A1m4l̦58@!B%i9/(h d1c#Te8'8U8(g1)$1$:8_~?3zKF +A5?FXh8 mӁ%dVSk .ۺNf=_k"b:=;ZM\=4sZe6inѫe^Cutɩ 7MYx-R Y)sXv^X*tU149;zxF`S?M4C ZQ3b#%,V%xW8ѫRJ\Ty!0 mouTdd-&eU(/T͈?q8J 6{mGzq}v)c4Y3K65O+%gIh1H Z4V)Q~:1-pHс #YFm'- yIӘ{ ]g$ghg?d\v:7 iM:jM1Kѩ`Jןo2M7h LyѨ@mh}m;V\i}@blqսcߏ>#uP8钾j6l98|^k3M)voGY0؛Ўu9u4"3&j3 m 7` %c[QC\,Hjaro86p9Ⱥ:9-2W }#qcSqFZQ@\R(mlcFa$$>, f*5ρN9?bM"t\hُ Y;I{Hcpb>^phc$qd$fS:(;LTc1 "@gD ^&׭HtGATܴY菞=;~^x7jyyS|iUo &fGǝKw~>?b4jǴr,|0q<sյpksiU~qc'bGB 3ɺzd`P/z⎰Ce}T6.1t<3@AWg "|2l娮;L Xo2vCF gu_soJ2e3 "B1Y !N&"U5ҹ#Um̴$!^K&D7H1OF=xJ YFB*ͅ9qs&gF?Sj@lsIQ#ƈ\\4bMq-eIg:FVJZ:9"LDY&!Գ k>? y\V RJ!kNtZvQ9DF6y Ȣl$ ҞJũu7oA$т(Ѹq '1.4b/30ph!U?. fj@r3 hA'88zki7hM2ъ\B،^lp}f?z^G̈́_dWWKdikfÎ~]g϶n\sI3/o\(esgDc]?vtqF`N3ZhH;ό5c|LO=7mh)BhRnaq(26Vxx-"cD*-Q3q( n,+Z'2vv5;:9+= FB(lrΠFKd\ d=d([M&R'/;3:Û]M/?ʒ1a0RQt);.C; v%Zlb&4P3vg"t{%Q/V$j<DI Q8hZ/sGZdݎNߎʿ?W?`&~,]ݯq4 /Fa̦ p 0Izv͂3={V~ǯKy}޴Yv*J묪mh:Nv/Fw3 ;ZPrVG$'(ZWdtܖs_aL6R#vQ]f&tq1&6 uu"Xd(>]k(rlz^6ӱ2&vvlxF-GHa8P\1?$Y"d0Kq@iNeCQ9lL5l(٤e=^gTzGᔟ5d~KEfw3j c4}np  ~Yh(p&`GiӰW5&EwS0hLH*5-uOc1neYˤ2QMM:6Ψ3;n?'P(TXĩVU+?Kb8NYb.hͤSj IDAT4˨b H6iM{>]SӟL?jM%;ZDl? _;BVbqbqF`G=e?~u?ZL]}#z:v3 ;ZՖWRz!m_iOrb6X7(D" FpQ(T3;Z.xʔG!d[u[L$ "S4ey)m$Y:HLl(-<'Jb\g*ÚqFu3?jlLvf)!PkeSP('BҋlRI`+BI>edE2&ى Nd5]񴧅SQg(b7aww8>Q!y*dğ-4f(q B;EFD91"M@XIJtq83]d^T@㒚9k-ԬM1&SR٩Pmo@L `Q@#Mͯ{#Du/zv؆( FΊtJ1T&r92hh[-Qe:c֤QQ be> ل(kQ,LJs\֟ @^^,aMuԖif͊:""7<4q牸QdmVn␷ v1!xzL%*,)UaSa&§W)DdyKg)do42(MƪaC9!9f c0D,'9[xq¤dǙ bf0ű9ON/?֯~z*3|FxMm]fL&^Qk0d@TDd-MEm/K)jGG?a/;Z3H ,`Ʋ*]d,Fp-h t?Gi"r=ym-MG>#G͵.,&&uڄժ`*%Ĭ?zJ&iۦJbdldUDU.I ч`揎\9AteIr)!aY6!;2M qm.& e猸f}te2NRel~Ru=ٸXGI.'OApDǐd@m ㌉3L̾=V3Yb0?//Jgx|?6ۈNfů/M%eY27'a["ݵä́AO-t:QUU=[3hgdHB4\jDdb27"~~тψt$֔ڂf?Ispb'UeP5h\⡮ Hċy9Ʃ?{6_#3uIEP$sK0Ũ(K8-3jPJb)PE!9R\#>VZg5en3gA@ԍ9܃&mW%Gkl2Dž&]fMgG?ݓځ-$6V$ -M12kM,# b0[%giQ)U@O7cWZUw GBhU, \ K)v3×IBDkq IO eF# P$ r2qh9tE H#!DB ~p}s}_oaF$]vLd`D]Uj-)t|YD14ϐ _34w>QI|xJɅwmDeQLI4m(aLc5ZU9v!#72tA1Cli,ӊޣԜu`#~AhJxd!fqͮ$R *1^!'U' 7OȎL{_ ؍[ȈcYid>QR5 ?Z0a;T%#> G2=J(1hi*T!1Ip˄["\\8#b$LJ7Q(LsjB:љ?`w8Xm 9iyJrGvYmGS(?UA-9N\-?%ؗ Vu{`8ڦ?ڼ\֞00W|&Td0T {f!9sDtZvT[XWqz2˲T[qJLdiXNɷx4JzR91q ݤB3⒮RyDGFf(Ⳳ* 4Ҭd͒S-"oe|jKqHgGlZ20,y:ZY8A .mxHvvD06 $eM>\&QW K.VTXħXG9aL!3:J:3q(Ce?0߆@/ފ?U#Ə!>,Q:>TFHP&2lm;KF ec:2!DpQU:k]*~D6Ƙ_b`"D[ lFHHD1\ #"Qb4A{(Uht[5TQFuhGK^@T :)0#E[iL='_YydVbA0LįO(\lzy?}G_o_-jdX\[Y&y܁DEWaH[S.k%Fa&ˏxF`G?K39Г$~e/0>NGOn1YdZvGDKO;ZVg-bfo Z:7f qW֎-JgH3^;9$,FlZҙWҳXub5~d P@R°"Q$o% 4@QJuDDoyRd( Lg)KzIGrK|s ZMkNGuC742' ɏ߫?:'WBD,QՉ,  R 5'6gbt3{l6Qp&vSG]N( Ƴg{{ώ'|#j ˋ!kD KE4nxX؜/x8Jc9)dblCDg]%=o @opL%`y*@dh2?=ԁȊlr (,!yÀA@qMH5 L҄Y]b5w*C{񔾈Kz >f 0*¥òA&| aq}uGcawjؼ{TItWIqV'1̥*ꇅYQW{6,{yN\9RdTQqׁ=z?J~UdIKQ KI,iqW'1U&HV( 7UjIgM}v?{d6E ▙5ϴn <~uoįJuY2[}gڔ$7anUǜRu]hTȆ9$!35A,KLag%Q6E * g\ 7G`ƒR >Q]bgaLl&QK}4.Tgkb RTd*XQ9=z}z"(& #fH9 G 0oQX3vh\sأRR0)0Ls(E?WD+yf8s%*f&$`DS3.$^QcDz :H^T@ӨrR@qeW#Wңܟ/OGuScZ :"a*(n6%Fn5FS.l=$  ||R;Q 0FwU:>F9Gb gG1HT\F*?NBٮkb#|, Fq,ЍЈD. `U3>B|ֿGf 0̳hhxJ&P"71A2u ТaZ60D Yk) ̊QaZWtӣGe Huq ugf8+pgOi3KL⹼aw<ǥ(MU3!jwR6Lsɀ֍D-4'0 3͸aF5%d[k2R@0H"Z 1gM8yzt} RcG Qpx"D] @;Ѿ' !5.! 7YnJQl^!QjQ8dt@z!QUقa$iV 7$EAH5< yA]DńoQN4t_ivH9>xKzÐGH-~) BCC8U5ϐ% ]BCF1vE._3)@xr4;S<~)B(PH,K0J2  #gSIZI)`WO5$gLi=pQP>UtTETbCDC KF* w~]UTUP-V)Ή?̊"'I/3#:(zjbTLR;ٻeԇz^*W!Vp豗/@TXɣUhB'N,5F*9-_Q^3UIxo6 ̣ tVE@R5I{o RYsB$-)fXA՛ZSsZP=8x'g]{pL/9G 17u{HLT{,RIu>#4ʹȵhe{Q7B$yGeO&’2?:_fF 4Ty֙bD}kG*oK7k Q9. tb=_U9u47T5Ғ $=j1*n/ooaQip8@8D+=ID<4,` @Sw0x$cu_=|#%K| JZ{D+ :r[v%6+@/^O|((PdA@ 'w`Џ_/`?@8u)^?*G,/DNT2aIu)^? 049,4sF|IDATLR@cab.LBP9R4A1jN5cD' Ef>\s;w]z>/vB>?_ӲE+xm1URڲ4EiqK/J);zt<|,߲ܿc":XV7ڱ~c[>j\>Ngg=|=j%^mu߱dl=:X^*@tlQvxz?,gǔ谑v?@tLDhg_7Z7$g_?SS^R*v\D7m_8߾)D[/ƓԈTr- ;Sr}]P6s-ۭ蛝e'~&tʛ%9;Q3>v{"?Qmߐ\~~voV=D:wNqpIi&~"֣Hm~u^>n'/#.^eCt8rG.e+uZa]fXǾw}4뭬Ѷ2J{'X;λ~#IA48ɅeZVoR9'yDv%y]i7iu{-AeR~V#[i-F煽jQkm'eCm3 g; ]wRکLʭATb'Tf>~֣tb曍A`EC^C.G3^nuuєZGz{n-Ui{imd\v~[Ԝɭ/xVgx/lYZ!bm[EthZF؍ 栘-jX|D}G3s8LL=6],V]gbq}tXh#Cʢ'o=z Pj?B-FۙL+ԏjYIVYT !TuxvJ:ݰSö#Yws;񸏨r ޶#Bwz1֘0mwcq?Nm[ԭw~mZLKFOeub?[hnv^R׏/gsCDw2Ln Z V1aShݪ[uogueж^Z\{MzNZ5~ug<_[~m/Q{𰧓W7 *˅R78i/ d/9^w^$>:ѹҤ;}7W^vrNɾ&=3uֿځ Hh C9 _ @^J;CfDUDUwbxA.{ + 빅ԃQ ӓNGG%<CpA:*8.V?jIENDB`TreeLine/doc/sample_basic_booklist.trl0000644000175000017500000000413411651514477017100 0ustar dougdoug SF Books Greg Bear www.gregbear.com Darwin's Radio false 2000/10/01 4 Evolution caused by virus begining again Blood Music 1985 true 1998/07/01 2 Smart viruses take over Orson Scott Card www.hatrack.com Pastwatch, The Redemption of Christopher Columbus 1996 Yes 1998/09/01 4 Time travel to change history; discovery of America Enchantment 1999 Yes 2000/08/01 5 Boy travels back to Russian fairy tale Ender's Shadow 1999 Yes 2001/05/01 5 Ender's Game from Bean's perspective TreeLine/uninstall.py0000755000175000017500000000457711651514477013661 0ustar dougdoug#!/usr/bin/env python """ **************************************************************************** uninstall.py, Linux uninstall script for TreeLine Copyright (C) 2003, Douglas W. Bell This is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, either Version 2 or any later version. This program is distributed in the hope that it will be useful, but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. ***************************************************************************** """ import sys import os.path import getopt import shutil prefixDir = '/usr/local' progName = 'treeline' hasData = 0 def usage(exitCode=2): """Display usage info and exit""" global prefixDir print 'Usage:' print ' python uninstall.py [-h] [-p dir]' print 'where:' print ' -h display this help message' print ' -p dir install prefix [default: %s]' % prefixDir sys.exit(exitCode) def removeAll(path): """Remove path, whether it is a file or a directory, print status""" print ' Removing %s...' % path, try: if os.path.isdir(path): shutil.rmtree(path) elif os.path.isfile(path): os.remove(path) else: print ' not found' return print ' done' except OSError, e: if str(e).find('Permission denied') >= 0: print '\nError - must be root to remove files' sys.exit(4) raise def main(): try: opts, args = getopt.getopt(sys.argv[1:], 'hp:') except getopt.GetoptError: usage(2) global prefixDir for opt, val in opts: if opt == '-h': usage(0) elif opt == '-p': prefixDir = val print 'Removing files...' global progName removeAll(os.path.join(prefixDir, 'lib', progName)) removeAll(os.path.join(prefixDir, 'share', 'doc', progName)) global hasData if hasData: removeAll(os.path.join(prefixDir, 'share', progName)) removeAll(os.path.join(prefixDir, 'share', 'icons', progName)) removeAll(os.path.join(prefixDir, 'share', 'icons', '%s_lg.png' % progName)) removeAll(os.path.join(prefixDir, 'share', 'icons', '%s_sm.png' % progName)) removeAll(os.path.join(prefixDir, 'bin', progName)) print 'Uninstall complete.' if __name__ == '__main__': main() TreeLine/icons/0000755000175000017500000000000011651514477012371 5ustar dougdougTreeLine/icons/tree/0000755000175000017500000000000011651514477013330 5ustar dougdougTreeLine/icons/tree/plus.png0000644000175000017500000000057711651514477015032 0ustar dougdougPNG  IHDR;֕JgAMA abKGD_ pHYs  ~tIME  ؎ IDATxcdbĥuw__ww# Xr[OJ4&go##S514 ,L R"| B W=gh뙅pp;ӗ 7t?l=Q$>Ȱg. bda```s:J brJGoO_#./ 0l\f٥)IENDB`TreeLine/icons/tree/warning.png0000644000175000017500000000145111651514477015504 0ustar dougdougPNG  IHDRabKGD pHYs  tIME 42 tEXtCommentMenu-sized icon ========== (c) 2004 Jakub 'jimmac' Steiner, http://jimmac.musichall.cz created with the GIMP, http://www.gimp.orgqIDATxڵKHQ7yh5Y&hN`3ce0m* "6"Enڵ(ZZ*{AH f3NY3sZ8-Ep9Ņ`&p w@7` x}\&QEȪjaBa ,"eBw |*& t@?,V-sqpK%@h,^`Q_q؀1h Eg:kkb1* 2__m y*Ѐ + S=Lp%7(Ֆ,@!L2>ljUDq |ɫqTھom *]T옦A&gx,p;/qp fnel%+O./-RZo+H)E0@vJ i=`IENDB`TreeLine/icons/tree/write.png0000644000175000017500000000075411651514477015176 0ustar dougdougPNG  IHDRabKGDIDATxڍAkQ? v"%ϽRu!ET&HqHBL! ) yLf a&h/Νt #ιxZrr㏘`__Jz`4J)|GD<7x|]?8mNT2y'/^me "vl<]~nݒ>]OXkf"B\>y+GA׳1s TZ= GAEDQ$aJ~9 f<16Fy5ιB[7WJ!"T*iWMιn1ft 0X-qsZf.03IENDB`TreeLine/icons/tree/date_1.png0000644000175000017500000000102211651514477015166 0ustar dougdougPNG  IHDRh6 pHYs  ~tIME 0-I!>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XgIDATxڝQ"Aie5XM430301%|354SGCCA`qٹ`]:Ɪ1nzf @@ۍ&o`0PJ}&|eZι7r[B 0|Лf4$Ip8lۯ׋6Ƙ,NrZj,.w/MSzxIDATxmmHSQ7qHm RrL@$6X.& DTH 4)Bz%?L$ tsnW79w6s8<=@  ET z}Vd\ńbMrr d񢄴suu?'x'}NP(PL )))h}3Ei>L"%%|G@;#3XXql`jjl tCZ=#oڲƚۇ<hlvrMxd'j{ hD5f D(,,T?bpR ? ;/H]}։NL ]gìH$Job9@v7n>+ ݍEd21@TjV/`́W˔cb.^2KEQK<,;<@/yޗy\~"&O2a'zE*Njo[0 @wߌ1X|6hƣ̡v$ESF3Noe/~&^H$

 F1  F " @aC/Gpr000r10 0`^1a8ph:P"A %^`` S @@14%$@ xjPM3 g 0f6Or n5]ex4ؿU |i X?=L ^5S(\ `N~` /, {b,HIflb47Ź QU!6IENDB`TreeLine/icons/tree/note.png0000644000175000017500000000102611651514477015002 0ustar dougdougPNG  IHDRh6 pHYs  ~tIME : D>tEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XkIDATxڝ=K\AsZDB"MJ)@6BU 4VVY6AM ]A Fp\{ubf05S;[QzlktԐ,(mۺBwo4߬u !)j(~ty_7$Eńt*O.?< /=,+R.V :ؒTŇz\i|}${s3#HiR23&‰H*:WHzT;x٣R!J{ P]71%f0,yֱ[yXV$ Nu]8tjl5PnK淋!~?b}ː,VbIRP2db'9wj_6(3˿9]vjϞB}xV'9R\7pѪÕ[qfIR(?Y 2(;ށ$, IqxbӞGshNRz8Ǐ젷@-@pEx޶C "ϾΈ*2|dgxr%$BVq W[?_p9>C$G [w\nk[%'ddBx4)!0mM).6XL,, RXE S018f{Wa@.:`MIENDB`TreeLine/icons/tree/smiley_2.png0000644000175000017500000000145611651514477015567 0ustar dougdougPNG  IHDRabKGDIDATx}[hg/(zcT%BQVEJJ/E#x^I{%ZςXZ,(DSh6"Fec jL6{}_/JkaxSZw2&UZP[J6n&JW]':5Ps]Ќ)Af`0xG`51i>ΏwiDXxABH7O*1ogui[w4o@#gM3hߍn24EH7 >ml* 1se(f;w <}UĵȺEkQ03C{ ,*]|蠙L^qI#z{{9r _'Vzy& gμD8c8~oBz"n/| e3 Mf<颤N'Ɂ#(?'\W~6"D6JXtPC@[u{O4 0pSMזyu%=MuW]ZL6ۍ!?sp&Vր"O'XTјKKnqwN[f>,>-fVkN_5PRh$H4pߧǶ ChzKHY,)^8>{3]Um!iX"Ć='kCp8,}ocIENDB`TreeLine/icons/tree/smiley_3.png0000644000175000017500000000145311651514477015565 0ustar dougdougPNG  IHDRabKGDCIDATx}khu?˙[@m /hQHxA(,`i#'Z"^E  Q --Jps36 Ιudzs. oE \:?=Ƨvxoֿ]#p\/$ǁ\e0*T8aXX *&Jrȯ|vn2d˛|YNRpÌb* h^]ވUAo>p-K؈2H?aX8Ћiא)1*p0ީcqZ־3W}M`մJo8X-Բ6nYseto@sMf|`a5%GO 9to֊[f}3;hϷ}x;&S~wu&;ˁa *D2~w|-8#E+ ZK KZL/VC"*C4|RJ>f(,d(2)` V/oťulZg(24X8gEP¥j:" *!CW7W=6pZ/?o_켹e>CM}C'#d8#%+ΪxieIJ#.yeutHOxq4ܾeM -UTW}*FҜD r%/qIENDB`TreeLine/icons/tree/smiley_4.png0000644000175000017500000000141311651514477015562 0ustar dougdougPNG  IHDRabKGDCIDATxڕYHqߢ4-Z9e{$Qe=EAbiKA-QBDEeeTЂC4VN3||ۿ,:psϽp^9@)pxWoq~lQ]^-? -z9PJ,FR{H+ AI8~_jyJ_åSZaDca$9 [`jm`4/Jk䓦`i/z8#/Ig˪ةl]k&o_> '.c+1?+8uoř/ wWp#H=JXxs#, `,T+ʌu>S  %LBR KCX1ʤK6Mn0q:r<#LU[TzmFoъ#68}S[Oe~SnOkgq^g{ی MKw!E45)P 7P$>WbBKU$tU)^gű=bl$1/&'cF;px HrBqdbAA|l.ą(n]1gF)H! CiysU#`ߞb`y9nR$b AwMZu7>.r@`8 n BIENDB`TreeLine/icons/tree/smiley_5.png0000644000175000017500000000144311651514477015566 0ustar dougdougPNG  IHDRabKGDIDATx}{hq?y_̑3?93#D\ʥ??-C [$E[lss9sYY9{{Èo=}zϷ>7@)P UG,+=af6qnQ%2U;uEHr#;>sJ^JP~C%r׬j%1tnd}8]v-Ѐ|Jr^}4h>ѠpVzl#D4˕QϠ?6C\kr$0C1azslL idyJ I[G3(Fgxn e&fMV 3 v{Ȃ[k#sQL0P m~ъX|.Z @rⅅÓruAN,aNڸPgZ0\Bg˚|,N@$j$ {h'&x-/P{ow$fBO$!T,KTWsZ L32QxJ9$ItE˷߸x=ȫKF08DDcIENDB`TreeLine/icons/tree/trash.png0000644000175000017500000000141211651514477015155 0ustar dougdougPNG  IHDRabKGDIDATxmOOSY{R;PEl`bԡ7Pd14nܘc\zWJB&3$#Q'` 4b)TiKioV$>{-Bq00 ,"377" m;B$ ܾ) 7"%C3~/8[)_C^w?<|tb` ٽUx|7lNzzzzN'f^C'hy;¶LAm @,#ځN 0B l܅bXl]Fp-4iȀBk(¾Fb<rqWV q6-<K299A2fa!ͻw)ٵR'uwwP(DG]]S*'Bb US5TM71:zXV]X\|OZE5TJ ֶ~*CCqO\t_.'q]~(7 #<(HIENDB`TreeLine/icons/tree/anchor.png0000644000175000017500000000063211651514477015311 0ustar dougdougPNG  IHDRagAMA abKGD pHYs  #utIME 1- IDATxڍO+EaqQlowiaORB ZI}ee+wcݑk3G;gy睙Lgc%W8lWT 3\8Ё+$(lD`4aI\`r|<+<&>c"651S<׻[ sdmj|h=ou ڨvQWc\8k'N`Tx.cֱvqǨ h$qwI [(tʹ2X~~cLM !IENDB`TreeLine/icons/tree/doc.png0000644000175000017500000000053511651514477014606 0ustar dougdougPNG  IHDRagAMA abKGD pHYs  ~tIME,"o IDATxڵS1 0{_c |">Dl ^qDrq60v3|#@pC BkM0PUUV$ ,¢kRJQ;'(ж-y8(rJ!Iq~;Ƙ}4M7n^rɖ|8"(]g7珸Ak}ЁXW!a-i(Ng xn]D ~IENDB`TreeLine/icons/tree/default.png0000644000175000017500000000124111651514477015460 0ustar dougdougPNG  IHDRabKGDC pHYs  tIME 'WtEXtCommentCreated with The GIMPd%nIDATx=OSqキPjR+1`@ :8IÈo IJ r{Ag=I~\<|6(ێ["aەKjWh-Vq,"F!QHG3'N7;O7iJRR/S,q]$J 31Gn/&//VnRgQh8B3njqx`|ϲ V !7=C@t ]@:k@BfP#!hMJrRU;yl;.cC mWsg2`0*Ț\ گ,9Wx;GjcæF؃l*`@ mhDJG?VL4 +|6Uk, (C[jl*`*J=ݰCcsBhh}c {w2/OW[GXW(w?xufe5:ICzcs*kGPf{JY*PB5~lϳǗu0@ ׮Փ1gHzLk0 ÞwsDt+Rǥ<+ߩxr6L@0H47"L\j2A;]LY[- wZܶڈhrPߙ#{zj)멉vw'H<>3S TF)V6xZ\^Kmn_\ أ|Bg_ּR {B+_C{3G?=aYewp~eP*t-?Y1 9IENDB`TreeLine/icons/tree/disk.png0000644000175000017500000000150611651514477014772 0ustar dougdougPNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?% X@ĒGqr }? _~g ×/~w Ow0\x @.XO?iˎɞG`>:z̟?oN+{ _a셯_1|:?>6V~c߿b7?116 s QTŘ!߽RĘ=*rǎ5C!Z @C!;++#3#';!^.W@O?  @; 4Xr`G(a= .]@@^ tf`|i ' Y_?>'v?A O80‚ hׯ ?;Û7؁3 LL@W`@~@Ac0?Pd 4ϟpb/^axSEׯO@ Z`F_??#Çׯ} @_7778l (Iׯ3 `2  IENDB`TreeLine/icons/tree/book_1.png0000644000175000017500000000042411651514477015210 0ustar dougdougPNG  IHDRRgAMA aPLTE@@@@tRNS@6:bKGDH pHYs  ~tIME \m_IDATxeα E=- 0;2EI@ &` $ A;qyqt~XaUNꀭp:s٦l_IENDB`TreeLine/icons/tree/book_2.png0000644000175000017500000000121111651514477015204 0ustar dougdougPNG  IHDRabKGD>IDATxڵMHTaޛ$cZMSJX2"jW*Z D-ZmEAiZAE`iLR f7_ Q$E89=qƷ O Mo FnM(:.ӡV5iBp-z0]^r(Ӧ且 fe3ۛ lGJ)(`hJAe@v09yXy@EB˾i^#cp1֊΢ea{]:]AyCy.?$}=)f?xǓW/n3pp5Cx+}\SJ$sdJMW%<PM޴Q+WLii=0Ǔn}&R9mЃI ]C;ɾgu[:H$PJ R233]] -~~+yAi@xpb_QU*B6L˄Dg:ގ)8u;ahKP)ByY8 x{{CPW:gd^>dBh&ϣ|%җhɋIm˜ 6p{ 눊@5Ph}堅yegtX0$h Oz}3=&}IҢ\P UX9X?[W6/'lhs=HCCChF5u( 8_"hKw4{`_he߯ގx[J<:in|>nv{DjjFw̱(b]Q{NۺUH}cff>T6Ic]Qש3۹#~2ʪJKfKP4+W>s}zE:P԰yE O?hs]UP]G"WH`IENDB`TreeLine/icons/tree/term.png0000644000175000017500000000077611651514477015017 0ustar dougdougPNG  IHDRagAMA abKGD pHYs CfStIME49^gK{IDATx?nQyX(J qw@ hr]:*@ e߿ŋGO+FfF̌hж(]ɢm[1FrSN)Ri[6 UUQUr~Hu]}{~wG_>_npǀp~)A1+sF"Ὗ:ݚ{b C"NXV«=.(po a '3fᗽ菞פ) 킍(_="4Yf0tq8!L fQJ&Z agݧ't˴`:?W2U.׿u3xtLS _]-_v$R9XHngƍSF}?@17(.nv1v. `jbqViΝOkޭS_Ov,f|W)W_Q IENDB`TreeLine/icons/tree/rocket.png0000644000175000017500000000070511651514477015327 0ustar dougdougPNG  IHDRagAMA abKGD60 pHYs ?@"tIME .NJBIDATxѽJ`IZPEEp *HuR+~@up.EqނPAѩMqЄJ۾pec 23z,ap]4Xu|wVɗhj ap|yy6D ,Ak5 u*N<11^k1X,}m'3H@
2 s-jLf. @n*ENz acLஎJKd=lE3> F"&fd{>1@uaX\SLԶHRIENDB`TreeLine/icons/tree/heart.png0000644000175000017500000000103011651514477015133 0ustar dougdougPNG  IHDRabKGDIDATxڭKa>;QY)CBaA{Rt Х @j\vɴVyH%[/_^yD1 zS]G:vxۄ_:/|(% #CE+|(zˍl%en7[ jȼ :104H!qT#::q S)!ZceR[6 2\SKWϞ*o.eeb[&:e]c,RD7/'p~6Ex˭6 ?yٕ.h]p,k3,'Qw'NeHUb4ޓ?t:}~3B"ن3uxD\~-k_G!P;IENDB`TreeLine/icons/tree/task_1.png0000644000175000017500000000044411651514477015222 0ustar dougdougPNG  IHDRa pHYs  tIME (9DIDATx= @F_0]>mHk)'VBtEXtCommentCreated with The GIMP (c) 2003 Jakub 'jimmac' Steiner'3XIDATxڭ1HawީX q  Dt0Xh@ڵ8.v(DұJҒ ikXk;࿽{ߵV QTW&-,p͝]<ϣRP.hxfyxY|vjJ, c6BGfRPeYr9`d2>> R* quPѺYPGh{1Ƅ*D{{x=^Mbu u]z#Hx}{-n2M4D>V풎,;-4|ZxjL:F\EE"p0d d N"K/gu6|z:!2y6cIENDB`TreeLine/icons/tree/bell.png0000644000175000017500000000075311651514477014761 0ustar dougdougPNG  IHDRabKGDC pHYs  tIME 1ШxIDATx͓J&Y#"E O baa)U XME&f=͎.xaP1@#X1$4 L\n$2,eCdsuR;bbrs4/k a+>NWOր*Tꘈ6[@rj.ζzƺshN\E)1q 78K~jAU@yFQ ڛ=TWiaODt-xBTļp{&"T0 ,R_m#M$M M6)XNg ˋ+'͉엗/zTPsIENDB`TreeLine/icons/tree/text.png0000644000175000017500000000036611651514477015027 0ustar dougdougPNG  IHDRabKGD pHYs  #utIME oIDATxڵSA |G%=1ZvgL6M1 ^MN)A 3H12Ak]!(I@KZ[QoDe5Ba7Q;pUӖw0%blܼޏ{sryJH&>IENDB`TreeLine/icons/tree/hand.png0000644000175000017500000000124311651514477014750 0ustar dougdougPNG  IHDRabKGDXIDATxuKa?Ӷ֚y( 1P.MEtUAAAP!AfeT +-cXM9;}Xܽ}>V')@j>u=mPY7MID[n$`m|ey5,jer 癛ri-v3خ`& X%DZ56h8#ˉ;|ɩ+)]m{PŒGx4@ZPZZ⩨j :9},p1‘o;?:{`y ә̥@E3,G_c~q9Qaq 5\?wO!Q(D&Jrexz @.CuR,ST VǟA$Au,jMA>'2x<$IJE 4M"`p1xP(r暄Z-6FMh6J<3 lfɓZ ,!NG!BuYtIp$*`@4e2,nQeGrMӰ,X,=۽F8pVq ?z=^/$m|s<0_( nNnC'g6ӿS Gǒƻ.|d;;|;ducR?_{16IENDB`TreeLine/icons/tree/sum.png0000644000175000017500000000057111651514477014645 0ustar dougdougPNG  IHDRagAMA a pHYs  #uIDATx1N0EW ]ڹҖtt3!ΰG0;ΰ73Ď=gFO_Foc<0Hkó޿t5ܹ29vĆ&8 P4WG)a1u'S` L.7.,>9'{&E@b!Ru->MID|~~8ؓSlɠ+T{EH #k\/qރVк#Z[cL /Yho40j}\01Cc9|Q/vr(I$YFnȃW ׀fj6'aHR!NO&|L9HS kGU_(0ayM<],x|r!].IN\xw:V `we4|g)77xqR +[s]AFlmj<Ư@'ҽHKQD2ARQNY!k@iCp~₯IBa 9 l`E.f6zzy1͈o`Z-@c]*utyXYC~ _bH*UHfNdTQsW}"}+ҳ]}P`&?B?_BIENDB`TreeLine/icons/tree/bullet_2.png0000644000175000017500000000125011651514477015544 0ustar dougdougPNG  IHDRabKGDC pHYs  tIME ؁tEXtCommentCreated with The GIMPd%n IDATx͒nAFzsUb/qEJx4<@"***$'JG@BR D@+o쬽ٙ@&"5|կ_NsqAy_/J$膒 gBwgO?ÿn=in&)X)RQ8#;V>ozyX_\}T[V.W(S*p4RHcg6k^Q)`tma7(Tz ta-$E ux0N0P.iZ:_':c{lZx)R C{Mxίɤƴw)YIׅ6\,FI{>o@vQ>j6u!o]qBiq`b?Oed 6&~6FV0ccc\@ }}P@LE) 6Ro~2|zEk@'@1XߞnYj" ʲ@00pꙇ r@g30:|*r''5O ī} "BtTp;eN =SqS { VE g&FIENDB`TreeLine/icons/tree/sphere.png0000644000175000017500000000137011651514477015325 0ustar dougdougPNG  IHDRabKGDCIDATx}͋E]U]==;;cv3fCED&bK\O<"]='/ l hf=U5A6w{ǃSPJ!B !\E8> q L6Z&tR a$S3P0+ \@rLqf"<8iIENDB`TreeLine/icons/tree/arrow_1.png0000644000175000017500000000044511651514477015413 0ustar dougdougPNG  IHDRabKGDIDATx͒AjAE_Uw d7ЕADGQ\ gBe@jSPCe]~m<h<'7)1̳$EDxm-3T+@5} ir+ň]R@nXdè<&[] Yu)`p:_dS[6N-j,t}RH¿+>HQmIENDB`TreeLine/icons/tree/arrow_2.png0000644000175000017500000000046111651514477015412 0ustar dougdougPNG  IHDRabKGDIDATxœ 0Ek/7(͡AcTJIA|%{H. !͞t {G!֨,L rDp=I+0  )efjJ*@D TlpqBջKEsXHO`&r+аU ua]40u=D,eJ$׀q Q@eIENDB`TreeLine/icons/tree/arrow_3.png0000644000175000017500000000045211651514477015413 0ustar dougdougPNG  IHDRaIDATx͓=A_w̌ gjmXY l, c/É_ LLLp9Yԥ|*ýÂl#}|7@_߻pmR P Ï/04q`Ռ-bԎZ %*.M^xoݜ IENDB`TreeLine/icons/tree/arrow_5.png0000644000175000017500000000150311651514477015413 0ustar dougdougPNG  IHDRabKGD pHYs ,tIME &0NQIDATxڅk[e?{ޓĤibHҎn \cFaؕUEx)dPe]"]KS4'Yy&s< I t֜}}W<%L8aHvG"6:B+Wg *S %- G $ L_ﱸ;پAeN !oW^8E:( t$[:6WZ)K?OtRNS@fbKGDH pHYs  ~tIME&  aIDATx-MA 0 KV Y#d;l)IC"*WqUwNn@LK3Gp8x)dSvl\?|]/Lk@IENDB`TreeLine/icons/tree/lock_1.png0000644000175000017500000000133111651514477015204 0ustar dougdougPNG  IHDRabKGD pHYs  tIME 5%_tEXtCommentMenu-sized icon ========== (c) 2003 Jakub 'jimmac' Steiner, http://jimmac.musichall.cz created with the GIMP, http://www.gimp.orggGIDATxڭ=SA"\6jL?! B!6{E fbt%9ALq<̼ꏀ@nG@ ?JWV+ q8vle28ٶ-zWfeez]t:e۶ wh\z A-tBo";9bo- /4<-NdJKESܐ 1@4]_4w9 r:k".)a ֆDk R 3U% åR29M7 K,EWgX`z:Cz`:=sza(ИX|9O_  ΤZdwIENDB`TreeLine/icons/tree/lock_2.png0000644000175000017500000000126511651514477015213 0ustar dougdougPNG  IHDRabKGD pHYs  tIMEp9tEXtCommentMenu-sized icon ========== (c) 2003 Jakub 'jimmac' Steiner, http://jimmac.musichall.cz created with the GIMP, http://www.gimp.orggGIDATxڭkSQ{ojb%bBAjLKA!.wC@;!"HԡB$%1E=p}x{΁c`y^ٌ{m4L& p uMJ%m꺮<\ VGnwf?jiZYEdMD}9F#)[)^am5xbOJW5Kkʱ5غr417f!yMؽs M:#hL|O_I͋pw7>b#1q% 6B>`Ypf6Q$bɗ>"!<+{Bͼ`HD )lv(Ixe ,[§A`~J:C:=WKcAmc1> @Ϲg DVIENDB`TreeLine/icons/tree/pencil.png0000644000175000017500000000057711651514477015321 0ustar dougdougPNG  IHDRVΎWgAMA abKGD pHYs  tIME5#6@/IDATx1j@@oePZPD] 'Hk(:. jCJK `-'.fvy ; (]=I'>Ծ ",BKH9"@B՝'p+HU*ٌV(qF8im$˜ݒeIQ|; !\$Q=z^ r&DKKKzju.Q۶1Fu!J㜶,ijjjϟ?]|9[׿jWGGGO0hEizLFs ($ˍ9Mj͍---JrR! XT>p8J)bG)j_?~AP(\pn)%0R qc|뤥IENDB`TreeLine/icons/tree/round_plus.png0000644000175000017500000000605311651514477016234 0ustar dougdougPNG  IHDRasRGB7MS tiCCPiccxgPi CD HI$Q20$L "AD\]"QP ."**}xWO?UOU x"')6 ug2  VNj?XKލHxI<.nQN[XnWXrfZJ v\a:W8Xȑ8Q1x_.GʄBSBZW6k57EyZ࿝%*|qiq9X}{m;`)^S; e7=k,@)'.h@@2@M  oBF A&@>(% 4Vpt2n`<0^y,A uH2ؐ5yBP@<(ʁvCP)TAM/92tB,7F`L` Xfvoc8 ΃p=| /÷X"lFBhlG riE~"@( btQ(WTJAmGP'Q>=jMFˣuh7t0:Ga`X3+&ac00CI`l6[=Na8%rqf\n7[‹xo<_owK `E'v*q"B4'; Mr IH'HHIodٖJN##7ߋĸb;Ī:Ć^Que#%RN9KCNjk;Go?'>& A0H(h!1CR5NT.5zz:ICh4@Jc,=^H>HJKJn )` #Q8e|R+*5,(-'m+%] &="Q)$ _S,JV[G6SU99G@#yX^[W>[BB9EbbbMZ)NL $ӎȬ`1]ӕTX***m*OT lh2^y5%5/Gxuz!~E FN4ˍjak5m4S45kaZ Zj&ڱwt`S8:CЫWVկ%fN1R)ᕌT4tׁ2fYAۃ(7.=D8~HPYUVYR*jھFfoa#GZkj k?;Υ^XƱ Ǜe ?=d,\̞ ;ugǟZu[m/~ eǙ޳쳭ZNk/:vwv BιnMW_PCYuqR1'{7>|~OUׯ9_o7,nɾyVǀ@m탦wt5=fgf={ݿ5vdh4`Xؘće44c4s~y/^&\SϚW~ysޅu O%[Z,x/A2?a?U|rD. r \@"D. r \@"?vnVccgy*4`|zTXtauthorxsVZ!P,F=IDAT8]MHTQϻcԖC L Em`EA-,I- "- 4#B(16Q2R"iTqz<s߽gI:ܱ ,ZA> <?oa.l p`4*=S.plaw  @#]؎y`Ӱ{ _Ůjj{#0g˰`:@M`g Wkyk N-,4TUgЩ VcCs|_` h eL _n1J ~j89]P2`ܙɿkpGY-[ TWaH$( =%<#km/nL `36>;4̶p0|-dP *~DZ$7~t } % wɗC;S?OGT_q?K@v#犓elI3]GT6~&6̧2;'q088cfl§jdMj'kޛ2>_/V[ IENDB`TreeLine/icons/tree/round_minus.png0000644000175000017500000000575411651514477016413 0ustar dougdougPNG  IHDRasRGB7MS tiCCPiccxgPi CD HI$Q20$L "AD\]"QP ."**}xWO?UOU x"')6 ug2  VNj?XKލHxI<.nQN[XnWXrfZJ v\a:W8Xȑ8Q1x_.GʄBSBZW6k57EyZ࿝%*|qiq9X}{m;`)^S; e7=k,@)'.h@@2@M  oBF A&@>(% 4Vpt2n`<0^y,A uH2ؐ5yBP@<(ʁvCP)TAM/92tB,7F`L` Xfvoc8 ΃p=| /÷X"lFBhlG riE~"@( btQ(WTJAmGP'Q>=jMFˣuh7t0:Ga`X3+&ac00CI`l6[=Na8%rqf\n7[‹xo<_owK `E'v*q"B4'; Mr IH'HHIodٖJN##7ߋĸb;Ī:Ć^Que#%RN9KCNjk;Go?'>& A0H(h!1CR5NT.5zz:ICh4@Jc,=^H>HJKJn )` #Q8e|R+*5,(-'m+%] &="Q)$ _S,JV[G6SU99G@#yX^[W>[BB9EbbbMZ)NL $ӎȬ`1]ӕTX***m*OT lh2^y5%5/Gxuz!~E FN4ˍjak5m4S45kaZ Zj&ڱwt`S8:CЫWVկ%fN1R)ᕌT4tׁ2fYAۃ(7.=D8~HPYUVYR*jھFfoa#GZkj k?;Υ^XƱ Ǜe ?=d,\̞ ;ugǟZu[m/~ eǙ޳쳭ZNk/:vwv BιnMW_PCYuqR1'{7>|~OUׯ9_o7,nɾyVǀ@m탦wt5=fgf={ݿ5vdh4`Xؘće44c4s~y/^&\SϚW~ysޅu O%[Z,x/A2?a?U|rD. r \@"D. r \@"?vnVccgy*4`|zTXtauthorxsVZ!P,FIDAT8˅KHUQ@?9y~,B B8rJ&nj'Xgd}xR̦{ .غ I8C  I4L;@BۂI\} o̸ <OT{YqQ&A|0)6,2n PKUvˇcPnNQe.[vX'js֋쩨X-U2`٪=w~I^'#DZVޘ>=խ(J# Yuc] JUe͇w6YU(  c#<4ǯ"yq8%Eުc3Pе ϻi 2@:e`ZXyJFl::)DZwNAσ  br\fTs~1$b֩d7H]d$?`b>҆m!vr&V#>JfIbo"4}/\U[ĄjͱI3 $UAA4tk#@QLkJ dNΞ2EmH!gC}^y۷YN_@%X>7.Zn+wXk/7-h 9wbxq`r:=zў>`6&~*̇Wߴas7R9ziP9`_ nn\#ZtҪakY.L7q~MCH IENDB`TreeLine/icons/tree/star.png0000644000175000017500000000126211651514477015010 0ustar dougdougPNG  IHDRsO/gAMA7tEXtSoftwareAdobe ImageReadyqe<DIDATxb`>U1_0b%A T:be lzZ0R@8 ``cgscrRfK@a5mC#cF  OD- u1A/Я9q3 "/cx>߿ _|d`` @= ƿ '0;Y%A}"@{N2~}Fo#P3Õi>[ha@}C|n+>L?ڼçmw2, J:x?Y"X Çw0l  JJ ~}'DR<?@(zȞ 1#ïMP f0=bO_93 oB :L @5pO4\`hՋv_  x NdP>05 :+Pρ'^&ba@& * Nj83s10r20022۝C xyBf< M7_b $Zi1r2p2(++13mNzzCpH 32030|< 60230J09h0wf@!,t3 O3`yX?202nеh. >=b<3C+_$.103001"4'}dI?=pk X^1W/0<8pvL@ ;lxA @6Ggqo`n@W1qa@>! N1afgX<0?}fA(?<&?#o x e03\xᳰ./(V|}E@DH?#hݾ2O ?}f'-A3 ,緿 7oas! 9}.T5@ =Tf,7o_fx×o~]𗉕Տ aY^^nIVUU 9pǏFa? `(Q.HIENDB`TreeLine/icons/tree/question.png0000644000175000017500000000135211651514477015706 0ustar dougdougPNG  IHDRabKGD pHYs  #utIME 1q,(wIDATxuKawgFS#aFC:P@iUܹp^Z¶pmY,#$hS"[:::;-tF*=p6眗8ú$$BTJ܈JV y*} ]XS]lH4cjTRmPw ]\16˿ [|]uлRXe#L_GpDXZfAENxc 0Yc_N=M$d -8~G`;@Q#WP0*pa2, ڤw vE#0t Iȱl @h]20Q]E_Bрg9h39tCKajݽ-ucQ%\Cj| B&8FHeD!F#_EpHʚQC Kmk̲CbQx6G$Ɲ c[{s*^S9. Wnu8?&~lډaڧ%TҼe)Mbmܛp;iCwB.~+b<|ǂӂfvKL-YHC. !e1]̕2ekbڸs;*IENDB`TreeLine/icons/tree/bookmark.png0000644000175000017500000000035411651514477015645 0ustar dougdougPNG  IHDRRgAMA aPLTEXXX000wtRNS@6:bKGDH pHYs  d_tIME  A\CIDATxc` `46VVV@@$2A@DH 3Tuu  f'?1if3IENDB`TreeLine/icons/tree/x_1.png0000644000175000017500000000037211651514477014527 0ustar dougdougPNG  IHDR7gAMA abKGD̿ pHYs  d_tIME  1,{IDATxڵ 0 O9PXA20A J 6Dbvx LPPR!ޒcw`@r&:64BF+ oV<#Zq%)]1gUrh`L<.IENDB`TreeLine/icons/tree/x_2.png0000644000175000017500000000146411651514477014533 0ustar dougdougPNG  IHDRabKGDIDATxڝKh\uNΝ;4L f*t1 V("ZADNRRpS7"!5vFg2y8~*v98SBxt:=lP3 cf}BUU^UՑ zϾ0W >yWo bMU~鄊uevxq4x&aud2yvǧ$icrAأ}z֫l,mǛыTr̷?1-_[[anv7cG>z~%ݽĀWdOui??5#]Juw~pfMq| @ꌙJ^]S>Ck7Y-^ͫ%+ .v8d YkN[nW8/S3a~pq/oYnW{aܝ i" ˰nclݿ(27Hߝ2.q!KJp$[)+f ?{'gl6m( VPnȱG{ty4o$-xJ).$C%7(嫍}DEOʭ T⽷D9C5-`_nxZUoM>8ʽwIENDB`TreeLine/icons/tree/x_3.png0000644000175000017500000000157211651514477014534 0ustar dougdougPNG  IHDRabKGD pHYs  @AtIME ",ᢂIDATxm[hufg/&K.hR)mjԂ-TA` VWT)*QV+} ڠ^&-6YldRsٰl6dgƇhM?9ps>6sy[q>\\q]pv+Vn!y\RƝ4|,%)ׄJ}AD|sv u8Ͽ`>;Ux̩M%rwfу6(~v/NCNhm;L>+ǩ0qx G}><ݷZEFтAT k5_Yoڄ .B3e&ߦ}a&B y#t_~?~ɧWp2;+՝:Х"iG&cx;1&X:O]Z, -7b 07ov G/7W#\weSY"W|i@(dCuH^UtmleA4{}g |xu~l,_4 gk5Ȭ*8>utS8TڞAfя0?-;vt-ѵs E2R)X8w.S[Ӊzݙp:}ݍdׅ$dL&S [SqW65P/~-;NĮd2ʆo6:+՝.GDEs `1X_&јIENDB`TreeLine/icons/tree/folder_1.png0000644000175000017500000000054711651514477015537 0ustar dougdougPNG  IHDRabKGDCIDATx͓MJA .\(эQ"3i.WzvWQ*3Ӌt!` n,,,!l^!rT_e xO9j"2P@6N$c*$I/V=P<-9 ߢ/6FKɚ_ 4T5JOO7FdJvpш}" T<<=أKF͚N\e!nUPW_K{G;8|11>S-y !LdO q#ZR|]({Oϸ\|қݻEIENDB`TreeLine/icons/tree/folder_3.png0000644000175000017500000000114611651514477015535 0ustar dougdougPNG  IHDRabKGD pHYs  ~tIME ('IDATxkSaDmbJk⤒E!\]Dpܬ (JAjH2钥DJn%ښVWo?roס&&9pR E 9-X56hZ'ϒɿ1@"'9Z0l^+V6$-,MOjFd7{r)2K;`o! cyJGS3nmZT,Ɇ%m_'j:^Z٬S/>U qnVވ|K怡s<|rfQz&x#Ÿw`,taŎv2,ӵ[ 2ܩcT&E c<RSlHӸ4S9b,$2^ j@diY7͚Q%df aA}Oq7 OZH&^A\7e&߂DPE;NY Yk_ 06\s)Ћ9ZN2 m ^wFMWA0⥌OJbCb2W*xR1}H8Ғut kz gȚUa S78J_؟uLV̉]!,zQ#cXY\OHV87R`kC[L2 y%9S%RW} 0'SՖ*ccb,Qn,4aZ[l.ׅ`q%=ȥ\П iMWB&Opeq QT,`h1 A^jo. TUn1\UMu ;w_v%9ƛz@֖ ;i] m̙ci;wWX1+R+'ԅnp۝g1 m0d٩7{u5r|TU22.: ~CAǛm=Tu/:]#}ܯvDB/(++ϣ`,SUU^cnnn^ӴQGi3Ųh~8>jL3-ޒ=9p0F^aiGjsp_k\`Vr}D&<(7`:32b6n QIfI-`dffN~)z*%Ru$ ;4|ɉDB*.KZE` ۥc!H&!X]]%/\v.: Σun+, )\N'"dM OPftt -# !4$Jhq7Jܺ\lwaK}I$Wd~*)/Dx ݽd2 jtM %mYsl0 7,|$!"e yϻRS5@9{By IENDB`TreeLine/icons/tree/tux_2.png0000644000175000017500000000111211651514477015072 0ustar dougdougPNG  IHDRabKGD pHYs  ~tIME ԻsIDATx͒kq?; A5j!'+![* nVU'$,6EAWB{rK~.^Fq?$K'@2 @h& 3C2prx( BU$pOK$ EQ, MwH#0sgQU˲T*E"H;5z@ KY]}I\&LdV82躞qbxG^xږp O9zJaGwĒ]bLs5`"nL$n;Džح}H(x[Ӵ1.b`2(,\yGSx$觯_jNcA!jCaiYVVjf~dt:i4 ,뢪fǀ/$o8¶my}ХRiYsE}/IENDB`TreeLine/icons/tree/print.png0000644000175000017500000000111111651514477015164 0ustar dougdougPNG  IHDRabKGD pHYs  d_tIME  7dsIDATxڭnA%JOI&!8e\˼}^:"PHMb)9zNm$}0""y^/y\.ur|ܗO? #E jшt:r f(QǕ;'ɐ]q~γ-$oMl?nB"Ji:l6x<'H&}>h!P)t]G ^a0`g9;B*| @q0M4X,m7"B\_/V6+zߙq~BYIENDB`TreeLine/icons/tree/wrench.png0000644000175000017500000000203711651514477015326 0ustar dougdougPNG  IHDRagAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxbY --_턦}ųg&~}222LMǏ_ ~adQ 7WW[E`xAggLuu50{υ̻_Z͛XYky{j~NKEE@bfx=×o=93}gx |54DD}``exÇ/ w$O1r 1& A((9: I 14˗/@ ?999yy4~BHL0AD$D}dexm_L/@ʲ[?}ؿ/ܦQ _ L?Xra@1kk[ǿ^`GC_>*gkƗ€g`diʑ3 l322  ¿ r7NQL[}0G_f`} ȸ ^`6i9[03gx#KDpzZh-N{/ 0{~ax`a&IENDB`TreeLine/icons/toolbar/0000755000175000017500000000000011651514477014033 5ustar dougdougTreeLine/icons/toolbar/32x32/0000755000175000017500000000000011651514477014614 5ustar dougdougTreeLine/icons/toolbar/32x32/editrename.png0000644000175000017500000000313311651514477017437 0ustar dougdougPNG  IHDR szz pHYs  gAMA|Q cHRMz%u0`:o_FIDATxb;pΗF~! 0 Ad]x??00\ w⮪ @xKDRp]tmG@_@?0x 7oĚ @L$Zƅ @K6`20|˝pc`e(Q Hr(\Y!+?@Y #% &* (n @,X @?c|;si{6 y3021m&@?0',W_~2?w@䀿ԁ ?QKۖp1 @D; 7h`(1|N '+3 Gh~2M^5Lfo e}'/v@00d- "02a:'330{PyFB! r!0 JwV0pkOz3ܿvA4/h'lA@q( :/O8XMr@ tWn(>###!lw((8T^fi9Nb? /gWP4  _ ?jH #@tbnFvP_Pd`xCA_%䟟_&_aɗO @CZ} < `ŋ } ~f;?m Xp=[ ׀ 1<p AXCb^e JF9~1e8bwЮ@dj^KGO_D ObMd`': P(fR(?@`D{{371(>ʹr!?6+(q' i D |s-( s;; ''(MR8c X|/r@ Ïj!| h)++$tFlFbဿH ,LK!gZ̕,XJ1K!pŰm< 6!,ـqDv@`~c0@(0cӀ9 `eLC; FX>(`trIENDB`TreeLine/icons/toolbar/32x32/datasort.png0000644000175000017500000000115211651514477017142 0ustar dougdougPNG  IHDR szzbKGD pHYs  ~tIME ,ѝIDATx׻kTAƠh#jL[}lH:! 6X 6/BPDA,D++ ؈X| kqgٛ|3w朙:mQ!'['.hV'Hɉ]"ޤO @TZW!tON9( I&8q[|QF0_6^ i#-:RZ[hiJw<*>0qŏķXƹ f`.9_kx0A=KK=0c6Y^iJ:aTl]@d%o>%.fcCe ؎'+\Mx' 7k[s~7f}Q)EditYrMR!;F Rq Npm$X@d)0%])`2ޓr >ddo~YXw d*~KQCѢ~:Ϗ~d*/HtEIENDB`TreeLine/icons/toolbar/32x32/viewtree.png0000644000175000017500000000202711651514477017155 0ustar dougdougPNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?@bWLeÿ? e#A?,?H_=`q<\ΧZGLDـ5o(dl@s-W gR8TMAFo0 DfDA?!|8H- @a o#`" _Hni`H_$ 4!0 PQ x @X ,9r#B⠿"ـ Dп!x`a Bgk`9t\߿~߿ۯ_ٛ FK@αbabf92?8$C>C 9d@,:Q  _`М| Hʗ=UiDZ΀bb 3 `3|i9( QԄU<#?rd,3 갊߯E ( reOd@$'BP9ff,5D9'Js̐@$q#  t!2!@Dd˟vJ`-&}"1Q@,sт o vzt Dr\il*pϛpD-G  lW.[4}k4xjG/㉱ DHr|5`GGq!(rXGZ9ځA F$@\`t ށlt{<=&49B@n1?1@-Hzߠ w @L   yIENDB`TreeLine/icons/toolbar/32x32/textaddcolortag.png0000644000175000017500000000365611651514477020524 0ustar dougdougPNG  IHDR VόbKGDC pHYs  tIME2J};IDATHǝmPTܻ,"]⊀b5DT Hu:jV85鴙JC?ؙNI8ZcĉNEoӤ*P@ ]~ط 9ss?y=gG21bFl4Lױ֕h5Br 3)UӌQ'vU{-['<Ƈ3' 6 O| 靀c]Z7xFVe%R2PMH[ BG?|!#UGA"CBęhss3yOqBp R !*jF ao_ P@*m j`˭MkbAXuM4m)$NǝhF   3Oܘ0PJB'$Ghe^ֆ \dc`G6-Dvv6p8L8曥 ŝJ Ip]"Q7Rs6犜4ӹU n?LZĄ z)TMxI{!%nIIw7mBٲ%kpBD )% >}n*OF4zi@s< r|y9 c9~U̓7ôea*%!ÛAٕ1F=̝)2=IH))Q=3B'_ _uyulz6p]=L[ [=.xBs9*(`pT2Jώq?WG]} <*֕7aJ)B%X;Œy NQ*=Q~«GBXG4~cZE RZZ/BQƣz/%}h ( :֛ XbXi!BD'$8hifC)HG1#$n.qf S\FY_4! Elݓ܈d1[0]BuC'){^|)3gL li뜌uRR"}_VmYCU ~7 <g<5_M9Ve֜y,&g"sܳߣ΅i" qs$hxgzqӸF*\IENDB`TreeLine/icons/toolbar/32x32/editaddchild.png0000644000175000017500000000154711651514477017733 0ustar dougdougPNG  IHDR szzbKGDC pHYs  tIME  33̌חIDATxŖKa?v,փU FW%"\! 䦬XQC+5uv̺㸻Ȭ=w= {Lˀ<t!F1 bn{V5hA7Z[էә[ɤ6 P-s'Z*cbM&d=r ڮ ICx-C)8:LMRi,/ag]]pwnCdn]^Xlu]4M7@WܾA ӃB"bn'kk{h<f] GT;$z>yU75(tf%,m [wnީ [w>_H}eJ2_^5›ے4US^P`aP/ёGS~YsEG%czl0d2Td[ @uMOMN7ۀ@g%(-c]+u;K`yҰHK2작 .v=+j PGa;z JqApR~v4M ]BbHWޙPղz7MNNEP(l\A*aaCVk7/l𥪪72WN|zZK'~cM311 h)&^%s\?e_@-R; B9rRAoE TȍY>%IENDB`TreeLine/icons/toolbar/32x32/viewshowchild.png0000644000175000017500000000033311651514477020200 0ustar dougdougPNG  IHDR szzbKGDC pHYs  tIME 26>hIDATxA  |XUK'ˆjɒth2 lWr$"Ѥgh*@D)9R "@@@ ~;DF֑PIENDB`TreeLine/icons/toolbar/32x32/editredo.png0000644000175000017500000000222411651514477017121 0ustar dougdougPNG  IHDR szzbKGD pHYs  ~tIME 3-T>6!IDATx]h[e$M6m&uVp8a9X 2]ɠs Sex0A&j729MүIs;Mc`@xHs7]^"k@[׉Xx^ c4TT{P}MW^'^uNU3-.G_)&>.c~de/CLdU.G ~(dp9D/6@ɖE?:ġn'ז"Oc||)@@yX ꑟRs5\bx4(I9܅fJ.Jܮ%!E/CzdX|% ljn@5ҧIDATxڥ{lǿ3;{?#DqE䠈H|U ibdG DHH TP<"9&JI)Gh(U]G䀠GM@LZ`g>cvfw87LF;;|~6jkRG̐2 JJVK>7HJU[7<j|6RX_Bea沠PĨ9QkڙGlz≚-N~҉ޞ틒 --ǶxT<& :(AJ)% EvBfR+W.I~JX?oݶ%K8̛(: ]i  T5 r9XT2jղvh9ad~zh%f2y|&C +c(/AT; f|+k_?t" !\Q8}pGbwrFpDC0G0GY0v, h)%Z9n$0 Gz9 靆!TT͞0vf~Dd 8eLfG)5wϔdsn$1L!Pe#J(N,G@4z~0]u1̀xiRԩy[C0R,#s`gy3A)FV|MӊVJ)a f 0]i04 !Mz OF}7 .lF&"@M5Q4Mw]#PJAz.G$1FIXBbp*ʊ Ə%r 堏F )%{9+uq?Y5XV t=N)t]ǟRpҗ;?/  3YRW #?˒G|sC@{J+1(M-6G_;Y҄Iu L~hK\)\))w=rTWW5@K#144 ˇBIUX*p#[ض[Cĩ*U#rv<>qx7ID,!c3\/k~n׵>cT8Ƣmy~2-Ր!VּpZ"}Љsp}`OJiȨX%ĖJ1* <9={,~*!_JTyc= xԘzfe,f2G؁ f_n+":^LdM?*پ)U8\$<:^sjttMavkK+ ֬^═=ȧ! pw+%ǒ˛ǒyCqnNԔ#=ǁ7E`x~1{o_[S&|ӯҕ\-Y3߫L],)M.;)bMZcIR?i(hP |i`?r%Xzc-g^m-|-# mhrd'Wj$_*Ͼ<=Zx藋-ޢ3}+ˆ, uCXmYT `b$*ޏ|YcI0L-i*/X4t编q8n5lѕ ^͚΢iP]@U\gu_.9Wg8smk?H+59e 8x  /d_g[1[ʿ%{33ŗ(`6Gj}ůqz̀wZ <|[}"^R^ρ|2gžO`hI~UE!syf$Jr~ڶ-Zz6+? z|@oah*{]wٖ:TA5e t:h,˒LNfB5P_ҟR0蚲 /8Rk S̅%'4=<0 U۶gB۶,+v3.PٶQUtuHyBJxmQʦ6I|]ۻ33sNvCHR#}3].;!<3{i8JPr_p8d:V&>< < Lv@zz[>@o8w?N˻`x %LًiEYཅ^*ୂz/8{I6Z :ٵ=ı]oHNKɆ .aLrY4Ȼ6Q <#fUyT|s|thRb"N*M6s5ށɩ߶_Msb'}DRK2RBsh:%7#ޮh8WJvҿ@u!D4qo&& z 2҃;f lch> y'EݲDo zaԛLoivgJ6A NZ>tאOª67v@+Mª~ |ⵚתOE]\zI7|^}o|<_m +Mڏ{Ph2z҉rkPW  '40t4[s){>T/9_r@!N;p8 {v\<~2];g\1'08N8lN-R%_ w "-40}̥O lenV״cY>55%tG0/铖!7蠤7~a賶Vk/d0{ٱޚk~(lLB@jZ=<>5/Qbd0ON JїfSSSZ=āt1 yaz:񳄕,|dJVկԜ֭a-îωF0ݮ9/B6z:IENDB`TreeLine/icons/toolbar/32x32/textaddunderlinetag.png0000644000175000017500000000301511651514477021360 0ustar dougdougPNG  IHDR VόsBIT|dtEXtSoftwarewww.inkscape.org<IDATHWKoG=^xw.('A|e ,+($RBp)E"H&(D, /ɏQLW3==;Fꫯ/wpKWs2#IңuǺ7y'_M+Nl-eAcnl+,N£[o🌿  ̰OW:4CNL8eC`9XZA`Ds`c̱q V~Zۭ[dpns`T(Z4slefm<=. {D4#ұ0}$z ӿjXV]psD!i=B(57qS+1ВVD ! GzĽ[D;"bbbR a /_իW!DVCT–͛q:{D@mlA2ݻo\Ç1==i`tt4 g#F&YFaa6!|ccc8uP,qYdhbq9rq: EQF[z&e,˂f "b(ndLQ,HM,3`620I)AADF6yDd-K%nNŔ^\ׅ rzvu {uʺmfF&moy>2 HJ =2/Ԙm#J} dMb 2OBd2!=BD!DApYF>s"eY {)% ڰXĶVHRBx)ضCPއbLXP AHp]' kс5@ZƁ!XuC@}}}H8<*2"Μ9! BM GVu=٣ ZR tJz%thFtǰz!p!(P4 'RJzx=@|ZUw?/X[[sGJEQM{aߙ ~cYVS^ȾXyZ̈́5G`@.@^%_8<nFm+wpU7٧+k}]cK3>_Y^h\qofr` ]NvpK+ގ>}`r< Ȳk{W '߯F D˛?_׳hpNRXO~ p≛l}Pޑq]q]X7EJ~0A߽(bsόhkWQ.1@{MM;YdM[y)CH>o.׏@`_CM$2,ilwOp#\3(=ç^oRGUVyt*wu2}rZ[TTx}x)Csk8Y2m+,+m7H'ҡxs 밐O>),&gCW(,t*XO ![V@X @}G;-~G$H8Lǹ # BZ/ O@&3qEvtd̀9 bp1S-f@2ȡ ^7merڰa)=4㓮Kv"k Jm}`fG;Esf:޷l#taPBx  GP=xz]vr}JcC=F"6 > 7mAJH–eJ#3qGճ`)wKB4ilA݂i IvC, ?0< )Ob\){Rt"]L6,z@/ t5̴T2QW8BAQTB<SgeۙI lDd]9ވ, 2wrºfҠB6)K$=!GԼZ` k+_RzRDZ3H}+{{8pHZz&\X@ 2a'`_jΎ2F[,k[yXƼC u"gꊾdj7 Ή26IENDB`TreeLine/icons/toolbar/32x32/helpabout.png0000644000175000017500000000233511651514477017310 0ustar dougdougPNG  IHDR szzbKGD pHYs  d_tIME ,P6ajIDATxڥoTUϛt F# jEDN[H [ݙvo02hQ"?"/\s1wLg=73{9sV_wTIsqMXzI;N䌨&wcBw 9D$ίxgYK.^ O+7f"ql0O:航x!uUYz|(2<ٝ2rjAU84OOϸQdYp/e_zs09Qk6c~%O!Qm;NvɴI%h>&x#Ÿw`,taŎv2,ӵ[ 2ܩcT&E c<RSlHӸ4S9b,$2^ j@diY7͚Q%df aA}Oq7 OZH&^A\7e&߂DPE;NY Yk_ 06\s)Ћ9ZN2 m ^wFMWA0⥌OJbCb2W*xR1}H8Ғut kz gȚUa S78J_؟uLV̉]!,zQ#cXY\OHV87R`kC[L2 y%9S%RW} 0'SՖ*ccb,Qn,4aZ[l.ׅ`q%=ȥ\П iMWB&Opeq QT,`h1 A^jo. TUn1\UMu ;w_v%9ƛz@֖ ;i] m̙ci;wWX1+R+'ԅnp۝g1 m0d٩7{u5r|TU22.: ~CAǛm=Tu/:]#}ܯvDB/(++HAjiJ-P BNllc^LG:ڙ?w `5W~iѢE v7nD"]v;cƌ'N8xر2jkرbɂeY,Do^xwG88q ǎ ʡ!soll|C d2)w<c@lfwM:}t)݁yrW- n۶|SSSX@}}c@s6lqS@؛T'T +EV rٴkka˖-R"u]TUU@!EQUUY~'6y͞ST QFnvD4/HU٣Y; DJ(X_ !aŊGҸ I])%te֬Y7v]u4 @a󀸮ŋk\Fs)ȍ"11,TZhx|vIIcӧ*LXtWu)--%7S:61"z^RT%TUU\iӦswTB$JP>`HH@[[Ο?P(*t],4M޽ѣGۆMPTDIDB BBcӼ}5O:ZccM ݶm8ugΜOP4VB$Zd@LlU -0M?HӸ ۶I$C~ - +?8D+##ÕN=iS=s8 d_UʑzW P )t{*Xy6샢޽Ƕ<0>D/W_nf+8c*̺k^{6ai L/_xW{'Ι35kHG25vvC @@y'}Lon=:w\,yr7k7̃#Lڠ.$tФC'&.hFQ H)<&5MoCK _ pg5`2#Ͳ,Wut]'λYzMGZ4 ̟?ۦMF("jժy֭{>@t:MGG===~#(#lڴtSSSU,Cu"uuuYfySS/q'`Y/rrʕۺu W^u,A,++++}DQ*++2e >qEall'O|WWWw3Ƀ/"FGG?d2wTUE4UU4MڡR 񨪪3eeeUV=0OQŰ,˗/>|}?$Y2HfZ]qG)_ >dűUΝ;>II{¿?>} BjNEff}&&Zbb ׮] 131bASSA;0(@sJ`rD^AA /ï_ ~u}# r$ H X(S`6r fm` ߾@ (?~A- >}-r`ї`&V0(@ ߿;P:`˭b<8!o0(HL,Yl>|pֽ@鿰, @Tp$`){{#=ee؛7m;u˖ӁJ2H@P+ l_b W-{>9ԙA%W`?{Ly>`vU s~rk4cu@TFlU2|z{G?}ֱ N\ [:2]\ۃ;SV}ǎjiZW@(b7×>k Kf~` T32s1XaV`6pfgPfKY]ϋ" ~1YZ^5e% d/c_nU7_  )$kN!N@~1HH;G&w[s ^13ueO;[lT00\ h?tuasa`Wg`0PO @(g~3bЗb pc+_}];p>FK/ /6lV%$( K^6GT BW/:(KPAsp.æ/| 3y=%'YghNB i MFM=?py޳?v?x?@09D h_7'3n'@xrHG `h93ES61e8b-t;6?vel}BЇd7߅ns߾ AQ%8LٺUϕ ![@*Pmh'O zy~_#w~.cQh@i#:=jÍYĐ|a5kfCC4u@QkƳo ~~R&@1׵_uCG j8$ ĹkUDqbxK={]x9jAv%z*IENDB`TreeLine/icons/toolbar/32x32/filenew.png0000644000175000017500000000253111651514477016754 0ustar dougdougPNG  IHDR szzsBIT|dtEXtSoftwarewww.inkscape.org<IDATX[lTE9le[mKm,h0Q016&WMPyE>XtEC %.[ -TzK9g|8K[ڦ;33GhdQLk0hWG5+HQPEвL5CC,^CV[乮|mxO8ArJ!@J1-)%H |>A%Ykbn 42SI%CMmj&Yf 4 nxefP͚/Ss<7M0~Y+J6űPS[|n+HO !> ~,1)έ+FPS[֢K!L~22 Y~%UZPS[1R94ݡ]p5()h9W?[*Er`6˳y;q#Ük?Wg@2(Mp]/D.oK+H'=b[+(g.~.Լ_~1&;bAW90 )& ry'Z=8$Ju7觏tj@!(P ͘A_S<$;o2&D 0!rX@]Yyܯ^}` @G@8*^4' > _&?r \A;Q !ѠtW_" 0Pri 0.9_|c$3ߘ!@Q(/0 n1@w@gXDXӿVFVvfF.Vf%" PK' Kf9( \r o"cHQq_$=C [^0 BXkJP(@a?H R F`D {@5~ǐ/ ¼@ox `9w`EjAG98,vF"*(@e,֬p%JHP, T2?xars!?PAB'@PaT@oC* >FNp(#CȆ"!i l0#3A}c``c?C2`QBq}+pb{Fky^??ڦ F 10Hp7@ |]7PJ s @7w@p RIENDB`TreeLine/icons/toolbar/32x32/toolsbackcolor.png0000644000175000017500000000473111651514477020347 0ustar dougdougPNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe< kIDATxb?@bb`@b$FTcpr 9%e@Q70|ҟ? bؘP? 6"j 0(1(!) gxvn>x/@!Ӂch`⏗veaNA]7YlZc`pS0G lQqW1\Yo1ng b4@ ]޷B3e> Æ+ _1|j ?00\|A k^=c`Pe`e`GA3APBюr9f8-ČfV.~mb&@!7 ^1|d` aгTf1030 /i/kfkk/o3l@1#[#oTCHl)&ax+ã/ /d!kſ20|y mBX20q2(817(``3Fya " !o! %ws!Ce] Kb}fx wO?1rx/_)#1Nn O b@io~;?)K_o <@Sng i T'|ܿ Т<(`. i°瞹2Lcgx' -/& mB8?3Ř|6`Fϋ@y{؀1 mMθ ?7P3v8SP6}|P*$ d,Zh"q w3؀h@,`_iCi==0AD;Aw_w8Cg2p112˛|z c`bT%b4 L/~|`AT X~azflxVVp;LC:Gq(pܿ| Vbd@ ļ^gw7+0$xQ-Gq JP??w կ ֐ n1;O`OD^eg/^?|Q"l, K:)d>u٨Anf7 _62p˃?~23@1C+g2~1K~GODĥEX5Z;&k5T?2<%0'm{/1{p `uh QcxR3\`x-+?0xOw3\}ΰ ЫcC@_Z됙/>PR``[p"󆭈˽A.lǧX~g(a%OŸr>e'ǯ iv]6x143.[~RSK `sw} 3`Hea0c4cvg#ˮ0 fAppJ{? JzxЁc3b>r0p;r!// "3Dړ?a(@&9 ݦ pGel1 ~//pԈ f"[gx`+__&2tsS `k;_ #?6p1 y@` IhW`87oI 1k , _D0ր7XMdPmbdEXX&-:LOήƐ0 ` F2:2ܵ M, ɺJL r9uRW Pv; &f  +(}#PqZ0HI11131hT\`_o3l{aw`_:43#]:Pb`gDK~Y  }Kf$̈l1 @w Y f䚔^ IENDB`TreeLine/icons/toolbar/32x32/textadditalicstag.png0000644000175000017500000000254211651514477021027 0ustar dougdougPNG  IHDR VόsBIT|dtEXtSoftwarewww.inkscape.org<IDATHWoE&8r|8iQJ[5.RUT8pʍc$n\P* qT TPTЪ|DQPE7߻;gkS:jov>gjӯmmYN9,Φ`)S[ ͥc/T?O$+m :xr%m+=ײo= ~{<76]Z!LZ}cKtlݶzWs>}$F`%j_A);My\V~7\i`QS+r&QYfl}Ӱ ;\ЮI {p>Rn{h4P.h4 /yOg-A|J ζ)4 r!"RJj5H)}@3%A@غBZ<Z ` 1TٶB MX,f:?>KM+'=Url2)Dccc\'K@MKt*{~0i f2FޤR*@0ɑRFGG[]@:#M(xI J%ضj'O!ؤH@b /mBrs N0/7s; >Ը8p]~{ ,2!"q̌dowfpqpp!###\d#Pg J8$R%A^G*А^׀1͈9dy\8::&''۳==:mFf32I fyT5 -8-sa1Ή=`X#tk(RyDe dfQ!RwV[9'H+`H@A/P)Qh6y^!IR깶=x~ ݺ>;o_ڽU'u 2=^ВUJ<| ~H'=}w?>=In2̈́eK??uc76֏_i֭o\>}ń/#k!waMGja竟k(.*]?#t6IENDB`TreeLine/icons/toolbar/32x32/toolsspellcheck.png0000644000175000017500000000332711651514477020525 0ustar dougdougPNG  IHDR szzbKGD pHYs  d_tIME  ;gdIDATxŗOlw?g׻qv6㸶IHB-ZTM( . C\9pAzBU$%-&4%#)$$nj;z^pg8iK$~h77}}{P6S@(++k9I6&H|lpd8~ nZ;L5]Y@+ׁ?w_қ)6? <+2= |$ω <.ߺbTX77=úer8|8~ߒ{/pxh<&5PSv$$[| @v:RIOCBg1LmI@HԚMQgi`((U`v;֯ȈsⶤojCŪ. |ցfwI!y UY7L$@ԕ 'No$MnJ p^6 EOI9x8 Ylyr52~ARYJ QXȦ;yIŰx8pEyB8:$K/VCNQmaJl[Eyn_n}RA9h mJ:aGdZܠ2+L!$#x۷{^m4Ws|m.m|.% uEv+rL(J6Ltt )aw\Σ2&ٮ{5~ؾsa.qf:=Ʌ2GT~kзYApK-rTOkx<š^USt2VQ"Q^,pL.I $oOj66 kmm|ѹ^XP#>R jk nӵFe`:%Nuxp2hNr{q67Q@tsH-W"fȿ0Q-uJU!vb(R]T^3 pc>h1kB۩5q3M9Nr=m6Z8!Z=^#_.sst2C)-|lKTYE:CTħYd%2)p%NXcU$b{]=!2r{Sks致p?bͭRp,~Dݣ~"gOIENDB`TreeLine/icons/toolbar/32x32/helpforward.png0000644000175000017500000000315711651514477017645 0ustar dougdougPNG  IHDR szzbKGD pHYs ,tIME 3rIDATxŗ]lWckfi;V! T""J}iX`ɭPU$ @H< JWN BC !%J)Im)_κ;;3nkĕZ͞sϹg9K8ã;@1 !E;RL!'3H|8F@ ۸f_n+":^LdM?*پ)U8\$<:^sjttMavkK+ ֬^═=ȧ! pw+%ǒ˛ǒyCqnNԔ#=ǁ7E`x~1{o_[S&|ӯҕ\-Y3߫L],)M.;)bMZcIR?i(hP |i`?r%Xzc-g^m-|-# mhrd'Wj$_*Ͼ<=Zx藋-ޢ3}+ˆ, uCXmYT `b$*ޏ|YcI0L-i*/X4t编q8n5lѕ ^͚΢iP]@U\gu_.9Wg8smk?H+59e 8x  /d_g[1[ʿ%{33ŗ(`6Gj}ůqz̀wZ <|[}"^R^ρ|2gžO`hI~UE!syf$Jr~ڶ-Zz6+? z|@oah*{]wٖ:TA5e t:h,˒LNfB5P_ҟR0蚲 /8Rk S̅%'4=<0 U۶gB۶,+v3.PٶQUtuHyj޺Rw"y}53O{  #ѩԑsv8s@ѩ/WVl5Ǧg&p]}lzF$-_4]c& Q(;q MĶ.$$CI=IY0l*('rJz<"4殝ANJHFy^,5ȅcjyh e"QL3cƙwgqH'kI7Jj.l4EC!kD?wjߖ#'sdE;NjEMj1B(|_kTI\v~Pde|Tsc R)!({ r%XS%7 w))dS/:\;J58BYAC@|Fyz |RiM(52Ѵ=Y̴Rͽ@R(䳚dcȔE|/)E@I.A|y\T4J;畆ʪJ;g{GtZIlc5<kK6XVvc fHlH)r56je~<膠69a=QLfw :0N \;÷%eON'M=}~o[̅vRӫ)/bR@;<t9re{)=J:6ƻQ*G_:GH*nEBJ0;QQc#/.JǼ#IENDB`TreeLine/icons/toolbar/32x32/helpcontents.png0000644000175000017500000000306311651514477020032 0ustar dougdougPNG  IHDR szzbKGD pHYs  #utIME 0u?IDATxŗMl\~sr*R&FTi iQ$"@Y{($ҫ{񢗢HQuZh+;b&vR5-ڪAR!%XÒ)&K0ٙy/rhil5 LJƜ KlA,#oܙ ,`":yVBH,)|2m@` DÉGDA(]2r80ui:c.|.k[,qMX{J|Je h |&x.F 2]4Ft?=چM4}RgQ7J| Ú=3} a|O|*Ǝ֎3zyyi/?ͥmt,^mDssA 8UHe+PlgΡgƨWn"0jv מwT d/A"7pk g;q!wZJ]Pxn丆S w;7pmY뫐/W}4~Ʌwu恳q('4MnsNn&aYP,Mpʕ=h<fM"C0S)i nBmIs|X%V] 7,5 $+Rzn : IMFy6|hl;4 %%(|kzTZG%W@C?)AxKZkQ1"5e1PǏ_=WXڄ(^ zuB .K)~Fwm[ʩG@x\AqޓJOJoۃaI|U(g~&"mk|L K{PyD4ug$ki e6LDlI!I٠*hC;(.Utʹd[5,%#ֿS16P,y5!0@suqfu+Ǘ823FOHd 2Wj'fNH.+*='~^yU #Cm2yĥʹ\~@&>zT.G29FO_ِǬ2L&C‘0Nkx rA3;fNHf$w#dRwU{iPr;ݾɛ#M@]񩜙>dM +cm|XSgφ~oӒɤ珆HDCu]ZH#~s#bL26$ yqP,qL 6伀;.&B\g̺][z^N[ K dw..z f\\ {St5s;Ft(_myCIENDB`TreeLine/icons/toolbar/32x32/editindent.png0000644000175000017500000000117111651514477017451 0ustar dougdougPNG  IHDR szzbKGDC pHYs  tIME L`~IDATxOkQ/&XqQuQ7.+D.tR?@QA7v!ք6 3I;ouQZ0ML3`.\x˻s gY>. x$I&N6.Yu'19:8El{[0w:BQc.&BzȖ yR)3%ͳ_f߯r{ lLN6-2bi 8%o}X5x(L,4Uc)҈O/=7-Eb"(0K(zWQ F͋+Oi'M:K? =C0qxYïj ;-UX/|DNa2pdbpC!obĖ -LSWku0B$qSF:k߿<ЮJ6&3|@׈ZHQ|'o>pt#|$H:~5|<\IENDB`TreeLine/icons/toolbar/32x32/winclosewindow.png0000644000175000017500000000333611651514477020402 0ustar dougdougPNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<[IDATXKlY~/sj;'u*Lgt !N= + fU6/;ӴQiLtAt"P eXy5#!t={LB|} 5 'BOhޯRq"1z3.xI[SS;M8~atHPkkr,[v!zA6Xb QbS8M" <{G" " <Flxx_ CKb1z1{O8V ;7%ۄ\#gm+D-H@66֛v`u5?5tG>"c dEE:6&޶*ks##gu1dsY!9GwcWDO&c!]|~H,#;&9]%rYp!@WWUôǒ{ Ϗ}E.>` >Bpxz.m4㫚42<6Z t= 85NyrA D|ԧFΎ"BtttHӆiۻ.ڡjAr:yuo\vP4M˲.4MHϧ q{5He˛L\f$,c2H&~D<~ݲMؖ0$I4X Ө"UELLW"=J%c`Du Q,u  ܄MA]x. B[8qmwt@ܾ}jօw>2@C|0=!P(ԎBa(;;\DD@Pч֭ǵ&3 S'8|S<nc؎od2HT'(mс"kKǮMS@X,4wz8,),e qߜj*kсP,wC0,X\q,G/?r9G[2KôNNDDRrʂi__,U\C(jތr Ls` ^y{{9~HDFb05JZ7+[^^[q`ip{Ku.ZcD$s3^@@Q!1bWηZn>{U=# A$lonBoPH&"&z$|p{^( \6뽷mt}.ߍwO;#r-(!V \{ !P/O==EUd ]Z;ҩ5 h'>3Ǐc쓀(es_./`B<D8QQR9pTD^Ft{@U{^PՍ]!C^jo8 `)$5IENDB`TreeLine/icons/toolbar/32x32/textaddsizetag.png0000644000175000017500000000276111651514477020354 0ustar dougdougPNG  IHDR VόbKGD pHYs  tIME/%t (~IDATHǝW]lUΝٶ. jj@I#M5o ^6A)D7h҄P!M/` IUnuiK{|ݖg3;3;|߹s.g׿V}Uva 99\>ǨҠ_g~#s1ɽ|EV5rƹ3KzJ57;9&B([x%3}Owfo#ھ'Ր&`>w& ./`s`Ș5+lvNzx{c׆·6jZ|/%(BDD#T)e0J%,5 0`jJ 7{QExC7(PB`c"R E,?D? !# ( hMளDM؈A( 8JYK+*2t94 uuuvVԘT9, EON jtBΝ;獇a/J`!?dW%FaMѰi78`鐽Q+/6p]̏ILӭ{ ~֖Nl ggNHW„7>d~_iIjru)~itOD`s WtwLl,u*Bn'Fò{QbIK.dZjIENDB`TreeLine/icons/toolbar/32x32/toolsexpand.png0000644000175000017500000000176411651514477017672 0ustar dougdougPNG  IHDR bKGD pHYs KY tIME$FLIDATxMhU4&iATBEsǡ/9%^z#zi(JE~M{*/XcD/ e.b"b۟7nP TľEGH卟 {(8m ݭ b.~'=ħ%qgggk?zr$AڞGǑ8A@M_i88nȵdGc嫛}ny ^/Oid !X.CI\vVpoDwIENDB`TreeLine/icons/toolbar/32x32/helpback.png0000644000175000017500000000313111651514477017071 0ustar dougdougPNG  IHDR szzbKGD pHYs ,tIME *IMIDATxŗ][Gwڬ#.Iv6P%6{D " )/-m +CߪTBJ""%* UԈ .]lM^3<ڎl 33yhx.PGA(ٴAw|tZ},L$X@:8K&+9K?0vfx%!A"8;%>@فFj,e8O[Io%|?'EDۚR}[L'5鴿ur= yx`MFh;<[gLn.,_XWH]ɍ;vle}nvl}QZ0rh>5P!gYdT3o0ϜTU̕9u$;\Z>5s$ >v]sCӗ ~u$?]oMr(C,)<}4 ل6sZƵNG{/P(`gJfҶ[f,rP 4-^8TX:GK6a{ˀm}]abe?Z lٕWWJ咋BfF'k'Z  2H*Yb{Ӎ?/u%^XqpEjn1bfA`jM(>VOrkWWs#-h@D=@ܥ|O=ݢ<^&] "؍$ȧ"F2Ru܍9Q=FsJxnk( v CzJCz18kQuz.ς@yY@aAө>ޚCg]7hM/]ˢw%em8^.]t a 7yčv|'={-X<AVOH_(pOG_9T_(mlRT[d2f_+oO $Čvq׬D旦ow_]o\{S&Pl>] @v>G荺mF]bGv>W7t/z ɨϫ Ǘ5vE:Ϳ9*5>)Pu>>.Ĥ#DϢcQSɭ IueMx Uj_Cw 6]PD T>{M]G WK>۴X-߇m (tښk{1""{I /KP2a wjV9 az2`p6jN JWIENDB`TreeLine/icons/toolbar/32x32/helpnext.png0000644000175000017500000000413211651514477017151 0ustar dougdougPNG  IHDR szzgAMA a pHYs B(xIDATXåil\wy3{o8^}=Ϲ9~m4Y@'t`lǁwVqp;T$C`u\cXY9^һ>]1~{@:hmAru&Bgc cce&>±q\%ۮ@ۮfe  -7^̎)"Z  !T|xg,ϼ>{ ‘cP(|я'ж 4ӵ[yN.;/A&d<!^`L%ђ#qfKȾ{Ndϐ}v}H-ܹRفkxDVD R1L)/ok? ;sfZ< K;! C_V,Q2pʾe- K۟#Z@r@/pmW/91Ua!ΩamC]LqNC3doB`9J%[ ضwl ګ $aﷻ02%r~c)&ZeI+mq:m$?KG@<FK̖ E\r|]r֎츺Z+\"rKΫD͝F*vΩiR ,2Q805i̓PRe}N3+ τ.xwFhn}7us$obIX[FMR*$bq'+|pc3SŀB@bSŀ-!GP2|" Rj,b5bÙ29א_%Ko.!VfB<Ԫ]KPkB =Qp*WBOO)JT+݆0[,RJ֞VZ Kk6N}& 2$>,?\ՙ\X odug)fNI"Ra8YHD$Z Lj s$O :x>~BrWւcRjuTJA:C+A8zZ LTɄ!{z;vr罏 cΒ}];HR H~-3 1e<W*ijogS2 x,XD>O5+ZiYͧe67.-3ΟɵG~/y}' HD&#*KfI'|'&HD$^>G ~|cοD?oyD-Hūy z..WHJINMꢊH(ntR=VƧ&]~15Eh-$RT'`xo5合VQ߆Zx]'7Sί0jC3I0χftDkam1JcQZ#Ɔ()88\bmT08Z9@6$X>L)4FJ/eMijFsͤҜt!^FCkR~@5&jA&3=pjBmGږ_nP?'I55D9k(HM*QE,"K:13@فFj,e8O[Io%|?'EDۚR}[L'5鴿ur= yx`MFh;<[gLn.,_XWH]ɍ;vle}nvl}QZ0rh>5P!gYdT3o0ϜTU̕9u$;\Z>5s$ >v]sCӗ ~u$?]oMr(C,)<}4 ل6sZƵNG{/P(`gJfҶ[f,rP 4-^8TX:GK6a{ˀm}]abe?Z lٕWWJ咋BfF'k'Z  2H*Yb{Ӎ?/u%^XqpEjn1bfA`jM(>VOrkWWs#-h@D=@ܥ|O=ݢ<^&] "؍$ȧ"F2Ru܍9Q=FsJxnk( v CzJCz18kQuz.ς@yY@aAө>ޚCg]7hM/]ˢw%em8^.]t a 7yčv|'={-X<AVOH_(pOG_9T_(mlRT[d2f_+oO $Čvq׬D旦ow_]o\{S&Pl>] @v>G荺mF]bGv>W7t/z ɨϫ Ǘ5vE:Ϳ9*5>)Pu>>.Ĥ#DϢcQSɭ IueMx Uj_Cw 6]PD T>{M]G WK>۴X-߇m (tښk{1""{I /KP2a wjV9 az2`p6jN JWIENDB`TreeLine/icons/toolbar/32x32/viewshowdescend.png0000644000175000017500000000034111651514477020521 0ustar dougdougPNG  IHDR szzbKGDC pHYs  tIME ./knIDATx C҅tGE߉p!q"u(j&dS^~R9OMM1YR~0u SԆ5%UU(-FZaki{G>` jBlfg#m1?o d(HߢUL,/o01eGyeK@ `,ʰm׃ .XYdj+6e eY-&>c@Mht!%T S(4͒x<%%_t/][~RjOVN uBh*~TROTނ &NMT;ԧc:&.*¶=SX'+ΛIENDB`TreeLine/icons/toolbar/32x32/editmovelast.png0000644000175000017500000000234711651514477020030 0ustar dougdougPNG  IHDR szzbKGD pHYs %xftIME4&ctIDATxV]h[e~_&iZUgEPbm-ݜ tAb E' &^xS8e q*2/::maҟo|>_j$[ox8}}~s[ P6}htj4@E-e\l)d’lF) q XB)z`Z': 4W6"1!Vx9.ࡖNt;0èR3X&W̒T@Q\錅BɅ,2 -B)ͻB/i@TE+3\Cmg.FؿnJNǡehr5T*.KNe*92A^y mF6ع_N\apmѼzYb@U4 `GCᷘgW_@-5[G]8|_A6-PEIq8u W[uuu֭[WvR۽JQq4L&$F^/nUUQ۶fad2Df~wg``W{=z߉DBDD:;;%}\Υi߿_ٳgrk>{sXhlllZ ],Hv`'@ %ۻZ"{A_===Alkz꺖D,X,&HD,˒֤(?Z.Â0h&uʕ+gRKD(b&a:\ ,e]R ,Eidm\.\oi\.8u'? [`v~zmXmۘ( 1 #Nx<8 lvY)xc匛7o~H$Du>|HWWCygz9### (uӧfWWW˗?5MU4m(-˚/E_]k^[IENDB`TreeLine/icons/toolbar/32x32/winnewwindow.png0000644000175000017500000000241111651514477020057 0ustar dougdougPNG  IHDR szzsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDATXŗɋ\UW{(c'fJE8(ʅ.ƕdq֍ &*BI !!CRt{\TwJ]mws~sU7T3P Xls)bѶ&;JF@zR2n!G<UC8'B!woP3^0 :bTƜ,*-JM sܯ%)WR0jR~XuM4(5)D~]/pImZӣPk^JG}짇xеg:`bx|0O>X nd:\|k_Љx-iR{8}n8/wلbxm8%͓g[AX>cyԍ\쑺f3~( t_~ Y}1oS)6,֙,,1Yx> AR?81{Њ3TSH*~PM3Mlm2^$u{0lI~bIEƫӎTxv_"ad ,VɭZ2[Tb$:ѷ\kͦQN}q=; sXƆKJ~ݨ$$YIlcVx(Y iͺQ-"j::F@MGUu vMDJ;Y,j뱘?3Bq99"2pϚ .D+Y;@F .A@0RR:I#ii-" XQ}u!b3&| PeZb4#?Dd 6!mA92~v'ZlʟC5J26*MyTaYt@=Rgrc$2BȠZs(ݸpHcna"f !FC2N8"6Ź P;vhKC<9YKF[\#B1?z.\<}ϮFJcPU=D$:#'N8M' Df/]] ";<{5g+yS ꪪ}/#* :뚜NwmlJ6Z c&uTIENDB`TreeLine/icons/toolbar/32x32/viewflat.png0000644000175000017500000000176411651514477017153 0ustar dougdougPNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe<IDATxb?@bWLeÿ? e#A?,?H_=`q<\ΧZGLDـ5o(dl@s-W gR8TMAFo0 DfDA?!|8H- @a o#`" _Hni`H_$ 4!0 PQ x @X ,9r#B⠿"ـ Dп!x`a Bgkcp^YP("#'6C1 XM O/~0hI| 0C@%Яg}p2J3`8 p%U 옖cš-1Y&BQ@Qsd߻"kkk4HssF"%8|q^WWĕ8|B ©-c9<@$q# 'rҍ@" Ҙ$_T DDY" ™ "Lc b "坝m`~qqIt"NN "JK+PBK2A@, DD؊U\N!X<@$B)!N!@4EHPK G;P7 Ln;_rDŽ&^mbY s#? XB/q{0 #ZIENDB`TreeLine/icons/toolbar/32x32/datafiltercond.png0000644000175000017500000000352711651514477020314 0ustar dougdougPNG  IHDR szz pHYs  gAMA|Q cHRMz%u0`:o_FIDATxb? 899٘x5f/@1rSZZZyu:y9 sGgϞ}|ĉ{hѢ} j 9 sh0Pp6@1TT`p̀Fb~0A7ZZdZ1VJ)!'HΙs8>c̫O_2` $ 9P8$H$ן֞{~,Խ-9kwJΙb#{scZ#QJB9'Z<PA6LH@3 ,+:])Bz෢7ƠFΙ1FB86Zz 5J)1[|I)0WJ26a`&3H>ft J K'|ֵ轣ME9gI)aCca!SWk cS)\_W@Yٳŋ-,,?///R 9 #恢nPf/@{ԩ_|_UU_QQ?Q`A쿛ONNA 9h׮]`Gxyym tuu6oY #X0(hAHP<'--_HH2gϞ/^;נdv#H '$$z t\4Pf/@$Baaa'޽(KA.''?)) \^|?@9鷂BP4r" rYׯ_a7n|"##GGG˅jc>X/_ 8JWEφ$Ƨ?rċ/hb?T2_ѣGAv#Xl@,;w}OPA31({\g2$..(8;Pc+oe,;Κ/_ʦ̠e۶m7l.̙ԩϝ;*xqD6&FP))(NA ̠J `Mڴi \kgjCb =7͛ @XnV`| / cK\E VOdP2p%0~+ #.o~O?0od``e`ge8C_fe``:rn>0ܔgpa j PZn.v#yA>.~.ySho@5L nc+&Q@,Z @6 0H 0>00lc`dd`xL _f ԇJHB Q~ @^fx ho?]8j3k1?/``VZh9ec`&LO݋>W~3+Aâpjs@P'P`xx ×}k@f:_p"T?PAA<Ǡ|"0 v 1%Û}~.7~b=8*;Q F+Ơo Lt@YvfvPrx@AB ,BPII|߄d 7$ @1|L= 2 ˿Cх+ 9>[ o`T;ѐv @,a /`I/?X &@` XN=bg`Y Wh 1!)A k-KA߀Eϟ}C 4?/+dy%f<3k=Pt1aNb 7`!1p=bl,~}}z@C3lƏ??H Rp9}@)? ~F0 r_~@a >" }ǐNrPe`,r|:XF +ý.J2|q`22tڮbՂ ?~f&ʿ( 00F]pZx:s%.[فY(8 ( @7x$(03]6 0:ñ^d3R l_p=, 8Pp} Xu0QJc'RT,_Yhod8̟JŠ2\V&@.}2(2ppY/Y Ь )x  :8[< 2~660\l?h-ήK17?_C *:(q'Bb׿> @XZ% _R UD!Jο(YX,02 ×  @0dO#$ ,~h3 eOp,%A W&` ~H !3N/`{O#(1޽{)|D~$Ty__We\8@10Hp7=x0lt/@Ճ::p ZSr@11 0w@{()IENDB`TreeLine/icons/toolbar/32x32/fileprintpreview.png0000644000175000017500000000425211651514477020723 0ustar dougdougPNG  IHDR szz pHYs  gAMA|Q cHRMz%u0`:o_F IDATxb? ܹQ\<9z *ǯ? ~a÷}: P\H cXvR@,$.^g/zAWG 3b&F ff&‹{$FQ@0fZ L3o3% &DtbA"F@ 3p20s3q33 -]1QQ ?0|'0Ү4+;01fx+/`E.1I%i2C+qkY8XXXXX@_}vAV87@ 3\ | \< @o߿3 013o`[w1 ӧ K'46Qa)s1!6.e@?1O`VÇ'|/# ##4@xC  1+0 h t4_#80mp A 0cQ+ j!+>|@ZAfa?+lr_Cp]])*``^ !g[yt 9؁ 033×A:g L@Xsp(LtR \@0s0ށ`@G0TDK_@o>2pKTV<~2&+DXyq? `0 > +,_u VJT? ;; #`X|`_?+E3+0{2s!@8n|ax5@{6PbfFH _>3e: :@VN\{/$ʠ" \2?8! %_b?!04ٔ3`t0KJB PTbac#Ëg9~r22n` r3=fï| 8f Bb4@8Vp2DG{3t/e~,6/rnv% .>\|o3B|ڒ aT$\p~F&7{Y)A#@66h#Зi͋o ߟ?BzXg > DGO8AMS ogxt!_eP"< ,z ߾f? ,=]\ +ík8p$?8?77p_d`ëԴ32ebZt' p5* j>~py/O`i|IV^ACK /Oa`q aum-ó>ݪMtU`!?#Y r<sNu fgf_V4M S/DC, sRPcf(lW0d7>PuL&&Z|g<=w>QJ U%K**b !Tc104 sL S+2B H2&F؆X)T̪P6(EQ < ;^ Ry%q' NnrRVBܥ[vP&ڃ;p]> Ax'2eSO&m qAlM*-qg9֍*f^{(3f}؂[^E})fV?;W&e9 %"Çfz#ܫ07BXJUD.*Y,662vʓu':=}ϟP 5c^s.,Gܤ-X6sg:EYL>ۙvԅ]f͙&gخ}XUVtIbwalۍl [('N/S=X1ƋIENDB`TreeLine/icons/toolbar/32x32/fileprint.png0000644000175000017500000000314111651514477017315 0ustar dougdougPNG  IHDR szz(IDATXoT?3;{z fmcŘF*$RpdPyh$KP*RRE?* D$ * }hpxpmjl` 6e׻{;3}p QHWgf>9g~+XN)N{.vݾ=:pاS E֝;li|j;kb|IJL1m-X[q_\( Q|n XkZ 0Ƭn`/`ioEhT4{0 v^r ԫ`6,<7Zc:E; uc1vepͬw)~ة~w3` +h&RDgg'Dⷞz/\P~#Gth{vln~tXk0 >}7o\봷wܻz#~}sDH)rcCQhJ\KC\B\XҢ%i9hiifeL&c```H=zwuX*G1y=G".I(I".HU+k"c BCY*Yoٸ{ٴ)%BO~q 818'KCԒ (d3 -TYiK![p/w]7|MZZ}}"b60_싌%B!|`sWI*mX N̲۷K}x0~R- }(PqcBsB!}}kVU+Tʄa@aXu|ٹu.:+"JeL !*;UZ\ɓ,Ljltt3gΰaи >-łd2CqK)|tt:˚e]d%u[wٳgona쁱mvjkISukkmRp@7͍(%rݿ ŋDSSsd5׻A۶22rrYuʕz.dݳgOR[Z:y症vھYh8uOs#쿁I{*/՘1Ɔad}?Ţof X]]_dQM?V9{ $H$7hm=V>qRwm( <55ہ*`Qa_YV IENDB`TreeLine/icons/toolbar/32x32/editunindent.png0000644000175000017500000000121211651514477020010 0ustar dougdougPNG  IHDR szzbKGDC pHYs  tIME K >gIDATxMkQ;3wƤ1?"-4]++jnDAv!X" .. XP+(t)!Z-E&i3{\(R$Mj9pW3s39;PBAz!}(|>-LDW:頸[-7dJN@&;^׸Z/փpk lvrq&:hmgn 4J@.<Es ֊")ueZ!)8qBv4'^2E*h:@?B,& aP㋻68x",<|wX#Xrvf2j8;,LU)BZ9 1ds`#79yv{-PUC|4qvei2ve)Gb ,SX_QJ! U&c9D Bc jsLQ,hԊ'N0Z]1jj`>R/(h5l5Ž' _OJ5j.wmyjNg>yY<IENDB`TreeLine/icons/toolbar/32x32/editinsertafter.png0000644000175000017500000000167211651514477020524 0ustar dougdougPNG  IHDR szzbKGDC pHYs  tIME  9 ifGIDATxŗKQ?wWDWw~ K֪HJRK=E D,YDA틄AK@o~<,*vMkvntum[ {wv{ 8LJb /OsR0$@LF *ZJk@H.4:DHƠ7,hkiwίWvM%~[Cevw"wC;ev궉 vP^),>U ~(ukzg4v.ٻw;vUֶx< /T6)T[Zr .MR$?H$t @; ڼ7zkkkcccî]+:|ē'H$t^Nb7zWW8l}[fj=O .XY>~@;ѨR*RKVP}s4Mh__bwwRWt=r&p pq"&0333$28pAB:\n:syDO(-NN(Mp˯Wm 7b qh,$uzʺ?Z#phFG L@z@b5PQ􂓫 c@2< ᆕsph/J 0G=`I`GQH!,j; aP[[i!NY]6U۷FGA=/ àSss<X;2WScnt}MgI)eՙL&xIe ^:H8@@+upyy]% s&l_IENDB`TreeLine/icons/toolbar/32x32/editpaste.png0000644000175000017500000000266211651514477017312 0ustar dougdougPNG  IHDR szzgAMA7tEXtSoftwareAdobe ImageReadyqe<DIDATxb?0222011CxZOu@@1D4PS3}k?6~:j?@1"7?ӟWʈKJediڵN|SJ}_X͸oUu>8 + uk2cfSdxa# 6>YbЏc ~}|ko幷ܞ  B& ^@o_72ab7\ < ~F`m>)]ԝ~` ;/y{Bן? ߾2|􍨸G`ÃB <tܛPp@&? 阝/Ý;O@9gݸh_0``(@ [4߁  汓_c1RB D$6G`?PDv+8) `64MeqE@L ^  r1E?} L,Z5.79P ĄId97r30i me'> fFv@C zzJ@G̘w(pO_!/ AR FF *&Aq@1!+ ƿW=} a'~#QA00Pf ]w@"<3Ђϸ-D\r2lt ǀh!!T(3 RG`?GcKTo~@:70};A1@f`aagQ PJl_e `V7$@ &&Dꀏ@W;a90@Q+$?YPCd_/rOA9r ( (FK1}d\@[Z2tr>|(Ԑ5f0sцt)^k tAM# S?[2&Jph dK@Ά^Q [!@PL3TG ; mw*2Ht Kő Xny˗3b@;+ʋy2137&~b 7h3o> @BB A?|߿Lg(:(盶{|޷?| 5 y_m_?1@kC<4Pw Q`#dV/ vPΓz\⹤IXTP7IANǏ ?.&& o<FFT $>'߀A?C=>(Eow[[+0ؘ+;޿pc#Wna}(9$\g'A\ ?͛ 1F 7ᅨGQ5@Ϡ@ @!>=%K#yCM_$x>~x4 4-@,D?sxyH˭W~ar ǧ l, ~KxZno_ H@~}ȐoM<r@CAEN~gзaP P6{ OL e o0I1dEj3| I /Nj2|q p6cNtBʇD0 eG/1k2dGk3=+lp,Xz& ܿv hx2,-ga=AA(˂gdge`x ,uu!c71`(CW >f |bbecOX^|_@>C=oO3\=r+/^2dr((GJ쵻14F)1Ug| fP%_@f2|\O_gx? ?eÏo~WwO1{_30ga`$x@D Bh~o >B "P B w>|¿`   /0H~AJ@vn?0;B@O|E ȹ'{`pH(:0?+`!? `/|ңw ^-XJ@ 7$1_O?1| /# Ç@%Ba0Ó/ ` SS023 ?`_C踗20rr10:?7zu +O | 19@_ #$ 030~3<}򙁓 )2P (/!O ,#C' u_4?8a0ky1 ?@dBA * yyC2x(@pϩODÈ F`T1[;L@&&`EQ@*ξÏ@׽Y @Ag3$a/!l!n#,e!?|cq V!]gE3p!)Sgf8x @ν r R;L"D '0?~1|HT5}c`vШ/b{`!\~Z72C Zf/0c`7$A\C񿟯o~)@4X <g|wb m7% T"s2{;=Jw_@O7 k;X,ƚ&" S5f*!ib  ntǀI`O_-g[HցZ!i^z(r9S<]~u nĞpcG ơ@I$IPo>z‹/ཌ~zwuMqh)(צ(bZIhrՈ?zG `ŌΞ\ s&na3Xrt)rh8ǪMw6Q)LW"B ВF8њxUT+D[ `*nR۱ r1[gD! S(iVw$H} g)ZIhAB=bZx(}庁ҁgN oH%c\Γ_XV)-~#K9\5⪁ mggkRD"N`ogflp3-t,b*<ʾetgt2XHn,R}Ӏbݠ-0Z؞=ͼ~.??x!X:U9/igU{Sy~E)gmMI(>K۴ ŭK;*b,D5n<֞XD~3RvēqusV OҖz{ïeZju5w1[G1U %z yhC -d7ܢ}m1]ȑi&fc' Ocy&TBK=xZ+I."0xot%=knFc`:-t$iѹ:gwFۺ`E!ZwCQ%_'sd QiUM4Ihgk(cKbk- /M1N 2sZ|@7 !IVPG"r _)GRJցTZyp2S .E*_7${߲ )YQ sX.dxt1Z),J))S0SH$Z uꆩRo( S\ԮJ`)b})Gd!`xJ)A\ϿHU)"Z bӧa\b1Ş[7EɏZd <$^PG ZR?KŐ"Dox`FIŽ15J%HP鵟8.4)ZLru lJQvphxEIwZ oN -I)w쾛E2 R1I& |Q~$9 /z8O 2J%D!ڛw/wI+nrcs>z2D(%N5bM&71T"{rǤn"庥'㑊S{&p#80(xi渤;Q frAj'wdѲKJaWRDBn?xp\W x55FQ.O!|NJysf)]?u#Ċ.2t=[ag)OG'C& gAl>k&jϿAY5y^̊eIx3f$t\I2^ȍ)O9u7< L7y[l-hfa(4R''D~_ٸo?ٴ6RKIIIYmm&`0gee:55۷z }}}}{T+#VXAȈPJUU5ϟ?wX͍AVV;''b VaR?q  l l E߾!!+**ϟ?g XԔ!;;`qC$A X|,<㤠{ZkL  _2<;{e ̟?318q۷`(`(..fpttƍ.\@( XN2000ZYO#+IY"5taåKHI>{l1P #B0?Z~ȼ(#""2r; ~/~@R@ @Q_>f GF&R} ߷oҥK psE/P)Y @,`70LVl@ /pe%K5( đB=b0 &v ugEaP^@;vڵkK70 J'@ nF_~y`¼ P @W@>% .Y dq&,..S~=(Q0ѳ">@,I5XRϝk+9-g?~(9P#( 1)!@ ;Lb`ء?B/$Z~@@999 X /&(.UBh@I5 R?bxMꓯB&&FT v,tMG?Eb0 ,f J; q̇T |tOǯ_! bdyyyYF`I Pna`i?^' pA ^q N` m AmM W,,,U0 tX " \# J L*c%F:Wi',y4  PPE@ T 7o`A*`(lZwPaZ:vbpjDXꁅ`; 7 ^< ZLѩ56@^la߁o@Ev`p">X c0;@=P Pt 456"Æ0Hqʕ+>~P~7T#P 9غrؠPe?O@Iv/v10lz}5 \-w)Hzσ!YZf|;2 gCpoQ{؏d3P$.@Q mB-0@;\5堖,} 6,M& -@PW$34o@5_E:_Z80F~16IENDB`TreeLine/icons/toolbar/32x32/editdelete.png0000644000175000017500000000233611651514477017436 0ustar dougdougPNG  IHDR szzbKGDRo pHYs  tIME  % kIDATxkW?Kj? :QE.DjZMY4kt*V/H9^E*., 7E4M(c99<zժVW_<)LMс,,`M-/5b Ơ]ya~A>ǭZ`ftGQ>hjO2 QD)1Eָ.=.9k(fw2;&y(0,-(Z@kf=G%knkAX$Q~|w{{e8fD*ψeU֭JZ.Lڇ95Eʶ b_T @pqHg۶r咈FHƶe^)y%Y(ZWJ2-@Ѩ:dض 8WJ2JxZ/c2 핺\; Fkq1eRbI(-w@@,pVXBkǎ`_n+Շ"VBIgQGk(ƲĀ ⟦ܯh`5~+mJ ̂ɤ4=xGӘ@8X 8~ :$o$J$Armmة۷,ʂ@vk-64ٲ`HQ^h-7e@]YDw6TfZ2- W2>|X+ժ7U܌4O mkIENDB`TreeLine/icons/toolbar/32x32/filequit.png0000644000175000017500000000311211651514477017141 0ustar dougdougPNG  IHDR szzgAMA abKGD pHYs )ItIME CJIDATxŗmϙ{U^UR]hZ &,@# ZOY PZRI h"RCT$mn\5uwwvWo{̜;slB syM5o2pp}jJ5vէ3{Jf5֠528hkA;hu68s~޺c;MlM14&dZtx!˜@Y7x-ۙa #o<PH褘ː?GyɅ{O<ǹ矤ϹP 4^Dm?:~9c>}w/O.D"k ^OS ]q@SF3L ڬBH<$\1?[m.\/GDW]ی^&neW{y5 }OѮvP_dڗٓ"e&ۇ/|l -(PTO])b6SpX(Q1$fn;5ZtE^-B^%".\YSWŗOfRݬy i߅+=.P;R1t>PI'(2#_-@7gf‡v,m9"a+[T\u\!p湊?ڽ/Tj\ElJO7| P.eU/-@]:Lk__5~1 ^ɛW |կjy{G MB mA,p+ϓ<.M?o0?DhlYBjFv) wFF~Tg{,WR~I"m#\Y1)#,{s/D+N] b2t9|f*S4,aM4,]啢@>>< 0nSj8vJwӊ6:h_709| u⭊z"?OϧG;6Ja"ETXq,>r;sEq1۵IJEͱۆVG B)0nswl|IT;Q@}Ӛ]c;5}v{]֔ ˣf'=^Ny<\^),IENDB`TreeLine/icons/toolbar/32x32/textaddboldtag.png0000644000175000017500000000312411651514477020314 0ustar dougdougPNG  IHDR VόsBIT|dtEXtSoftwarewww.inkscape.org<IDATHW[lU3gv;\K!)i%!DLー"Hx7oĠ!>4H# PK$)[V@7T趻ݝ}g[DdΜ?gVWu{7pPêwq6 ʣrH&E o~eҧ^l G&%Kb#.y 0Z¤{Ts49~ӦM<ה"pfR0&kp*ß7fNR@_l8?Ŝ 11p ssLNL\.RKFYS)z tbT}ᤦwD2Ps7&~IG!,Fddq߂TpEߥ{bf\:[bŃ܌!m46Vr;[^7S-\=iļWڷyyݵsݩP+k!=3h\U"IfqIENDB`TreeLine/icons/toolbar/32x32/toolsshortcuts.png0000644000175000017500000000405311651514477020443 0ustar dougdougPNG  IHDR szzgAMA abKGD pHYs  ~tIME:'AR IDATx{9sιs>w.*U$-hVCդ VZm166FƚXMTibIt s,.}ܹs? Kϓߗw#IլMPSٹG~Xgs̓.d64˖,e|?ٿm#wvp{w۫|Kg3E45E:݀i,ƾ-ޱ><z%ɬk3s7H_;9Ŷ r(c;:{vl)#I0{\9OusaR:)-VhBK !$v}MRL!_$_,:ͭ-&,^_ҿi$d$NfL[kp(e\BQuSpP(JLbBiVeZRQib5ù<Ǫ\qëVoѻbY35!()"u\FFy\mW9|$mnGJN36;~"?y/_(~yuP3FqFvš\(P\rJ>W` :OÊާFSWH:I>~Ss<ϣ\,6gScn0B)-2xa%=_OAȵ.`Z/wqMp}4Xhsy"cL`ђ465ZT 3n8vnirLG_GAHu!- Ƭ3H5Lӡ\*ܔ2 |ïՈC(-Cmvmʯ.=AQܪKD. +?ZI d3ŦOJgY)S  l{.ݝ^_?S,κj!:TR0KZíԽ*jR$V{[Y)W[4Ν}>ߞ:uxua$AD!i5S\+ue j5Q$@ Lag@/tLFJfBBOPMx%|_[Rܯ@k #TQ-NkKoolp|~ =Qq\0ݗR d ݓSS*ae6v;l,L^' C)JLpi2_+~3zc(É xsξ"io+4{lY؎VڝxZ-6 A(č81=ƥg>;%[ rfR?t]gܼ};nI;erӣ xw872FN348((,Mg{M`;%^??*Ea;&k?lXXoﱼ *g08OP߃!cJ<c U:!sQOT\7SWeO\v.^_ l*+vwLn<Sݢ_ˋk%F̈LZcPDv&1#>yF)f+"t>u24>H:p{k]%:Ϙ)OwIؤ'39fMx+ ۵uDߩ=>7m]o/N!>pIENDB`TreeLine/icons/toolbar/32x32/toolsfileoptions.png0000644000175000017500000000341611651514477020742 0ustar dougdougPNG  IHDR szz pHYs  gAMA|Q cHRMz%u0`:o_FIDATxbd@;ww`ddd7}^Dww T@]p#;/+++õk?}5@X%M[jzO<yŊ&L/L 6O>q;z{)+&33KL# DAA‰gϞ'PGX3|_XXܸ 66V3/ 0?~)@LLh(  0 ޿ MM_-XXjSɩ\-$.;{S/^~T -aa~D_?ڼF)%!@1A!ԩ7l8k`:9 yy\gA”AP-PjRB +#‰Bq22B ի VVz`|¿.L``qA=Ӄw !~qjJLFmj) sIIpw5iiQצ30 !G! o^G9;G2<{{ ~G@8䘡%а06vښhBC$_?M ܎9 I=5U20ÂP2ph0\>5=Bq9 Hq  |\ \l_ zĘCVLHj]9?!NSY<&j2jH#9,!Ř_jNPfdeطk#r@1iB_f&%&'+jT|'#da`d'/SȴD fexҬ[>]w?[)@Gp3_@ˁ 20|p o{3`dw$}پ7 {?|? |=gxA^ 2<ް@1QZf/:5w2= U煄0!203H0P+&e n.8bNV7wA,c&N ,3oqUn:ӿ5kN 0~ / RG?xoǛJ[P9@4q(hKp{ qֱ;c専 hZA/rI@8{t\ XIENDB`TreeLine/icons/toolbar/32x32/datasetdescendtype.png0000644000175000017500000000244311651514477021202 0ustar dougdougPNG  IHDR szzbKGD pHYs  tIME _IDATxڵMhelgl+MH&ٍDL I-%z9'[(Eу Ts4)6ذf7Mwg'Mvf;{cRʡ<|ft>00J*œ5˽[@Z u "T_!@Ӵc[| iE` ;:ڕ?'<|gUU R%&7_K18 G:ivy[-==sic@n](;=<ܢ(XMF Iߝ%m'l]~jnONN^@h.gs,^dk4R,`}!D}``x2YƲ,"x 8G'KRfb؃'eh8J1XY\;T&fgg]sE%tx I[@E/--]Bl D:)KG \'$UwADL+a:yK VP\I:_{b6 T ``͒JYkE? TL Wո):???u6\+ W(~bCsc3`SCId <(q;WӥqlsF~{h 5O+i/fYbb[%a!|s>rˬ+uŅb+nB ;d {̜7Sf\ZI MP(ȍPeƙ3gj'a6WQlYUQv ~{$,K`0n[@ů9M9@]mcq~b0(gm%S*1zګlc;[Z:yh^yeS1;::pÁwNJy`-#,5݅ba"0h7e33#@^ 2 4{Ufg,,0M$((XE4Ŷm[<׮-L$R))De׷[ {S 1u+e{zz>~\Q?[k/0P YD(qvh𘊊 *=N z}Kb Db% T+IENDB`TreeLine/install.py0000755000175000017500000002776611651514477013323 0ustar dougdoug#!/usr/bin/env python """ **************************************************************************** install.py, Linux install script for TreeLine Copyright (C) 2005, Douglas W. Bell This is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, either Version 2 or any later version. This program is distributed in the hope that it will be useful, but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. ***************************************************************************** """ import sys import os.path import getopt import shutil import compileall import py_compile import glob import re import subprocess prefixDir = '/usr/local' buildRoot = '/' progName = 'treeline' docDir = 'share/doc/%s' % progName templateDir = 'share/%s/templates' % progName iconDir = 'share/icons/%s' % progName testXML = 1 testSpell = 1 def usage(exitCode=2): """Display usage info and exit""" global prefixDir global buildRoot print 'Usage:' print ' python install.py [-h] [-p dir] [-d dir] [-t dir] [-i dir] '\ '[-b dir] [-x]' print 'where:' print ' -h display this help message' print ' -p dir install prefix [default: %s]' % prefixDir print ' -d dir documentaion dir [default: /%s]' % docDir print ' -t dir template dir [default: /%s]' % templateDir print ' -i dir icon dir [default: /%s]' % iconDir print ' -b dir temporary build root for packagers [default: %s]' \ % buildRoot print ' -x skip all dependency checks (risky)' sys.exit(exitCode) def cmpVersions(versionStr, reqdTuple): """Return 1 if point-sep values in versionStr are >= reqdTuple""" match = re.search(r'[0-9\.]+', versionStr) if not match: return 0 versionStr = match.group() versionList = [int(val) for val in versionStr.split('.') if val] reqdList = list(reqdTuple) while len(versionList) < len(reqdList): versionList.append(0) while len(reqdList) < len(versionList): reqdList.append(0) if cmp(versionList, reqdList) != -1: return 1 return 0 def copyDir(srcDir, dstDir): """Copy all regular files from srcDir to dstDir, dstDir is created if necessary""" try: if not os.path.isdir(dstDir): os.makedirs(dstDir) names = os.listdir(srcDir) for name in names: srcPath = os.path.join(srcDir, name) if os.path.isfile(srcPath): shutil.copy2(srcPath, os.path.join(dstDir, name)) except (IOError, OSError), e: if str(e).find('Permission denied') >= 0: print 'Error - must be root to install files' cleanSource() sys.exit(4) raise def createWrapper(execDir, execName): """Create a wrapper executable file for a python script in execDir named execName""" text = '#!/bin/sh\n\nexec %s %s/%s.py "$@"' % (sys.executable, execDir, execName) f = open(execName, 'w') f.write(text) f.close() os.chmod(execName, 0755) def replaceLine(path, origLineStart, newLine): """Searches for origLineStart in file named path, replaces all ocurrances with newLine and re-writes file""" f = open(path, 'r') lines = f.readlines() f.close() f = open(path, 'w') for line in lines: if line.startswith(origLineStart): f.write(newLine) else: f.write(line) f.close() def spellCheck(cmdList): """Try spell checkers from list, print result""" for cmd in cmdList: try: p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p.stdout.readline() p.stdin.write('!\n') p.stdin.flush() p.stdin.close() p.stdout.close() print ' Spell Checker %s -> OK' % cmd.split()[0] return except: pass print ' Spell Checker not found -> install aspell or ispell' print ' if spell checking is desired' def cleanSource(): """Remove any temporary files added to untarred dirs""" for name in glob.glob(os.path.join('source', '*.py[co]')): os.remove(name) global progName if os.path.isfile(progName): os.remove(progName) def removeDir(dir): """Remove dir and all files at path, ignore errors""" try: shutil.rmtree(dir, 1) except: # shouldn't be needed with ignore error param, but pass # some python versions have a bug def main(): optLetters = 'hp:d:t:i:b:x' try: opts, args = getopt.getopt(sys.argv[1:], optLetters) except getopt.GetoptError: usage(2) global prefixDir global docDir global templateDir global iconDir global buildRoot global progName depCheck = 1 for opt, val in opts: if opt == '-h': usage(0) elif opt == '-p': prefixDir = os.path.abspath(val) elif opt == '-d': docDir = val elif opt == '-t': templateDir = val elif opt == '-i': iconDir = val elif opt == '-b': buildRoot = val elif opt == '-x': depCheck = 0 if not os.path.isfile('install.py'): print 'Error - %s files not found' % progName print 'The directory containing "install.py" must be current' sys.exit(4) if os.path.isdir('source') and \ not os.path.isfile('source/%s.py' % progName): print 'Error - source files not found' print 'Retry the extraction from the tar archive' sys.exit(4) if depCheck: print 'Checking dependencies...' try: pyVersion = sys.version_info[:3] except AttributeError: print ' Python Version 1.x -> Sorry, 2.3 or higher is required' sys.exit(3) pyVersion = '.'.join([str(num) for num in pyVersion]) if cmpVersions(pyVersion, (2, 3)): print ' Python Version %s -> OK' % pyVersion else: print ' Python Version %s -> Sorry, 2.3 or higher is required' \ % pyVersion sys.exit(3) try: from PyQt4 import QtCore, QtGui except: print ' Sorry, Qt Version 4.4 or higher and '\ 'PyQt Version 4.4.0 or higher are required' sys.exit(3) qtVersion = QtCore.qVersion() if cmpVersions(qtVersion, (4, 4)): print ' Qt Version %s -> OK' % qtVersion else: print ' Qt Version %s -> Sorry, 4.4 or higher is required' \ % qtVersion sys.exit(3) pyqtVersion = QtCore.PYQT_VERSION_STR if cmpVersions(pyqtVersion, (4, 4)): print ' PyQt Version %s -> OK' % pyqtVersion else: print ' PyQt Version %s -> Sorry, 4.4.0 or higher is required' \ % pyqtVersion sys.exit(3) global testXML if testXML: try: import xml.sax handler = xml.sax.ContentHandler() xml.sax.parseString('test', handler) except: print ' XML Parser -> Sorry, the expat library or '\ 'PyXML package is required' sys.exit(3) print ' XML Parser -> OK' global testSpell if testSpell: spellCheck(['aspell -a', 'ispell -a']) pythonPrefixDir = os.path.join(prefixDir, 'lib', progName) pythonBuildDir = os.path.join(buildRoot, pythonPrefixDir[1:]) if os.path.isdir('source'): compileall.compile_dir('source', ddir=os.path.join(prefixDir, 'source')) print 'Installing files...' print ' Copying python files to %s' % pythonBuildDir removeDir(pythonBuildDir) # remove old? copyDir('source', pythonBuildDir) if os.path.isdir('source/plugins'): pluginBuildDir = os.path.join(pythonBuildDir, 'plugins') print ' Creating plugins directory if necessary' copyDir('source/plugins', pluginBuildDir) if os.path.isdir('translations'): translationDir = os.path.join(pythonBuildDir, 'translations') print ' Copying translation files to %s' % translationDir copyDir('translations', translationDir) if os.path.isdir('doc'): docPrefixDir = docDir.replace('/', '') if not os.path.isabs(docPrefixDir): docPrefixDir = os.path.join(prefixDir, docPrefixDir) docBuildDir = os.path.join(buildRoot, docPrefixDir[1:]) print ' Copying documentation files to %s' % docBuildDir copyDir('doc', docBuildDir) # update help file location in main python script replaceLine(os.path.join(pythonBuildDir, '%s.py' % progName), 'helpFilePath = None', 'helpFilePath = \'%s\' # modified by install script\n' % docPrefixDir) if os.path.isdir('templates'): templatePrefixDir = templateDir.replace('/', '') if not os.path.isabs(templatePrefixDir): templatePrefixDir = os.path.join(prefixDir, templatePrefixDir) templateBuildDir = os.path.join(buildRoot, templatePrefixDir[1:]) print ' Copying template files to %s' % templateBuildDir copyDir('templates', templateBuildDir) # update help file location in main python script replaceLine(os.path.join(pythonBuildDir, '%s.py' % progName), 'templatePath = None', 'templatePath = \'%s\' # modified by install script\n' % templatePrefixDir) if os.path.isdir('data'): dataPrefixDir = os.path.join(prefixDir, 'share', progName) dataBuildDir = os.path.join(buildRoot, dataPrefixDir[1:]) print ' Copying data files to %s' % dataBuildDir removeDir(dataBuildDir) # remove old? copyDir('data', dataBuildDir) # update data file location in main python script replaceLine(os.path.join(pythonBuildDir, '%s.py' % progName), 'dataFilePath = None', 'dataFilePath = \'%s\' # modified by install script\n' % dataPrefixDir) if os.path.isdir('icons'): iconPrefixDir = iconDir.replace('/', '') if not os.path.isabs(iconPrefixDir): iconPrefixDir = os.path.join(prefixDir, iconPrefixDir) iconBuildDir = os.path.join(buildRoot, iconPrefixDir[1:]) print ' Copying icon files to %s' % iconBuildDir copyDir('icons', iconBuildDir) # update icon location in main python script replaceLine(os.path.join(pythonBuildDir, '%s.py' % progName), 'iconPath = None', 'iconPath = \'%s\' # modified by install script\n' % iconPrefixDir) if os.path.isdir('icons/toolbar'): iconToolBuildDir = os.path.join(iconBuildDir, 'toolbar') copyDir('icons/toolbar', iconToolBuildDir) if os.path.isdir('icons/toolbar/16x16'): copyDir('icons/toolbar/16x16', os.path.join(iconToolBuildDir, '16x16')) if os.path.isdir('icons/toolbar/32x32'): copyDir('icons/toolbar/32x32', os.path.join(iconToolBuildDir, '32x32')) if os.path.isdir('icons/tree'): copyDir('icons/tree', os.path.join(iconBuildDir, 'tree')) if os.path.isdir('source'): createWrapper(pythonPrefixDir, progName) binBuildDir = os.path.join(buildRoot, prefixDir[1:], 'bin') print ' Copying executable file "%s" to %s' % (progName, binBuildDir) if not os.path.isdir(binBuildDir): os.makedirs(binBuildDir) shutil.copy2(progName, binBuildDir) cleanSource() print 'Install complete.' if __name__ == '__main__': main() TreeLine/source/0000755000175000017500000000000011656633673012563 5ustar dougdougTreeLine/source/spellcheck.py0000644000175000017500000001130511651514477015245 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # spellcheck.py, provides a class for a interface to aspell or ispell # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re import sys import subprocess class SpellCheck(object): """Interfaces with aspell or ispell and stores session hooks""" def __init__(self, spellPath='', langCode=''): """Create initial hooks to outside program""" aspellOpts = ['-a -H --encoding=utf-8'] ispellOpts = ['-a -h -Tutf8', '-a'] if langCode: aspellOpts.insert(0, '%s --lang=%s' % (aspellOpts[0], langCode)) if not spellPath: cmdList = ['aspell %s' % opt for opt in aspellOpts] cmdList.extend(['ispell %s' % opt for opt in ispellOpts]) elif spellPath.find('aspell') >= 0: cmdList = ['%s %s' % (spellPath, opt) for opt in aspellOpts] else: cmdList = ['%s %s' % (spellPath, opt) for opt in ispellOpts] for cmd in cmdList: try: p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) self.stdIn = p.stdin self.stdOut = p.stdout self.stdOut.readline() # read header self.stdIn.write('!\n') # set terse mode (no correct returns) self.stdIn.flush() return except IOError: pass raise SpellCheckError('Could not init aspell or ispell') def checkLine(self, line, skipList=None): """Check one (and only one!) line of text, ignore words in skipList, return a list of tuples, each with the mispelled word, position in the line, and a list of suggestions""" if not skipList: skipList = [] self.stdIn.write('^%s\n' % line.encode('utf-8')) self.stdIn.flush() results = [self.stdOut.readline()] while results[-1].strip(): results.append(self.stdOut.readline()) return filter(None, [formatOutput(result, line, skipList) for result in results]) def close(self): """Shut down hooks to outside program""" self.stdIn.close() self.stdOut.close() def acceptWord(self, word): """Accept given word for the remainder of this session""" self.stdIn.write('@%s\n' % word.encode('utf-8')) self.stdIn.flush() def addToDict(self, word, lowCase=0): """Add word to dictionary, all lowercase if lowCase""" if lowCase: self.stdIn.write('&%s\n' % word.encode('utf-8')) else: self.stdIn.write('*%s\n' % word.encode('utf-8')) self.stdIn.write('#\n') # saves dict self.stdIn.flush() class SpellCheckError(Exception): """Exception class for errors on reading file content""" pass guessRe = re.compile('[&?] (\S+) \d+ (\d+): (.+)') noGuessRe = re.compile('# (\S+) (\d+)') def formatOutput(result, line, skipList=[]): """Return tuple of word, position and guess list""" result = unicode(result, 'utf-8').strip() match = guessRe.match(result) if match: guesses = match.group(3).split(', ') else: match = noGuessRe.match(result) if not match: return None guesses = [] word = match.group(1) if word in skipList: return None wordPos = int(match.group(2)) - 1 # work around unicode bug in older versions of aspell while line[wordPos:wordPos + len(word)] != word and wordPos > 0: wordPos -= 1 return (word, wordPos, guesses) if __name__ == '__main__': try: sp = SpellCheck() except SpellCheckError: print 'Error - could not initialize aspell or ispell' sys.exit() while 1: s = raw_input('Enter line-> ').strip() if not s: sys.exit() if s.startswith('Accept->'): sp.acceptWord(s[8:]) elif s.startswith('Add->'): sp.addToDict(s[5:]) elif s.startswith('AddLow->'): sp.addToDict(s[8:], 1) else: for word, pos, suggests in sp.checkLine(s): print '%s @%i: %s' % (word, pos, ', '.join(suggests)) print sp.close() TreeLine/source/recentfiles.py0000644000175000017500000002267611651514477015450 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # recentfiles.py, classes to handle recent file lists and states # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import os.path import sys import time from PyQt4 import QtCore, QtGui import globalref class RecentFileList(list): """A list of RecentFile objects""" def __init__(self): list.__init__(self) self.numEntries = globalref.options.intData('RecentFiles', 0, 99) self.loadList() def loadList(self): """Load recent files from options""" self[:] = [] for num in range(self.numEntries): entry = RecentFile() entry.loadPath(num) if entry.pathIsValid(): self.append(entry) def writeList(self): """Write list of paths to options""" for num in range(self.numEntries): try: entry = self[num] except IndexError: entry = RecentFile() entry.writePath(num) globalref.options.writeChanges() def addEntry(self, path): """If path is in list, move to start; otherwise create a new entry at the start""" try: entry = self.pop(self.index(RecentFile(path))) except ValueError: entry = RecentFile(path) self.insert(0, entry) self.updateMenu() def removeEntry(self, path): """Remove the given pathname if found""" try: self.remove(RecentFile(path)) except ValueError: pass self.updateMenu() def firstPath(self): """Return the first valid path""" for entry in self: path = os.path.dirname(entry.path) if os.path.exists(path): return path return '' def changeNumEntries(self, numEntries): """Modify number of available entries""" for i in range(numEntries, self.numEntries): globalref.options.changeData(RecentFile().optionTitle(i), '', True) TreeState().writeState(i) for i in range(self.numEntries, numEntries): globalref.options.addDefaultKey(RecentFile().optionTitle(i)) globalref.options.addDefaultKey(TreeState().optionTitle(i)) self.numEntries = numEntries self.updateMenu() def updateMenu(self): """Refresh menu items""" menu = globalref.mainWin.recentFileSep.parentWidget() actionList = globalref.mainWin.recentFileActions menuLength = min(self.numEntries, len(self)) while len(actionList) < menuLength: actionList.append(RecentAction(globalref.mainWin)) menu.insertAction(globalref.mainWin.recentFileSep, actionList[-1]) while len(actionList) > menuLength: menu.removeAction(actionList[-1]) del actionList[-1] for action, entry, num in zip(actionList, self, range(menuLength)): action.setPath(entry, num) def saveTreeState(self, treeView): """Set the tree state from the current file""" try: entry = self[self.index(RecentFile(globalref.docRef.fileName))] except ValueError: return entry.treeState.saveState(treeView) def restoreTreeState(self, treeView): """Restore the state in the current file, return True if changed""" try: entry = self[self.index(RecentFile(globalref.docRef.fileName))] except ValueError: return False return entry.treeState.restoreState(treeView) def clearTreeStates(self): """Remove all saved tree state data""" for entry in self: entry.treeState = TreeState() class RecentFile(object): """Contains path info and creates menu actions""" maxEntryLength = 30 def __init__(self, path=''): self.path = path if self.path: self.path = os.path.abspath(self.path) self.treeState = TreeState() def loadPath(self, num): """Load path from option position num""" self.path = globalref.options.strData(self.optionTitle(num), True) if self.path: self.path = os.path.abspath(self.path) self.treeState.loadState(num) def pathIsValid(self): """Return True if has a valid path""" return os.access(self.path.encode(sys.getfilesystemencoding()), os.R_OK) def writePath(self, num): """Save path to option at position num""" globalref.options.changeData(self.optionTitle(num), self.path, True) self.treeState.writeState(num) def abbrevPath(self): """Return shortened version of path""" abbrevPath = self.path if len(self.path) > RecentFile.maxEntryLength: truncLength = RecentFile.maxEntryLength - 3 pos = self.path.find(os.sep, len(self.path) - truncLength) if pos < 0: pos = len(self.path) - truncLength abbrevPath = '...' + self.path[pos:] return abbrevPath def optionTitle(self, num): """Return option key""" return 'RecentFile%d' % (num + 1) def __cmp__(self, other): """Compare paths""" return cmp(os.path.normcase(self.path), os.path.normcase(other.path)) class RecentAction(QtGui.QAction): """Menu item for a recent file entry""" def __init__(self, parent): QtGui.QAction.__init__(self, parent) self.connect(self, QtCore.SIGNAL('triggered()'), self.openItem) def setPath(self, recentFile, num): """Set menu title to abbreviated path name and add number""" self.setText('&%d %s' % (num + 1, recentFile.abbrevPath())) self.setStatusTip(recentFile.path) def openItem(self): """Execute function to open this file""" globalref.treeControl.recentOpen(unicode(self.statusTip())) class TreeState(object): """Loads and saves the tree states of recent files""" def __init__(self): self.timeStamp = 0 self.topNode = 0 self.selectNodes = [0] self.openNodes = [0] def loadState(self, num): """Load state from option position num""" stateStr = globalref.options.strData(self.optionTitle(num), True) states = stateStr.split(':') try: self.timeStamp = int(states[0]) self.topNode = int(states[1]) self.selectNodes = [int(s) for s in states[2].split(',')] self.openNodes = [int(s) for s in states[3].split(',')] except (ValueError, IndexError): pass def writeState(self, num): """Save state to option at position num""" stateStr = '%d:%d:%s:%s' % (self.timeStamp, self.topNode, ','.join([str(n) for n in self.selectNodes]), ','.join([str(n) for n in self.openNodes])) globalref.options.changeData(self.optionTitle(num), stateStr, True) def saveState(self, treeView): """Set the state from the current file""" self.timeStamp = int(time.time()) allNodes = globalref.docRef.root.descendantList(True) self.topNode = allNodes.index(treeView.itemAt(0, 0).docItemRef) self.selectNodes = [allNodes.index(item) for item in globalref.docRef.selection] if not self.selectNodes: self.selectNodes = [0] if not allNodes[0].open: # root closed, return default self.openNodes = [0] else: self.openNodes = [] for i, node in enumerate(allNodes): if node.open and \ not [child for child in node.childList if child.open] \ and node.allAncestorsOpen(): self.openNodes.append(i) def restoreState(self, treeView): """Restore this state in the current file, return True if changed""" fileTimeStamp = os.stat(globalref.docRef.fileName).st_mtime if fileTimeStamp > self.timeStamp: # file modified externally return False allNodes = globalref.docRef.root.descendantList(True) try: selectedNodes = [allNodes[i] for i in self.selectNodes] except IndexError: selectedNodes = [allNodes[0]] globalref.docRef.selection.replace(selectedNodes) globalref.docRef.selection.prevSelects = [] for i in self.openNodes: try: node = allNodes[i] while not node.open and node != allNodes[0]: node.open = True node = node.parent except IndexError: pass globalref.updateViewAll() try: topNode = allNodes[self.topNode].viewData treeView.scrollToItem(topNode, QtGui.QAbstractItemView.PositionAtTop) except IndexError: pass return True def optionTitle(self, num): """Return option key""" return 'TreeState%d' % (num + 1) TreeLine/source/genboolean.py0000644000175000017500000000617111651514477015246 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # genboolean.py, provides a class for boolean formating # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import types class GenBoolean(object): """Stores & formats boolean values""" formatDict = {N_('true'):1, N_('false'):0, N_('yes'):1, N_('no'):0} translateDict = {} for key in formatDict.keys(): value = formatDict[key] formatDict[key[0]] = value translateDict[_(key)[0]] = value translateDict[_(key)] = value formatDict.update(translateDict) def __init__(self, value=0): """Accepts any format from formatDict (not case sensitive) to initialize""" self.setBool(value) def setBool(self, value): """Sets the value from any format in formatDict (not case sensitive)""" if type(value) in (types.StringType, types.UnicodeType): value = value.lower() try: self.value = GenBoolean.formatDict[value] except KeyError: raise GenBooleanError('Invalid entry') def setFromFormat(self, boolStr, format): """Set value based on given format only Returns self""" try: self.value = not list(_splitFormat(format)).index(boolStr.lower()) except ValueError: raise GenBooleanError('Invalid entry, no format match') return self def boolStr(self, format='true/false'): """Return string based on the format, which includes a true string and a false string separated by a '/' Raise exception of format invalid""" return _splitFormat(format)[not self.value] def clone(self): """Return cloned instance""" return self.__class__(self.value) def __repr__(self): """Outputs in format true/false""" return self.boolStr() def __cmp__(self, other): """Compare operator""" if other and isinstance(other, GenBoolean): return cmp(self.value, other.value) if type(other) is types.IntType: return cmp(self.value, other) return 1 def __hash__(self): """Allow use as dictionary key""" return hash(self.value) def __nonzero__(self): """Allow truth testing""" return self.value class GenBooleanError(Exception): """Exception class for invalid boolean data""" pass ############# Utility Function ############ def _splitFormat(format): """Return tuple of format converted to lower case or raise exception""" result = format.lower().split('/', 1) if len(result) != 2 or not result[0] or not result[1] or \ result[0] == result[1]: raise GenBooleanError('Invalid boolean format') return result TreeLine/source/printdialogs.py0000644000175000017500000012500611651514477015633 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # printdialogs.py, provides a print preview and print settings dialogs # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re import copy from PyQt4 import QtCore, QtGui import configdialog import nodeformat import optiondefaults import globalref stdWinFlags = QtCore.Qt.Dialog | QtCore.Qt.WindowTitleHint | \ QtCore.Qt.WindowSystemMenuHint class PrintPrevDlg(QtGui.QDialog): """Provides a generic print preview with page controls""" def __init__(self, printData, numPages, paperRect, pageCmd, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Print Preview')) self.printData = printData self.curPage = 1 self.minPage = 1 self.maxPage = numPages topLayout = QtGui.QVBoxLayout(self) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) self.prevButton = QtGui.QPushButton(_('P&rev. Page')) ctrlLayout.addWidget(self.prevButton) self.connect(self.prevButton, QtCore.SIGNAL('clicked()'), self.prevPage) self.nextButton = QtGui.QPushButton(_('&Next Page')) ctrlLayout.addWidget(self.nextButton) self.connect(self.nextButton, QtCore.SIGNAL('clicked()'), self.nextPage) self.statusLabel = QtGui.QLabel('') ctrlLayout.addWidget(self.statusLabel, 1) self.statusLabel.setAlignment(QtCore.Qt.AlignCenter) self.statusLabel.setFrameStyle(QtGui.QFrame.Panel | QtGui.QFrame.Sunken) self.statusLabel.setMargin(2) previewButton = QtGui.QPushButton(_('Print Option&s...')) ctrlLayout.addWidget(previewButton) self.connect(previewButton, QtCore.SIGNAL('clicked()'), self.showOptions) printButton = QtGui.QPushButton(_('&Print...')) ctrlLayout.addWidget(printButton) self.connect(printButton, QtCore.SIGNAL('clicked()'), self.accept) cancelButton = QtGui.QPushButton(_('&Close')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self.reject) self.preview = PrintPrev(paperRect, pageCmd) topLayout.addWidget(self.preview, 1) self.updatePageNum() def updatePageNum(self): """Enable/disable prev & next buttons, update status label and preview""" self.prevButton.setEnabled(self.curPage > self.minPage) self.nextButton.setEnabled(self.curPage < self.maxPage) self.statusLabel.setText(_('Page %(current)i of %(max)i') % {'current':self.curPage, 'max':self.maxPage}) self.preview.setPageNum(self.curPage) def prevPage(self): """Go to previous page""" if self.curPage > self.minPage: self.curPage -= 1 self.updatePageNum() def nextPage(self): """Go to next page""" if self.curPage < self.maxPage: self.curPage += 1 self.updatePageNum() def showOptions(self): """Show a modal options dialog""" dlg = PrintOptionsDialog(self.printData, False, self) self.setUpdatesEnabled(False) # halt repaint until settings consistent if dlg.exec_() == QtGui.QDialog.Accepted: QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) self.printData.setPrintContent() self.maxPage = int(globalref.docRef.fileInfoItem.data[nodeformat. FileInfoFormat. numPagesFieldName]) if self.curPage > self.maxPage: self.curPage = self.maxPage self.preview.paperRect = self.printData.printer.paperRect() self.updatePageNum() QtGui.QApplication.restoreOverrideCursor() self.setUpdatesEnabled(True) class PrintPrev(QtGui.QWidget): """Provides a widget for the paper""" def __init__(self, paperRect, pageCmd, parent=None): QtGui.QWidget.__init__(self, parent) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.paperRect = paperRect self.pageCmd = pageCmd self.pageNum = 0 def sizeHint(self): """Return preferred size""" return QtCore.QSize(250, 450) def setPageNum(self, pageNum): """Set new page number and update""" self.pageNum = pageNum self.update() def paintEvent(self, event): """Paint the current page""" paint = QtGui.QPainter(self) viewRect = paint.viewport() paperViewSize = self.paperRect.size() # used for aspect ratio only paperViewSize.scale(viewRect.size(), QtCore.Qt.KeepAspectRatio) leftMargin = (viewRect.width() - paperViewSize.width()) // 2 topMargin = (viewRect.height() - paperViewSize.height()) // 2 paperViewRect = QtCore.QRect(leftMargin, topMargin, paperViewSize.width(), paperViewSize.height()) paint.setWindow(self.paperRect) paint.setViewport(paperViewRect) paint.fillRect(self.paperRect, QtGui.QBrush(QtCore.Qt.white)) self.pageCmd(self.pageNum, paint) class PrintOptionsDialog(QtGui.QDialog): """Base dialog for print configuration""" def __init__(self, printData, showExtraButtons=True, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Print Options')) self.printData = printData topLayout = QtGui.QVBoxLayout(self) self.setLayout(topLayout) tabs = QtGui.QTabWidget() topLayout.addWidget(tabs) generalPage = GeneralPage() tabs.addTab(generalPage, _('&General Options')) pageSetupPage = PageSetupPage(self.printData.printer) tabs.addTab(pageSetupPage, _('&Page Setup')) fontPage = FontPage(self.printData) tabs.addTab(fontPage, _('&Font Selection')) headerPage = HeaderPage() tabs.addTab(headerPage, _('&Header/Footer')) self.tabPages = [generalPage, pageSetupPage, fontPage, headerPage] ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) if showExtraButtons: previewButton = QtGui.QPushButton(_('Print Pre&view...')) ctrlLayout.addWidget(previewButton) self.connect(previewButton, QtCore.SIGNAL('clicked()'), self.preview) printButton = QtGui.QPushButton(_('P&rint...')) ctrlLayout.addWidget(printButton) self.connect(printButton, QtCore.SIGNAL('clicked()'), self.quickPrint) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) def quickPrint(self): """Accept this dialog and go to print dialog""" self.accept() self.printData.filePrint() def preview(self): """Accept this dialog and go to print preview dialog""" self.accept() self.printData.filePrintPreview() def accept(self): """Store results before closing dialog""" for page in self.tabPages: page.saveChanges() globalref.options.writeChanges() QtGui.QDialog.accept(self) class GeneralPage(QtGui.QWidget): """Misc print option dialog page""" printWhat = ['tree', 'branch', 'node'] def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) topLayout = QtGui.QGridLayout(self) self.setLayout(topLayout) whatGroupBox = QtGui.QGroupBox(_('What to print')) topLayout.addWidget(whatGroupBox, 0, 0) whatLayout = QtGui.QVBoxLayout(whatGroupBox) self.whatButtons = QtGui.QButtonGroup(self) treeButton = QtGui.QRadioButton(_('&Entire tree')) self.whatButtons.addButton(treeButton, GeneralPage.printWhat.index('tree')) whatLayout.addWidget(treeButton) branchButton = QtGui.QRadioButton(_('Selected &branches')) self.whatButtons.addButton(branchButton, GeneralPage.printWhat.index('branch')) whatLayout.addWidget(branchButton) nodeButton = QtGui.QRadioButton(_('Selected &nodes')) self.whatButtons.addButton(nodeButton, GeneralPage.printWhat.index('node')) whatLayout.addWidget(nodeButton) setting = globalref.options.strData('PrintWhat') try: self.whatButtons.button(GeneralPage.printWhat. index(setting)).setChecked(True) except ValueError: self.whatButtons.button(0).setChecked(True) self.connect(self.whatButtons, QtCore.SIGNAL('buttonClicked(int)'), self.updateCmdAvail) optionBox = QtGui.QGroupBox(_('Features')) topLayout.addWidget(optionBox, 0, 1) optionLayout = QtGui.QVBoxLayout(optionBox) self.linesButton = QtGui.QCheckBox(_('Draw &lines to children')) optionLayout.addWidget(self.linesButton) self.linesButton.setChecked(globalref.options.boolData('PrintLines')) self.rootButton = QtGui.QCheckBox(_('&Include root node')) optionLayout.addWidget(self.rootButton) self.rootButton.setChecked(globalref.options.boolData('PrintRoot')) self.openOnlyButton = QtGui.QCheckBox(_('Only open no&de children')) optionLayout.addWidget(self.openOnlyButton) self.openOnlyButton.setChecked(globalref.options. boolData('PrintOpenOnly')) self.widowButton = QtGui.QCheckBox(_('&Keep first child with parent')) optionLayout.addWidget(self.widowButton) self.widowButton.setChecked(globalref.options. boolData('PrintKeepFirstChild')) topLayout.setRowStretch(1, 1) def updateCmdAvail(self): """Update options available""" if GeneralPage.printWhat[self.whatButtons.checkedId()] == 'node': self.rootButton.setChecked(False) self.rootButton.setEnabled(False) self.openOnlyButton.setChecked(False) self.openOnlyButton.setEnabled(False) else: self.rootButton.setEnabled(True) self.openOnlyButton.setEnabled(True) def saveChanges(self): """Update option data with current dialog settings""" globalref.options.changeData('PrintWhat', GeneralPage.printWhat[self.whatButtons. checkedId()], True) globalref.options.changeData('PrintLines', self.linesButton.isChecked() and 'yes' or 'no', True) globalref.options.changeData('PrintRoot', self.rootButton.isChecked() and 'yes' or 'no', True) globalref.options.changeData('PrintOpenOnly', self.openOnlyButton.isChecked() and 'yes' or 'no', True) globalref.options.changeData('PrintKeepFirstChild', self.widowButton.isChecked() and 'yes' or 'no', True) class PageSetupPage(QtGui.QWidget): """Page setup print option dialog page""" pageSizes = [u'Letter', u'Legal', u'Tabloid', u'A3', u'A4', u'A5', u'Comm10E', u'C5E', u'DLE'] pageSizeDescr = [_('Letter (8.5 x 11 in.)'), _('Legal (8.5 x 14 in.)'), _('Tabloid (11 x 17 in.)'), _('A3 (279 x 420 mm)'), _('A4 (210 x 297 mm)'), _('A5 (148 x 210 mm)'), _('#10 Envelope (4.125 x 9.5 in.)'), _('C5 Envelope (163 x 229 mm)'), _('DL Envelope (110 x 22 mm)')] units = [u'inch', u'centimeter', u'millimeter'] unitNames = [_('inches'), _('centimeters'), _('millimeters')] unitValues = {'inch': 1.0, 'centimeter': 2.54, 'millimeter': 25.4} def __init__(self, printer, parent=None): QtGui.QWidget.__init__(self, parent) self.printer = printer topLayout = QtGui.QVBoxLayout(self) self.setLayout(topLayout) horizLayout = QtGui.QHBoxLayout() topLayout.addLayout(horizLayout) leftLayout = QtGui.QVBoxLayout() horizLayout.addLayout(leftLayout) paperGroup = QtGui.QGroupBox(_('Paper &Size')) leftLayout.addWidget(paperGroup) paperLayout = QtGui.QVBoxLayout(paperGroup) self.paperBox = QtGui.QComboBox() paperLayout.addWidget(self.paperBox) self.paperBox.addItems(PageSetupPage.pageSizeDescr) sizeList = [getattr(QtGui.QPrinter, name) for name in PageSetupPage.pageSizes] try: sizeNum = sizeList.index(self.printer.pageSize()) except ValueError: sizeNum = 0 self.paperBox.setCurrentIndex(sizeNum) orientGroup = QtGui.QGroupBox(_('Orientation')) leftLayout.addWidget(orientGroup) orientLayout = QtGui.QVBoxLayout(orientGroup) self.portraitButton = QtGui.QRadioButton(_('&Portrait')) orientLayout.addWidget(self.portraitButton) self.landscapeButton = QtGui.QRadioButton(_('&Landscape')) orientLayout.addWidget(self.landscapeButton) if self.printer.orientation() == QtGui.QPrinter.Landscape: self.landscapeButton.setChecked(True) else: self.portraitButton.setChecked(True) unitsGroup = QtGui.QGroupBox(_('&Units')) leftLayout.addWidget(unitsGroup) unitsLayout = QtGui.QVBoxLayout(unitsGroup) self.unitsBox = QtGui.QComboBox() unitsLayout.addWidget(self.unitsBox) self.unitsBox.addItems(PageSetupPage.unitNames) self.currentUnit = globalref.options.strData('PrintUnits', False) try: unitNum = PageSetupPage.units.index(self.currentUnit) except ValueError: self.currentUnit = u'inch' unitNum = 0 self.unitsBox.setCurrentIndex(unitNum) self.connect(self.unitsBox, QtCore.SIGNAL('currentIndexChanged(int)'), self.changeUnits) rightLayout = QtGui.QVBoxLayout() horizLayout.addLayout(rightLayout) columnGroup = QtGui.QGroupBox(_('Columns')) rightLayout.addWidget(columnGroup) columnLayout = QtGui.QGridLayout(columnGroup) numLabel = QtGui.QLabel(_('&Number of columns')) columnLayout.addWidget(numLabel, 0, 0) self.columnSpin = QtGui.QSpinBox() columnLayout.addWidget(self.columnSpin, 0, 1) numLabel.setBuddy(self.columnSpin) self.columnSpin.setMinimum(1) self.columnSpin.setMaximum(optiondefaults.maxNumCol) self.columnSpin.setValue(globalref.options.intData('PrintNumCols', 1, optiondefaults.maxNumCol)) self.spaceLabel = QtGui.QLabel() columnLayout.addWidget(self.spaceLabel, 1, 0) self.columnSpaceSpin = QtGui.QDoubleSpinBox() columnLayout.addWidget(self.columnSpaceSpin, 1, 1) self.spaceLabel.setBuddy(self.columnSpaceSpin) self.columnSpaceSpin.setMinimum(0.0) self.columnSpace = globalref.options.numData('PrintColSpace', 0.0, optiondefaults. maxPrintMargin) offsetGroup = QtGui.QGroupBox(_('Offsets')) rightLayout.addWidget(offsetGroup) offsetLayout = QtGui.QGridLayout(offsetGroup) self.indentLabel = QtGui.QLabel() offsetLayout.addWidget(self.indentLabel, 0, 0) self.indentSpin = QtGui.QDoubleSpinBox() offsetLayout.addWidget(self.indentSpin, 0, 1) self.indentLabel.setBuddy(self.indentSpin) self.indentSpin.setMinimum(0.0) self.indent = globalref.options.numData('PrintIndentOffset', 0.0, optiondefaults.maxPrintIndent) self.horizLabel = QtGui.QLabel() offsetLayout.addWidget(self.horizLabel, 1, 0) self.horizMarginSpin = QtGui.QDoubleSpinBox() offsetLayout.addWidget(self.horizMarginSpin, 1, 1) self.horizLabel.setBuddy(self.horizMarginSpin) self.horizMargin = globalref.options.numData('HorizMargin', optiondefaults.minPrintMargin, optiondefaults.maxPrintMargin) self.vertLabel = QtGui.QLabel() offsetLayout.addWidget(self.vertLabel, 2, 0) self.vertMarginSpin = QtGui.QDoubleSpinBox() offsetLayout.addWidget(self.vertMarginSpin, 2, 1) self.vertLabel.setBuddy(self.vertMarginSpin) self.vertMargin = globalref.options.numData('VertMargin', optiondefaults.minPrintMargin, optiondefaults.maxPrintMargin) self.writeFloatValues() topLayout.addStretch() def saveChanges(self): """Update option data with current dialog settings""" pageSizeName = PageSetupPage.pageSizes[self.paperBox.currentIndex()] pageSize = getattr(QtGui.QPrinter, pageSizeName) self.printer.setPageSize(pageSize) globalref.options.changeData('PrintPageSize', pageSizeName, True) if self.portraitButton.isChecked(): self.printer.setOrientation(QtGui.QPrinter.Portrait) globalref.options.changeData('PrintLandscape', 'no', True) else: self.printer.setOrientation(QtGui.QPrinter.Landscape) globalref.options.changeData('PrintLandscape', 'yes', True) globalref.options.changeData('PrintUnits', self.currentUnit, True) globalref.options.changeData('PrintNumCols', repr(self.columnSpin.value()), True) self.readFloatValues() globalref.options.changeData('PrintColSpace', repr(self.columnSpace), True) globalref.options.changeData('PrintIndentOffset', repr(self.indent), True) globalref.options.changeData('HorizMargin', repr(self.horizMargin), True) globalref.options.changeData('VertMargin', repr(self.vertMargin), True) def writeFloatValues(self): """Convert and write float values to the spin boxes""" factor = PageSetupPage.unitValues[self.currentUnit] stepSize = int(factor * 2) / 10.0 decimals = 2 if self.currentUnit == 'millimeter': decimals = 1 unitText = PageSetupPage.unitNames[PageSetupPage.units. index(self.currentUnit)] self.columnSpaceSpin.setMaximum(optiondefaults.maxPrintMargin * factor) self.columnSpaceSpin.setSingleStep(stepSize) self.columnSpaceSpin.setDecimals(decimals) self.columnSpaceSpin.setValue(self.columnSpace * factor) self.spaceLabel.setText(_('Space &between columns (%s)') % unitText) self.indentSpin.setMaximum(optiondefaults.maxPrintIndent * factor) self.indentSpin.setSingleStep(stepSize) self.indentSpin.setDecimals(decimals) self.indentSpin.setValue(self.indent * factor) self.indentLabel.setText(_('Child &indent offset (%s)') % unitText) self.horizMarginSpin.setMinimum(optiondefaults.minPrintMargin * factor) self.horizMarginSpin.setMaximum(optiondefaults.maxPrintMargin * factor) self.horizMarginSpin.setSingleStep(stepSize) self.horizMarginSpin.setDecimals(decimals) self.horizMarginSpin.setValue(self.horizMargin * factor) self.horizLabel.setText(_('Horizontal page &margins (%s)') % unitText) self.vertMarginSpin.setMinimum(optiondefaults.minPrintMargin * factor) self.vertMarginSpin.setMaximum(optiondefaults.maxPrintMargin * factor) self.vertMarginSpin.setSingleStep(stepSize) self.vertMarginSpin.setDecimals(decimals) self.vertMarginSpin.setValue(self.vertMargin * factor) self.vertLabel.setText(_('Vertical page m&argins (%s)') % unitText) def readFloatValues(self): """Read and convert float values from the spin boxes""" factor = PageSetupPage.unitValues[self.currentUnit] self.columnSpace = self.columnSpaceSpin.value() / factor self.indent = self.indentSpin.value() / factor self.horizMargin = self.horizMarginSpin.value() / factor self.vertMargin = self.vertMarginSpin.value() / factor def changeUnits(self, unitNum): """Change the current unit based on a signal""" self.readFloatValues() self.currentUnit = PageSetupPage.units[unitNum] self.writeFloatValues() class SmallListWidget(QtGui.QListWidget): """ListWidget with a smaller size hint""" def __init__(self, parent=None): QtGui.QListWidget.__init__(self, parent) def sizeHint(self): """Return smaller width""" return QtCore.QSize(100, 80) class FontPage(QtGui.QWidget): """Font selection print option dialog page""" def __init__(self, printData, parent=None): QtGui.QWidget.__init__(self, parent) self.printData = printData self.outputFont = self.printData.getOutputFont() self.currentFont = self.printData.getOptionPrintFont() if not self.currentFont: self.currentFont = self.outputFont topLayout = QtGui.QVBoxLayout(self) self.setLayout(topLayout) defaultBox = QtGui.QGroupBox(_('Default Font')) topLayout.addWidget(defaultBox) defaultLayout = QtGui.QVBoxLayout(defaultBox) self.outputCheck = QtGui.QCheckBox(_('Use &Data Output font')) defaultLayout.addWidget(self.outputCheck) self.outputCheck.setChecked(globalref.options. boolData('PrintUseOutputFont')) self.connect(self.outputCheck, QtCore.SIGNAL('clicked(bool)'), self.setFontSelectAvail) self.fontBox = QtGui.QGroupBox(_('Select Font')) topLayout.addWidget(self.fontBox) fontLayout = QtGui.QGridLayout(self.fontBox) spacing = fontLayout.spacing() fontLayout.setSpacing(0) label = QtGui.QLabel(_('&Font')) fontLayout.addWidget(label, 0, 0) label.setIndent(2) self.familyEdit = QtGui.QLineEdit() fontLayout.addWidget(self.familyEdit, 1, 0) self.familyEdit.setReadOnly(True) self.familyList = SmallListWidget() fontLayout.addWidget(self.familyList, 2, 0) label.setBuddy(self.familyList) self.familyEdit.setFocusProxy(self.familyList) fontLayout.setColumnMinimumWidth(1, spacing) families = [unicode(fam) for fam in QtGui.QFontDatabase().families()] families.sort(lambda x,y: cmp(x.lower(), y.lower())) self.familyList.addItems(families) self.connect(self.familyList, QtCore.SIGNAL('currentItemChanged(QListWidgetItem*, '\ 'QListWidgetItem*)'), self.updateFamily) label = QtGui.QLabel(_('Font st&yle')) fontLayout.addWidget(label, 0, 2) label.setIndent(2) self.styleEdit = QtGui.QLineEdit() fontLayout.addWidget(self.styleEdit, 1, 2) self.styleEdit.setReadOnly(True) self.styleList = SmallListWidget() fontLayout.addWidget(self.styleList, 2, 2) label.setBuddy(self.styleList) self.styleEdit.setFocusProxy(self.styleList) fontLayout.setColumnMinimumWidth(3, spacing) self.connect(self.styleList, QtCore.SIGNAL('currentItemChanged(QListWidgetItem*, '\ 'QListWidgetItem*)'), self.updateStyle) label = QtGui.QLabel(_('&Size')) fontLayout.addWidget(label, 0, 4) label.setIndent(2) self.sizeEdit = QtGui.QLineEdit() fontLayout.addWidget(self.sizeEdit, 1, 4) self.sizeEdit.setFocusPolicy(QtCore.Qt.ClickFocus) validator = QtGui.QIntValidator(1, 512, self) self.sizeEdit.setValidator(validator) self.sizeList = SmallListWidget() fontLayout.addWidget(self.sizeList, 2, 4) label.setBuddy(self.sizeList) self.connect(self.sizeList, QtCore.SIGNAL('currentItemChanged(QListWidgetItem*, '\ 'QListWidgetItem*)'), self.updateSize) fontLayout.setColumnStretch(0, 38) fontLayout.setColumnStretch(2, 24) fontLayout.setColumnStretch(4, 10) sampleBox = QtGui.QGroupBox(_('Sample')) topLayout.addWidget(sampleBox) sampleLayout = QtGui.QVBoxLayout(sampleBox) self.sampleEdit = QtGui.QLineEdit() sampleLayout.addWidget(self.sampleEdit) self.sampleEdit.setAlignment(QtCore.Qt.AlignCenter) self.sampleEdit.setText('AaBbCcDdEeFfGg...TtUuVvWvXxYyZz') self.sampleEdit.setFixedHeight(self.sampleEdit.sizeHint().height() * 2) self.setFontSelectAvail() def setFontSelectAvail(self): """Disable font selection if default is checked""" if self.outputCheck.isChecked(): font = self.readFont() if font: self.currentFont = font self.setFont(self.outputFont) self.fontBox.setEnabled(False) else: self.setFont(self.currentFont) self.fontBox.setEnabled(True) def setFont(self, font): """Set the font selector to the given font""" fontInfo = QtGui.QFontInfo(font) family = fontInfo.family() matches = self.familyList.findItems(family, QtCore.Qt.MatchExactly) if matches: self.familyList.setCurrentItem(matches[0]) self.familyList.scrollToItem(matches[0], QtGui.QAbstractItemView.PositionAtTop) style = QtGui.QFontDatabase().styleString(fontInfo) matches = self.styleList.findItems(style, QtCore.Qt.MatchExactly) if matches: self.styleList.setCurrentItem(matches[0]) self.styleList.scrollToItem(matches[0]) size = repr(fontInfo.pointSize()) matches = self.sizeList.findItems(size, QtCore.Qt.MatchExactly) if matches: self.sizeList.setCurrentItem(matches[0]) self.sizeList.scrollToItem(matches[0]) def updateFamily(self, currentItem, previousItem): """Update the family edit box and adjust the style and size options""" family = unicode(currentItem.text()) self.familyEdit.setText(family) if self.familyEdit.hasFocus(): self.familyEdit.selectAll() prevStyle = unicode(self.styleEdit.text()) prevSize = unicode(self.sizeEdit.text()) fontDb = QtGui.QFontDatabase() styles = [unicode(style) for style in fontDb.styles(family)] self.styleList.clear() self.styleList.addItems(styles) if prevStyle: try: num = styles.index(prevStyle) except ValueError: num = 0 self.styleList.setCurrentRow(num) self.styleList.scrollToItem(self.styleList.currentItem()) sizes = [repr(size) for size in fontDb.pointSizes(family)] self.sizeList.clear() self.sizeList.addItems(sizes) if prevSize: try: num = sizes.index(prevSize) except ValueError: num = 0 self.sizeList.setCurrentRow(num) self.sizeList.scrollToItem(self.sizeList.currentItem()) self.updateSample() def updateStyle(self, currentItem, previousItem): """Update the style edit box""" if currentItem: style = unicode(currentItem.text()) self.styleEdit.setText(style) if self.styleEdit.hasFocus(): self.styleEdit.selectAll() self.updateSample() def updateSize(self, currentItem, previousItem): """Update the size edit box""" if currentItem: size = unicode(currentItem.text()) self.sizeEdit.setText(size) if self.sizeEdit.hasFocus(): self.sizeEdit.selectAll() self.updateSample() def updateSample(self): """Update the font sample edit font""" font = self.readFont() if font: self.sampleEdit.setFont(font) def readFont(self): """Return the selected font or None""" family = unicode(self.familyEdit.text()) style = unicode(self.styleEdit.text()) size = unicode(self.sizeEdit.text()) if family and style and size: return QtGui.QFontDatabase().font(family, style, int(size)) return None def saveChanges(self): """Update option data with current dialog settings""" if self.outputCheck.isChecked(): globalref.options.changeData('PrintUseOutputFont', 'yes', True) self.printData.printFont = self.outputFont else: globalref.options.changeData('PrintUseOutputFont', 'no', True) font = self.readFont() if font: self.currentFont = font self.printData.setOptionPrintFont(self.currentFont) self.printData.printFont = self.currentFont class FieldListWidget(QtGui.QTreeWidget): """TreeWidget with a smaller size hint""" def __init__(self, parent=None): QtGui.QTreeWidget.__init__(self, parent) self.setRootIsDecorated(False) self.setColumnCount(2) self.setHeaderLabels([_('Name'), _('Type')]) def sizeHint(self): """Return smaller width""" return QtCore.QSize(150, 60) class HeaderPage(QtGui.QWidget): """Header/footer print option dialog page""" names = [_('&Header Left'), _('Header C&enter'), _('He&ader Right'), _('Footer &Left'), _('Footer Ce&nter'), _('Footer R&ight')] fieldPattern = re.compile('{\*.*?\*}') def __init__(self, parent=None): QtGui.QWidget.__init__(self, parent) self.fileInfoFormat = copy.deepcopy(globalref.docRef.fileInfoFormat) self.fileInfoFormatModified = False topLayout = QtGui.QGridLayout(self) self.setLayout(topLayout) fieldBox = QtGui.QGroupBox(_('Fiel&ds')) topLayout.addWidget(fieldBox, 0, 0, 3, 1) fieldLayout = QtGui.QVBoxLayout(fieldBox) self.fieldListWidget = FieldListWidget() fieldLayout.addWidget(self.fieldListWidget) fieldFormatButton = QtGui.QPushButton(_('Field Forma&t')) fieldLayout.addWidget(fieldFormatButton) self.connect(fieldFormatButton, QtCore.SIGNAL('clicked()'), self.fieldFormat) self.addFieldButton = QtGui.QPushButton('>>') topLayout.addWidget(self.addFieldButton, 0, 1) self.addFieldButton.setMaximumWidth(self.addFieldButton.sizeHint(). height()) self.connect(self.addFieldButton, QtCore.SIGNAL('clicked()'), self.addField) self.delFieldButton = QtGui.QPushButton('<<') topLayout.addWidget(self.delFieldButton, 1, 1) self.delFieldButton.setMaximumWidth(self.delFieldButton.sizeHint(). height()) self.connect(self.delFieldButton, QtCore.SIGNAL('clicked()'), self.delField) headerFooterBox = QtGui.QGroupBox(_('Header and Footer')) topLayout.addWidget(headerFooterBox, 0, 2, 2, 1) headerFooterLayout = QtGui.QGridLayout(headerFooterBox) spacing = headerFooterLayout.spacing() headerFooterLayout.setSpacing(0) self.textEdits = [] for num, name in enumerate(HeaderPage.names): if num < 3: row = 1 col = num * 2 else: row = 4 col = (num - 3) * 2 label = QtGui.QLabel(name) headerFooterLayout.addWidget(label, row - 1, col) label.setIndent(2) lineEdit = configdialog.TitleEdit() headerFooterLayout.addWidget(lineEdit, row, col) label.setBuddy(lineEdit) self.textEdits.append(lineEdit) self.connect(lineEdit, QtCore.SIGNAL('cursorPositionChanged(int, int)'), self.setButtonAvail) self.connect(lineEdit, QtCore.SIGNAL('focusIn'), self.setCurrentEditor) headerFooterLayout.setColumnMinimumWidth(1, spacing) headerFooterLayout.setColumnMinimumWidth(3, spacing) headerFooterLayout.setRowMinimumHeight(2, spacing) self.loadFields() self.loadText() self.focusedEditor = self.textEdits[0] self.setButtonAvail() def setButtonAvail(self): """Update button availability""" currentFieldPos = self.currentFieldPos() self.addFieldButton.setEnabled(currentFieldPos == ()) self.delFieldButton.setEnabled(len(currentFieldPos) > 1) def setCurrentEditor(self, sender): """Set focusedEditor based on editor focus change signal""" self.focusedEditor = sender self.setButtonAvail() def loadFields(self, selNum=0): """Load list with field names""" self.fieldListWidget.clear() for field in self.fileInfoFormat.fieldList: QtGui.QTreeWidgetItem(self.fieldListWidget, [field.name, _(field.typeName)]) self.fieldListWidget.setItemSelected(self.fieldListWidget. topLevelItem(selNum), True) def loadText(self): """Load text into editors""" lines = self.fileInfoFormat.getLines() lines.extend([''] * (6 - len(lines))) for editor, line in zip(self.textEdits, lines): editor.blockSignals(True) editor.setText(line) editor.blockSignals(False) def addField(self): """Add selected field to active header""" fieldName = unicode(self.fieldListWidget.selectedItems()[0].text(0)) text = u'{*!%s*}' % fieldName editor = self.focusedEditor editor.insert(text) editor.setFocus() def delField(self): """Remove field at cursor from active header""" start, end = self.currentFieldPos() editor = self.focusedEditor editor.setSelection(start, end - start) editor.insert('') editor.setFocus() def currentFieldPos(self): """Return tuple of start, end for field at cursorPos in focusedEditor or (None,) if selection overlaps a field end, or empty tuple if not found""" textLine = unicode(self.focusedEditor.text()) cursorPos = self.focusedEditor.cursorPosition() anchorPos = self.focusedEditor.selectionStart() if anchorPos < 0: anchorPos = cursorPos elif anchorPos == cursorPos: # backward selection cursorPos += len(unicode(self.focusedEditor.selectedText())) for match in HeaderPage.fieldPattern.finditer(textLine): cursorIn = match.start() < cursorPos < match.end() anchorIn = match.start() < anchorPos < match.end() if cursorIn and anchorIn: return (match.start(), match.end()) if cursorIn or anchorIn: return (None,) return () def fieldFormat(self): """Show the dialog to change field formats""" fieldName = unicode(self.fieldListWidget.selectedItems()[0].text(0)) dlg = HeaderFieldFormatDialog(fieldName, self.fileInfoFormat, self) if dlg.exec_() == QtGui.QDialog.Accepted: if dlg.modified: self.fileInfoFormatModified = True def saveChanges(self): """Update option data with current dialog settings""" newLines = [unicode(editor.text()) for editor in self.textEdits] prevLines = self.fileInfoFormat.getLines() prevLines.extend([''] * (6 - len(prevLines))) self.fileInfoFormat.lineList = [] for num, (newLine, prevLine) in enumerate(zip(newLines, prevLines)): if newLine: self.fileInfoFormat.insertLine(newLine, num) if newLine != prevLine: self.fileInfoFormatModified = True if self.fileInfoFormatModified: globalref.docRef.undoStore.addFormatUndo(globalref.docRef. treeFormats, globalref.docRef. fileInfoFormat, {}, {}) globalref.docRef.treeFormats[self.fileInfoFormat.name] = \ self.fileInfoFormat globalref.docRef.fileInfoFormat = self.fileInfoFormat globalref.docRef.treeFormats.updateAllLineFields() globalref.docRef.modified = True globalref.updateViewMenuStat() self.fileInfoFormatModified = False class HeaderFieldFormatDialog(QtGui.QDialog): """Dialog to modify file info field formats used in headers and footers""" def __init__(self, fieldName, fileInfoFormat, parent=None): QtGui.QDialog.__init__(self, parent) self.field = fileInfoFormat.findField(fieldName) self.fileInfoFormat = fileInfoFormat self.modified = False self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Field Format for "%s"') % fieldName) topLayout = QtGui.QVBoxLayout(self) self.setLayout(topLayout) horizLayout = QtGui.QHBoxLayout() topLayout.addLayout(horizLayout) extraBox = QtGui.QGroupBox(_('Extra Text')) horizLayout.addWidget(extraBox) extraLayout = QtGui.QVBoxLayout(extraBox) spacing = extraLayout.spacing() extraLayout.setSpacing(0) prefixLabel = QtGui.QLabel(_('&Prefix')) extraLayout.addWidget(prefixLabel) self.prefixEdit = QtGui.QLineEdit() extraLayout.addWidget(self.prefixEdit) prefixLabel.setBuddy(self.prefixEdit) extraLayout.addSpacing(spacing) extraLayout.addStretch(1) suffixLabel = QtGui.QLabel(_('Suffi&x')) extraLayout.addWidget(suffixLabel) self.suffixEdit = QtGui.QLineEdit() extraLayout.addWidget(self.suffixEdit) suffixLabel.setBuddy(self.suffixEdit) rightLayout = QtGui.QVBoxLayout() horizLayout.addLayout(rightLayout) self.formatBox = QtGui.QGroupBox(_('O&utput Format')) rightLayout.addWidget(self.formatBox) formatLayout = QtGui.QHBoxLayout(self.formatBox) self.formatEdit = QtGui.QLineEdit() formatLayout.addWidget(self.formatEdit) self.helpButton = QtGui.QPushButton(_('Format &Help')) formatLayout.addWidget(self.helpButton) self.connect(self.helpButton, QtCore.SIGNAL('clicked()'), self.formatHelp) self.handleBox = QtGui.QGroupBox(_('Content Text Handling')) rightLayout.addWidget(self.handleBox) handleLayout = QtGui.QVBoxLayout(self.handleBox) self.htmlButton = QtGui.QRadioButton(_('Allow HT&ML rich text')) handleLayout.addWidget(self.htmlButton) self.plainButton = QtGui.QRadioButton(_('Plai&n text with '\ 'line breaks')) handleLayout.addWidget(self.plainButton) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) self.prefixEdit.setText(self.field.prefix) self.suffixEdit.setText(self.field.suffix) self.formatEdit.setText(self.field.format) self.htmlButton.setChecked(self.field.html) self.plainButton.setChecked(not self.field.html) self.formatBox.setEnabled(self.field.defaultFormat != '') self.handleBox.setEnabled(self.field.htmlOption) def formatHelp(self): """Provide format help menu based on button signal""" menu = QtGui.QMenu(self) self.formatDict = {} for item in self.field.formatMenuList: if item: descr, key = item self.formatDict[descr] = key menu.addAction(descr) else: menu.addSeparator() menu.popup(self.helpButton. mapToGlobal(QtCore.QPoint(0, self.helpButton.height()))) self.connect(menu, QtCore.SIGNAL('triggered(QAction*)'), self.insertFormat) def insertFormat(self, action): """Insert format text from id into edit box""" self.formatEdit.insert(self.formatDict[unicode(action.text())]) def accept(self): """Set changes after OK is hit""" prefix = unicode(self.prefixEdit.text()) if self.field.prefix != prefix: self.field.prefix = prefix self.modified = True suffix = unicode(self.suffixEdit.text()) if self.field.suffix != suffix: self.field.suffix = suffix self.modified = True format = unicode(self.formatEdit.text()) if self.field.format != format: self.field.format = format self.modified = True if self.field.htmlOption: html = self.htmlButton.isChecked() if self.field.html != html: self.field.html = html self.modified = True QtGui.QDialog.accept(self) TreeLine/source/treexmlparse.py0000644000175000017500000003400311651514477015643 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treexmlparse.py, provides classes to read XML and HTML files # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import re import xml.sax import HTMLParser import nodeformat from treeitem import TreeItem from treeformats import TreeFormats class TreeSaxHandler(xml.sax.ContentHandler): """Handler to read xml thru sax""" def __init__(self, docRef): xml.sax.ContentHandler.__init__(self) self.docRef = docRef self.currentItem = None self.bareFormat = None self.rootItem = None self.formats = {} self.dataEntry = '' self.text = '' def startElement(self, name, attrs): """Called by the reader at the open tag of each element""" if attrs.get(u'item', ''): # reading TreeItem or bare format format = self.formats.get(name, None) if not format: format = nodeformat.NodeFormat(name, attrs) self.formats[name] = format if attrs.get(u'item', '') == 'y': # reading TreeItem newItem = TreeItem(self.currentItem, name) if self.currentItem: self.currentItem.childList.append(newItem) else: self.rootItem = newItem if attrs.get(u'nospace', '').startswith('y'): self.docRef.spaceBetween = False if attrs.get(u'nobreaks', '').startswith('y'): self.docRef.lineBreaks = False if attrs.get(u'nohtml', '').startswith('y'): self.docRef.formHtml = False if attrs.get(u'childsep', ''): self.docRef.childFieldSep = attrs.get(u'childsep', '') self.docRef.spellChkLang = attrs.get(u'spellchk', '') self.docRef.xslCssLink = attrs.get(u'xslcss', '') self.docRef.tlVersion = attrs.get(u'tlversion', '') self.currentItem = newItem else: # reading bare format self.bareFormat = format else: # reading data self.text = '' self.dataEntry = name if not self.currentItem: raise xml.sax.SAXException, 'No valid item' currentFormat = self.bareFormat if not currentFormat: try: currentFormat = self.formats[self.currentItem.formatName] except KeyError: raise xml.sax.SAXException, 'Invalid node type' if name not in currentFormat.fieldNames(): # add new field to format try: currentFormat.addNewField(name, attrs) except (NameError, KeyError): raise xml.sax.SAXException, 'Invalid field type' def endElement(self, name): """Called by the reader at the close tag of each element""" if not self.currentItem: raise xml.sax.SAXException, 'No valid item' if self.dataEntry == name: # finish data # self.simplifyText() if self.text: self.currentItem.data[name] = self.text self.dataEntry = '' else: # finish TreeItem if self.bareFormat: assert(self.bareFormat.name == name) self.bareFormat = None else: assert(self.currentItem.formatName == name) self.currentItem = self.currentItem.parent def characters(self, content): """Called by the reader to process text""" self.text += content def simplifyText(self): """Remove extra whitespace from text""" self.text = u' '.join(self.text.split()) def processingInstruction(self, target, data): """Recover xlst link from stylesheet instruction""" self.docRef.xlstLink = u' '.join((target, data)) class XbelSaxHandler(xml.sax.ContentHandler): """Handler to parse XBEL bookmark format""" def __init__(self, folderFormat, bookmarkFormat, separatorFormat): xml.sax.ContentHandler.__init__(self) self.folderFormat = folderFormat self.bookmarkFormat = bookmarkFormat self.separatorFormat = separatorFormat self.currentItem = None self.rootItem = None self.text = '' def startElement(self, name, attrs): """Called by the reader at the open tag of each element""" if name == u'folder' or name == u'xbel': newItem = TreeItem(self.currentItem, self.folderFormat.name) if self.currentItem: self.currentItem.childList.append(newItem) else: self.rootItem = newItem if attrs.get(u'folded', '') == 'no': newItem.open = True self.currentItem = newItem elif name == u'bookmark': newItem = TreeItem(self.currentItem, self.bookmarkFormat.name) if self.currentItem: self.currentItem.childList.append(newItem) else: raise xml.sax.SAXException, 'No valid parent folder' newItem.data[TreeFormats.linkFieldName] = attrs.get(u'href', '') self.currentItem = newItem elif name == u'title': self.text = '' elif name == u'separator': newItem = TreeItem(self.currentItem, self.separatorFormat.name) if self.currentItem: self.currentItem.childList.append(newItem) else: raise xml.sax.SAXException, 'No valid parent folder' self.currentItem = newItem else: # unsupported tags pass def endElement(self, name): """Called by the reader at the close tag of each element""" if not self.currentItem: raise xml.sax.SAXException, 'No valid item' if name in (u'folder', u'xbel', u'bookmark', u'separator'): self.currentItem = self.currentItem.parent elif name == u'title': self.currentItem.data[TreeFormats.fieldDefault] = \ u' '.join(self.text.split()) else: # unsupported tags pass def characters(self, content): """Called by the reader to process text""" self.text += content class OdfSaxHandler(xml.sax.ContentHandler): """Handler to parse Open Document Format (ODF) file""" numExp = re.compile(r'.*?(\d+)$') def __init__(self, rootItem, defaultFormat): xml.sax.ContentHandler.__init__(self) self.rootItem = rootItem self.defaultFormat = defaultFormat self.inTextArea = False self.currentItem = rootItem self.currentLevel = 0 self.text = u'' def startElement(self, name, attrs): """Called by the reader at the open tag of each element""" if self.inTextArea: if name == u'text:h': style = attrs.get(u'text:style-name', '') try: level = int(OdfSaxHandler.numExp.match(style).group(1)) except AttributeError: raise xml.sax.SAXException, 'Illegal heading style name' if level > self.currentLevel + 1 or level < 1: raise xml.sax.SAXException, 'Invalid heading structure' while self.currentLevel >= level: self.currentItem = self.currentItem.parent self.currentLevel -= 1 self.addItem(level) self.text = '' elif name == u'text:p': if self.currentItem == self.rootItem: self.addItem(1) # add node for text without heading if self.currentItem.data.get(TreeFormats.textFieldName, u''): self.text = u'\n' else: self.text = u'' elif name == u'office:text': self.inTextArea = True self.text = u'' def addItem(self, level): """Add a child of the current item""" item = TreeItem(self.currentItem, self.defaultFormat.name) self.currentItem.childList.append(item) self.currentItem = item self.currentLevel = level def addText(self, fieldName): """Append text to given field name in current item""" if self.text: currText = self.currentItem.data.get(fieldName, '') self.currentItem.data[fieldName] = currText + self.text self.text = u'' def endElement(self, name): """Called by the reader at the close tag of each element""" if not self.inTextArea: return if name == u'office:text': self.inTextArea = False elif name == u'text:h': self.addText(TreeFormats.fieldDefault) elif name == u'text:p': self.addText(TreeFormats.textFieldName) def characters(self, content): """Called by the reader to process text""" self.text += content class GenericXmlHandler(xml.sax.ContentHandler): """Handler to parse generic XML (non-TreeLine file)""" textFieldName = _('Element_Data', 'xml field name') def __init__(self): xml.sax.ContentHandler.__init__(self) self.formats = {} self.currentItem = None self.rootItem = None self.text = '' def startElement(self, name, attrs): """Called by the reader at the open tag of each element""" format = self.formats.get(name, None) if not format: format = nodeformat.NodeFormat(name) self.formats[name] = format newItem = TreeItem(self.currentItem, name) if self.currentItem: self.currentItem.childList.append(newItem) elif self.rootItem: raise xml.sax.SAXException, 'Invalid XML file' else: self.rootItem = newItem self.currentItem = newItem for key in attrs.keys(): format.addFieldIfNew(key) newItem.data[key] = attrs[key] def endElement(self, name): """Called by the reader at the close tag of each element""" if not self.currentItem: raise xml.sax.SAXException, 'No valid item' self.text = self.text.strip() if self.text: self.formats[self.currentItem.formatName].\ addFieldIfNew(GenericXmlHandler.textFieldName) self.currentItem.data[GenericXmlHandler.textFieldName] = self.text self.text = '' self.currentItem = self.currentItem.parent def characters(self, content): """Called by the reader to process text""" self.text += content class HtmlBookmarkHandler(HTMLParser.HTMLParser): """Handler to parse XBEL bookmark format""" escDict = {'amp': '&', 'lt': '<', 'gt': '>', 'quot': '"'} def __init__(self, folderFormat, bookmarkFormat, separatorFormat): HTMLParser.HTMLParser.__init__(self) self.folderFormat = folderFormat self.bookmarkFormat = bookmarkFormat self.separatorFormat = separatorFormat self.rootItem = TreeItem(None, self.folderFormat.name) self.rootItem.data[TreeFormats.fieldDefault] = _('Bookmarks') self.currentItem = self.rootItem self.currentParent = None self.text = '' def handle_starttag(self, tag, attrs): """Called by the reader at each open tag""" if tag == 'dt' or tag == 'h1': # start any entry self.text = '' elif tag == 'dl': # start indent self.currentParent = self.currentItem self.currentItem = None elif tag == 'h3': # start folder if not self.currentParent: raise HtmlParseError, 'No valid parent folder' self.currentItem = TreeItem(self.currentParent, self.folderFormat.name) self.currentParent.childList.append(self.currentItem) elif tag == 'a': # start link if not self.currentParent: raise HtmlParseError, 'No valid parent folder' self.currentItem = TreeItem(self.currentParent, self.bookmarkFormat.name) self.currentParent.childList.append(self.currentItem) for name, value in attrs: if name == 'href': self.currentItem.data[TreeFormats.linkFieldName] = value elif tag == 'hr': # separator if not self.currentParent: raise HtmlParseError, 'No valid parent folder' item = TreeItem(self.currentParent, self.separatorFormat.name) self.currentParent.childList.append(item) self.currentItem = None def handle_endtag(self, tag): """Called by the reader at each end tag""" if tag == 'dl': # end indented section self.currentParent = self.currentParent.parent self.currentItem = None elif tag == 'h3' or tag == 'a': # end folder or link if not self.currentItem: raise HtmlParseError, 'No valid item' self.currentItem.data[TreeFormats.fieldDefault] = self.text elif tag == 'h1': # end main title self.rootItem.data[TreeFormats.fieldDefault] = self.text def handle_data(self, data): """Called by the reader to process text""" self.text += data def handle_entityref(self, name): """Convert escaped entity ref to char""" self.text += HtmlBookmarkHandler.escDict.get(name, '') class HtmlParseError(Exception): """Exception class for errors on reading HTML content""" pass TreeLine/source/treeeditviews.py0000644000175000017500000007067311651514477016030 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treeeditviews.py, provides classes for the data edit views # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import sys import os.path import tempfile from PyQt4 import QtCore, QtGui import configdialog import treemainwin import optiondefaults import globalref class DataEditLine(QtGui.QTextEdit): """Line editor within data edit view""" fileBrowsePath = '' def __init__(self, field, item, labelRef, parent=None): QtGui.QTextEdit.__init__(self, parent) self.field = field self.item = item self.labelRef = labelRef self.sizeCache = None self.setAcceptRichText(False) self.setTabChangesFocus(True) self.setWordWrapMode(QtGui.QTextOption.WordWrap) self.labelFont = QtGui.QFont(labelRef.font()) self.labelBoldFont = QtGui.QFont(self.labelFont) self.labelBoldFont.setBold(True) self.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) editText, ok = self.field.editText(item) if not ok: self.labelRef.setFont(self.labelBoldFont) self.setPlainText(editText) try: self.document().adjustSize() # fix scroll bar overlapping except AttributeError: pass # not in Qt < 4.2 self.connect(self, QtCore.SIGNAL('textChanged()'), self.readChange) def readChange(self): """Update variable from edit contents""" text = unicode(self.toPlainText()).strip() editText, ok = self.field.editText(self.item) if text != editText: globalref.docRef.undoStore.addDataUndo(self.item, True) newText, ok = self.field.storedText(text) self.item.data[self.field.name] = newText self.labelRef.setFont(ok and self.labelFont or self.labelBoldFont) globalref.docRef.modified = True self.sizeCache = None self.emit(QtCore.SIGNAL('entryChanged')) if globalref.pluginInterface: globalref.pluginInterface.execCallback(globalref. pluginInterface. dataChangeCallbacks, self.item, [self.field]) def fileBrowse(self): """Open file browser to set contents""" dfltPath = unicode(self.toPlainText()).strip() if not dfltPath or not os.path.exists(dfltPath): dfltPath = DataEditLine.fileBrowsePath if not dfltPath or not os.path.exists(dfltPath): dfltPath = os.path.dirname(globalref.docRef.fileName) fileName = unicode(QtGui.QFileDialog.getOpenFileName(self, _('Browse for file name'), dfltPath, '%s (*)' % _('All Files'))) if fileName: DataEditLine.fileBrowsePath = os.path.dirname(fileName) if ' ' in fileName and self.field.typeName == u'ExecuteLink': fileName = "'%s'" % fileName self.setPlainText(fileName) def showExtEditor(self): """Start external editor for the text in this edit box""" tmpPathName = self.writeTmpFile() if tmpPathName and self.findExtEditor(tmpPathName): try: f = file(tmpPathName, 'r') self.setPlainText(f.read().strip().decode('utf-8')) f.close() except IOError: pass try: os.remove(tmpPathName) except OSError: print 'Could not remove tmp file %s' % tmpPathName.\ encode(globalref.localTextEncoding) def writeTmpFile(self): """Write tmp file with editor contents, return successful path""" fd, fullPath = tempfile.mkstemp(prefix='tl_', text=True) try: f = os.fdopen(fd, 'w') f.write(unicode(self.toPlainText()).strip().encode('utf-8')) f.close() except IOError: return '' return fullPath def findExtEditor(self, argument): """Find and launch external editor, look in option text, then EDITOR variable, then prompt for new option text, return True on success""" paths = [globalref.options.strData('ExtEditorPath', True), os.environ.get('EDITOR', '')] for path in paths: if path and not sys.platform.startswith('win'): if os.system("%s '%s'" % (path, argument)) == 0: return True elif path: try: # spawnl for Win - os.system return value not relaible if os.spawnl(os.P_WAIT, path, os.path.basename(path), argument) <= 0: return True except OSError: pass ans = QtGui.QMessageBox.warning(self, _('External Editor'), _('Could not find an external '\ 'editor.\nManually locate?\n'\ '(or set EDITOR env variable)'), _('&Browse'), _('&Cancel'), '', 0, 1) if ans == 0: filter = sys.platform.startswith('win') and \ '%s (*.exe)' % _('Programs') \ or '%s (*)' % _('All Files') path = unicode(QtGui.QFileDialog.getOpenFileName(self, _('Locate extern'\ 'al editor'), '', filter)) if path: globalref.options.changeData('ExtEditorPath', path, True) globalref.options.writeChanges() return self.findExtEditor(argument) return False def copyAvail(self): """Return True if there is selected text""" return self.textCursor().hasSelection() def insertFromMimeData(self, mimeData): """Override to paste properly from copied node""" try: text = unicode(mimeData.text()) except UnicodeError: return item = globalref.docRef.readXmlString(text) if item: text = item.title() self.insertPlainText(text) def addHtmlTag(self, openTag, closeTag): """Add HTML tag based on popup menu""" cursor = self.textCursor() text = unicode(cursor.selectedText()) selectStart = cursor.selectionStart() + len(openTag) selectEnd = cursor.selectionEnd() + len(openTag) cursor.insertText(u'%s%s%s' % (openTag, text, closeTag)) cursor.setPosition(selectStart) cursor.setPosition(selectEnd, QtGui.QTextCursor.KeepAnchor) self.setTextCursor(cursor) def addHtmlLinkTag(self, ref, text): """Add HTML inline internal link with given text""" self.insertPlainText('%s' % (ref, text)) def contextMenuEvent(self, event): """Override popup menu to add tag submenu, ext editor and config modification menu items""" menu = self.createStandardContextMenu() firstAction = menu.actions()[0] extEditAct = QtGui.QAction(_('&External Editor...'), self) menu.insertAction(firstAction, extEditAct) self.connect(extEditAct, QtCore.SIGNAL('triggered()'), self.showExtEditor) menu.insertMenu(firstAction, globalref.mainWin.tagSubMenu) addLinkAct = QtGui.QAction(_('&Add Internal Link...'), self) menu.insertAction(firstAction, addLinkAct) self.connect(addLinkAct, QtCore.SIGNAL('triggered()'), globalref.mainWin.inlineLinkTagPrompt) menu.insertSeparator(firstAction) typeConfigAct = QtGui.QAction(_('&Modify Type Config...'), self) menu.insertAction(firstAction, typeConfigAct) self.connect(typeConfigAct, QtCore.SIGNAL('triggered()'), self.parent().modifyTypeConfig) fieldConfigAct = QtGui.QAction(_('Modify &Field Config...'), self) menu.insertAction(firstAction, fieldConfigAct) self.connect(fieldConfigAct, QtCore.SIGNAL('triggered()'), self.parent().modifyFieldConfig) menu.insertSeparator(firstAction) menu.exec_(event.globalPos()) def sizeHint(self): """Return preferred size""" width = self.parent().newEditLineWidth() if self.field.hasFileBrowse: width -= DataEditGroup.browseButtonWidth + \ self.parent().layout().spacing() if self.sizeCache and self.sizeCache.width() == width: return self.sizeCache fontMetrics = QtGui.QFontMetrics(self.font()) lineHeight = fontMetrics.lineSpacing() if self.field.numLines > 1: height = self.field.numLines * lineHeight + \ DataEditGroup.vertMargins else: maxNumLines = globalref.options.intData('MaxEditLines', 1, optiondefaults.maxNumLines) textRect = fontMetrics.boundingRect(0, 0, width - \ DataEditGroup.horizMargins, 100000, QtCore.Qt.TextWordWrap, self.toPlainText()) height = min(textRect.height(), maxNumLines * lineHeight) \ + DataEditGroup.vertMargins self.sizeCache = QtCore.QSize(width, height) return self.sizeCache def minimumSizeHint(self): return QtCore.QSize(0, 0) def focusInEvent(self, event): """Signal focus to update html tag command availability""" self.emit(QtCore.SIGNAL('focusChange')) QtGui.QTextEdit.focusInEvent(self, event) def focusOutEvent(self, event): """Signal focus to update html tag command availability""" self.emit(QtCore.SIGNAL('focusChange')) QtGui.QTextEdit.focusOutEvent(self, event) class DataEditCombo(QtGui.QComboBox): """Combo box for fields with choices, fills with options when about to sow list""" def __init__(self, field, item, labelRef, parent=None): QtGui.QComboBox.__init__(self, parent) self.field = field self.item = item self.labelRef = labelRef self.listLoaded = False self.setEditable(True) self.setInsertPolicy(QtGui.QComboBox.NoInsert) self.setAutoCompletion(True) self.listView = QtGui.QTreeWidget() self.listView.setColumnCount(2) self.listView.header().hide() self.listView.setRootIsDecorated(False) self.listView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.setModel(self.listView.model()) self.setView(self.listView) self.setModelColumn(0) self.labelFont = QtGui.QFont(labelRef.font()) self.labelBoldFont = QtGui.QFont(self.labelFont) self.labelBoldFont.setBold(True) editText, ok = self.field.editText(item) if not ok: self.labelRef.setFont(self.labelBoldFont) self.setEditText(editText) self.connect(self, QtCore.SIGNAL('editTextChanged(const QString&)'), self.readChange) def readChange(self, text): """Update variable from edit contents""" # text = unicode(text).strip() # bad results with autocomplete text = unicode(self.lineEdit().text()) editText, ok = self.field.editText(self.item) if text != editText: globalref.docRef.undoStore.addDataUndo(self.item, True) newText, ok = self.field.storedText(text) self.item.data[self.field.name] = newText self.labelRef.setFont(ok and self.labelFont or self.labelBoldFont) globalref.docRef.modified = True self.emit(QtCore.SIGNAL('entryChanged')) if globalref.pluginInterface: globalref.pluginInterface.execCallback(globalref. pluginInterface. dataChangeCallbacks, self.item, [self.field]) def loadListBox(self): """Populate list box for combo""" text = unicode(self.lineEdit().text()) self.blockSignals(True) self.listView.clear() if self.field.autoAddChoices: self.field.addChoice(text, True) strList = self.field.getEditChoices(text) item = None for choice, annot in strList: if choice == None: # separator pass # separator not implemented else: QtGui.QTreeWidgetItem(self.listView, [choice, annot]) try: choices = [choice for (choice, annot) in strList] i = choices.index(text) self.setCurrentIndex(i) except ValueError: editText, ok = self.field.storedText(text) if ok and editText: # add missing item if valid item = QtGui.QTreeWidgetItem(None, [text, '(current)']) self.listView.insertTopLevelItem(0, item) self.setCurrentIndex(0) else: self.setEditText(text) self.listView.resizeColumnToContents(0) self.blockSignals(False) self.listLoaded = True def showPopup(self): """Load combo box before showing it""" self.loadListBox() QtGui.QComboBox.showPopup(self) def copyAvail(self): """Return True if there is selected text""" return self.lineEdit().hasSelectedText() def cut(self): """Pass cut command to lineEdit""" self.lineEdit().cut() def copy(self): """Pass copy command to lineEdit""" self.lineEdit().copy() def paste(self): """Paste text from clipboard""" try: text = unicode(QtGui.QApplication.clipboard().text()) except UnicodeError: return item = globalref.docRef.readXmlString(text) if item: text = item.title() self.lineEdit().insert(text) def contextMenuEvent(self, event): """Override popup menu to add config modification menu items""" menu = self.lineEdit().createStandardContextMenu() firstAction = menu.actions()[0] typeConfigAct = QtGui.QAction(_('&Modify Type Config...'), self) menu.insertAction(firstAction, typeConfigAct) self.connect(typeConfigAct, QtCore.SIGNAL('triggered()'), self.parent().modifyTypeConfig) fieldConfigAct = QtGui.QAction(_('Modify &Field Config...'), self) menu.insertAction(firstAction, fieldConfigAct) self.connect(fieldConfigAct, QtCore.SIGNAL('triggered()'), self.parent().modifyFieldConfig) menu.insertSeparator(firstAction) menu.exec_(event.globalPos()) def sizeHint(self): """Return preferred size""" return QtCore.QSize(self.parent().newEditLineWidth(), QtGui.QFontMetrics(self.font()).lineSpacing() + DataEditGroup.vertMargins) def focusInEvent(self, event): """Update list box to get autocomplete to work""" if not self.listLoaded: self.loadListBox() QtGui.QComboBox.focusInEvent(self, event) def focusOutEvent(self, event): """Update auto choices on leaving the widget""" if self.field.autoAddChoices: self.field.addChoice(unicode(self.lineEdit().text()), True) QtGui.QComboBox.focusOutEvent(self, event) class DataEditGroup(QtGui.QGroupBox): """Collection of editors for one item""" browseButtonWidth = 40 horizMargins = 20 vertMargins = 14 def __init__(self, item, viewRef, parent=None): QtGui.QGroupBox.__init__(self, item.formatName, parent) self.item = item self.viewRef = viewRef self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) layout = QtGui.QGridLayout(self) layout.setColumnStretch(1, 1) self.titleLabel = QtGui.QLabel(self.item.title()) self.titleLabel.setFrameStyle(QtGui.QFrame.Panel | QtGui.QFrame.Sunken) self.titleLabel.setTextFormat(QtCore.Qt.PlainText) layout.addWidget(self.titleLabel, 0, 0, 1, 3) fieldList = [field for field in item.nodeFormat().fieldList if not field.hidden] self.maxLabelWidth = 0 fontMetrics = QtGui.QFontMetrics(self.titleLabel.font()) labels = [] for row, field in enumerate(fieldList): labels.append(QtGui.QLabel(field.labelName())) layout.addWidget(labels[-1], row + 2, 0) self.maxLabelWidth = max(self.maxLabelWidth, fontMetrics.width(labels[-1].text())) for row, field in enumerate(fieldList): if field.hasEditChoices: line = DataEditCombo(field, item, labels[row], self) layout.addWidget(line, row + 2, 1, 1, 2) elif field.hasFileBrowse: line = DataEditLine(field, item, labels[row], self) layout.addWidget(line, row + 2, 1) browseButton = QtGui.QPushButton('...', self) browseButton.setFixedWidth(40) self.connect(browseButton, QtCore.SIGNAL('clicked()'), line.fileBrowse) layout.addWidget(browseButton, row + 2, 2) else: line = DataEditLine(field, item, labels[row], self) layout.addWidget(line, row + 2, 1, 1, 2) self.connect(line, QtCore.SIGNAL('focusChange'), globalref.mainWin.updateAddTagAvail) self.connect(line, QtCore.SIGNAL('entryChanged'), self.checkTitleChange) def checkTitleChange(self): """Update item title based on signal""" globalref.updateViewItem(self.item) self.setTitle(self.item.formatName) self.titleLabel.setText(self.item.title()) globalref.updateViewMenuStat() def newEditLineWidth(self): """Return width remaining for edit widgets""" return max(self.viewRef.width() - self.maxLabelWidth - 80, 100) def modifyTypeConfig(self): """Bring up type config dialog with this data type""" globalref.mainWin.dataConfig(True) configdialog.ConfigDialog.currentType = self.item.formatName configdialog.ConfigDialog.currentField = globalref.docRef.\ treeFormats[self.item.formatName].\ fieldList[0].name treemainwin.TreeMainWin.configDlg.tabs.setCurrentIndex(1) treemainwin.TreeMainWin.configDlg.updatePage() def modifyFieldConfig(self): """Bring up type config dialog with the caller's field""" field = self.sender().parent().field.name globalref.mainWin.dataConfig(True) configdialog.ConfigDialog.currentType = self.item.formatName configdialog.ConfigDialog.currentField = field treemainwin.TreeMainWin.configDlg.tabs.setCurrentIndex(3) treemainwin.TreeMainWin.configDlg.updatePage() class DataEditScrollView(QtGui.QScrollArea): """Right pane view to edit database info""" def __init__(self, parent=None): QtGui.QScrollArea.__init__(self, parent) self.setFocusPolicy(QtCore.Qt.NoFocus) self.fullView = QtGui.QWidget() self.fullLayout = QtGui.QVBoxLayout(self.fullView) self.setWidget(self.fullView) self.dataGroups = [] def replaceGroups(self, items, heightLimit=0, resetScroll=True): """Replace contents with selected item data list, stop adding when heightLimit is reached, return number of items added""" if resetScroll: self.horizontalScrollBar().setValue(0) self.verticalScrollBar().setValue(0) for group in self.dataGroups: group.close() self.dataGroups = [] return self.addItems(items, heightLimit) def addItems(self, items, heightLimit=0): """Adds given items to the view, stop adding when heightLimit is reached, return number of items added""" viewHeightUsed = 0 numItemsCreated = 0 for item in items: group = DataEditGroup(item, self) viewHeightUsed += group.sizeHint().height() + \ self.fullLayout.spacing() if heightLimit and viewHeightUsed > heightLimit: if numItemsCreated == 0: self.dataGroups.append(group) self.fullLayout.addWidget(group) numItemsCreated += 1 break self.dataGroups.append(group) self.fullLayout.addWidget(group) numItemsCreated += 1 self.fullView.adjustSize() return numItemsCreated def invalidateLayouts(self): """Invalidate layouts for resize""" for group in self.dataGroups: group.layout().invalidate() self.fullLayout.invalidate() class DataEditView(QtGui.QWidget): """Right pane parent view to edit database info""" def __init__(self, showChildren=True, parent=None): QtGui.QWidget.__init__(self, parent) self.showChildren = showChildren self.oldViewHeight = 0 self.allItems = [] self.shownRanges = [] topLayout = QtGui.QVBoxLayout(self) topLayout.setSpacing(0) topLayout.setMargin(0) self.controller = QtGui.QWidget() topLayout.addWidget(self.controller) controlLayout = QtGui.QHBoxLayout(self.controller) controlLayout.setMargin(0) self.controlLabel = QtGui.QLabel() controlLayout.addWidget(self.controlLabel) self.backButton = QtGui.QPushButton('<<') controlLayout.addWidget(self.backButton) buttonSize = self.backButton.fontMetrics().\ size(QtCore.Qt.TextShowMnemonic, _('All')) + \ QtCore.QSize(16, 4) self.backButton.setMaximumSize(buttonSize) self.backButton.setFocusPolicy(QtCore.Qt.NoFocus) self.connect(self.backButton, QtCore.SIGNAL('clicked()'), self.viewBack) self.forwardButton = QtGui.QPushButton('>>') controlLayout.addWidget(self.forwardButton) self.forwardButton.setMaximumSize(buttonSize) self.forwardButton.setFocusPolicy(QtCore.Qt.NoFocus) self.connect(self.forwardButton, QtCore.SIGNAL('clicked()'), self.viewForward) allButton = QtGui.QPushButton(_('All')) controlLayout.addWidget(allButton) allButton.setMaximumSize(buttonSize) allButton.setFocusPolicy(QtCore.Qt.NoFocus) self.connect(allButton, QtCore.SIGNAL('clicked()'), self.viewAll) self.scrollView = DataEditScrollView() topLayout.addWidget(self.scrollView) def updateView(self): """Replace contents with selected item data list""" origFirstItem = self.allItems[:1] self.allItems = [] if self.showChildren and len(globalref.docRef.selection) == 1: self.allItems = globalref.docRef.selection[0].childList elif not self.showChildren: self.allItems = globalref.docRef.selection self.hide() resetScroll = self.allItems[:1] != origFirstItem numGroups = self.scrollView.replaceGroups(self.allItems, self.availableHeight(), resetScroll) self.shownRanges = [(0, numGroups)] self.updateControl() self.show() def updateControl(self): """Udate control visibility, label and button availability""" start, end = self.shownRanges[-1] if start == 0 and end == len(self.allItems): self.controller.hide() return if end == start + 1: text = _('Node %(node_num)d of %(total_num)d') % \ {'node_num': end, 'total_num': len(self.allItems)} else: text = _('Nodes %(start_node)d-%(end_node)d of %(total_num)d') \ % {'start_node': start + 1, 'end_node': end, 'total_num': len(self.allItems)} self.controlLabel.setText(text) self.backButton.setEnabled(len(self.shownRanges) > 1) self.forwardButton.setEnabled(end < len(self.allItems)) self.controller.show() def viewBack(self): """View previous set of groups""" self.shownRanges = self.shownRanges[:-1] start, end = self.shownRanges[-1] self.hide() self.scrollView.replaceGroups(self.allItems[start:end]) self.updateControl() self.show() self.scrollView.dataGroups[0].setFocus(QtCore.Qt.TabFocusReason) def viewForward(self): """View next set of groups""" start = self.shownRanges[-1][1] self.hide() numGroups = self.scrollView.replaceGroups(self.allItems[start:], self.availableHeight()) self.shownRanges.append((start, start + numGroups)) self.updateControl() self.show() self.scrollView.dataGroups[0].setFocus(QtCore.Qt.TabFocusReason) def viewAll(self): """View all groups""" QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) self.hide() if len(self.shownRanges) == 1: start = self.shownRanges[-1][1] self.scrollView.addItems(self.allItems[start:]) else: self.scrollView.replaceGroups(self.allItems) self.shownRanges = [(0, len(self.allItems))] self.updateControl() self.show() globalref.focusTree() QtGui.QApplication.restoreOverrideCursor() def availableHeight(self): """Return screen height times number of pages""" numPages = globalref.options.intData('EditorPages', 0, 999) return (self.height() - self.controller.sizeHint().height()) * \ numPages def copyAvail(self): """Return 1 if there is selected text""" if hasattr(self.focusWidget(), 'copyAvail'): return self.focusWidget().copyAvail() return 0 def copy(self): """Copy selections to clipboard""" if hasattr(self.focusWidget(), 'copy'): self.focusWidget().copy() def cut(self): """Cut selections to clipboard""" if hasattr(self.focusWidget(), 'cut'): self.focusWidget().cut() def paste(self): """Paste text given in param""" if hasattr(self.focusWidget(), 'paste'): self.focusWidget().paste() def scrollPage(self, numPages=1): """Scrolls down by numPages (negative for up) leaving a one-line overlap""" delta = self.scrollView.maximumViewportSize().height() if delta == 0: return fontSize = self.fontMetrics().height() if delta > fontSize: delta -= fontSize scrollBar = self.scrollView.verticalScrollBar() if (numPages > 0 and scrollBar.value() < scrollBar.maximum()) or \ (numPages < 0 and scrollBar.value() > scrollBar.minimum()): scrollBar.setValue(scrollBar.value() + numPages * delta) elif numPages < 0: self.viewBack() else: self.viewForward() def resizeEvent(self, event): """Change the minimum viewport size if view size changes""" if (event.oldSize().height() == 0 and event.size().height()) or \ (event.oldSize().width() == 0 and event.size().width()): self.setEnabled(True) self.updateView() else: self.scrollView.invalidateLayouts() QtGui.QWidget.resizeEvent(self, event) self.scrollView.fullView.adjustSize() TreeLine/source/treeview.py0000644000175000017500000005230711651514477014771 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treeview.py, provides classes for the main tree view # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import copy import string from PyQt4 import QtCore, QtGui import treedoc import treeitem import treemainwin import optiondefaults import globalref class TreeViewItem(QtGui.QTreeWidgetItem): """Qt tree item, contains ref to treecore TreeItem""" def __init__(self, parent, docItemRef): QtGui.QTreeWidgetItem.__init__(self, parent) self.docItemRef = docItemRef docItemRef.viewData = self self.tempSortKey = None self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) self.setText(0, docItemRef.title()) self.childrenLoaded = False if self.docItemRef.open: self.treeWidget().expandItem(self) elif self.docItemRef.childList: dummyItem = QtGui.QTreeWidgetItem(self) self.setTreeIcon() def setTreeIcon(self): """Set tree node icon""" if globalref.options.boolData('ShowTreeIcons'): icon = globalref.treeIcons.getIcon(self.docItemRef. nodeFormat().iconName, True) if icon: self.setIcon(0, icon) def loadChildren(self): """Load child items if this item is open and not yet loaded""" if not self.childrenLoaded and self.docItemRef.open: self.takeChild(0) # remove dummy child for child in self.docItemRef.childList: TreeViewItem(self, child) self.childrenLoaded = True def loadTempSortKey(self): """Calculate a list of ancestor's view indexes for sort keys""" indexList = [] index = self.treeWidget().indexFromItem(self) while index.isValid(): indexList.insert(0, index) index = index.parent() self.tempSortKey = [index.row() for index in indexList] class TreeView(QtGui.QTreeWidget): """Left pane view of tree structure""" def __init__(self, parent=None): QtGui.QTreeWidget.__init__(self, parent) self.setColumnCount(1) self.header().hide() self.setRootIsDecorated(True) self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) self.setEditTriggers(QtGui.QAbstractItemView.SelectedClicked) self.updateGenOptions() self.setDragEnabled(True) self.setAcceptDrops(True) self.setDropIndicatorShown(True) self.dragStartPos = None self.incremSearchMode = False self.incremSearchStr = '' self.blockColumnResize = False self.editedItem = None self.noSelectClickCallback = None self.connect(self, QtCore.SIGNAL('itemExpanded(QTreeWidgetItem*)'), self.loadItemChildren) self.connect(self, QtCore.SIGNAL('itemCollapsed(QTreeWidgetItem*)'), self.setCollapsed) self.connect(self, QtCore.SIGNAL('itemSelectionChanged()'), self.changeSelected) self.connect(self, QtCore.SIGNAL('currentItemChanged(QTreeWidgetItem*, '\ 'QTreeWidgetItem*)'), self.changeCurrent) def updateTree(self): """Replace contents of TreeView from the doc""" if globalref.docRef.treeFormats.hasConditionals: globalref.docRef.root.setDescendantCondTypes() origX = self.horizontalScrollBar().value() origMaxX = self.horizontalScrollBar().maximum() origY = self.verticalScrollBar().value() self.blockSignals(True) self.blockColumnResize = True self.clear() self.blockSignals(False) item = TreeViewItem(self, globalref.docRef.root) if origY <= self.verticalScrollBar().maximum(): self.verticalScrollBar().setValue(origY) self.blockSignals(True) if globalref.docRef.selection: try: self.setCurrentItem(globalref.docRef.selection[-1].viewData) globalref.docRef.selection.currentItem = \ globalref.docRef.selection[-1] self.scrollToItem(globalref.docRef.selection[-1].viewData) for node in globalref.docRef.selection: self.setItemSelected(node.viewData, True) except RuntimeError: pass # skip if node doesn't exist anymore self.resizeColumnToContents(0) self.blockColumnResize = False self.blockSignals(False) self.horizontalScrollBar().setMaximum(origMaxX) self.horizontalScrollBar().setValue(origX) def updateSelect(self): """Update view selection""" origX = self.horizontalScrollBar().value() self.blockSignals(True) self.clearSelection() self.setCurrentItem(globalref.docRef.selection[-1].viewData) globalref.docRef.selection.currentItem = globalref.docRef.selection[-1] self.scrollToItem(globalref.docRef.selection[-1].viewData) for node in globalref.docRef.selection: self.setItemSelected(node.viewData, True) self.blockSignals(False) self.horizontalScrollBar().setValue(origX) def updateTreeItem(self, item): """Update the title and open status of item""" if item.viewData and hasattr(item.viewData, 'treeWidget'): try: if globalref.docRef.treeFormats.hasConditionals: item.setConditionalType() item.viewData.setTreeIcon() item.viewData.setText(0, item.title()) if item.open != self.isItemExpanded(item.viewData): self.setItemExpanded(item.viewData, item.open) self.resizeColumnToContents(0) except RuntimeError: pass # skip if doesn't exist anymore due to closed parent def loadItemChildren(self, treeViewItem): """Ensure children are loaded in response to parent expanding""" treeViewItem.docItemRef.open = True treeViewItem.loadChildren() if not self.blockColumnResize: self.resizeColumnToContents(0) def updateGenOptions(self): """Update tree option settings""" self.setIndentation(globalref.options.intData('IndentOffset', 0, optiondefaults.maxIndentOffset)) if globalref.options.boolData('ClickRename'): self.setEditTriggers(QtGui.QAbstractItemView.SelectedClicked) else: self.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) def setCollapsed(self, treeViewItem): """Set collapsing item to closed""" treeViewItem.docItemRef.open = False def changeCurrent(self, currentItem, prevItem): """Set current item in selection, called from tree signal""" if currentItem: globalref.docRef.selection.currentItem = currentItem.docItemRef def changeSelected(self): """Set selection based on signal""" selections = self.selectedItems()[:] if len(selections) > 1 and \ (globalref.options.strData('SelectOrder') == 'tree' or len(selections) > len(globalref.docRef.selection) + 1): # sort if tree order or always for shift-select for item in selections: item.loadTempSortKey() selections.sort(lambda x,y: cmp(x.tempSortKey, y.tempSortKey)) globalref.docRef.selection.replace([item.docItemRef for item in selections]) globalref.updateRightView() def edit(self, index, trigger, event): """Override to block editing with multiple selection, also saves ref to edited item to avoid commiting change to wrong item due to next commands""" if len(globalref.docRef.selection) == 1: result = QtGui.QTreeWidget.edit(self, index, trigger, event) if result: self.editedItem = globalref.docRef.selection[0] return result else: return False def commitData(self, editor): """Change tree based on results of edit operation""" text = unicode(editor.text()) item = self.editedItem if text and text != item.title() and item.setTitle(text, True): QtGui.QTreeWidget.commitData(self, editor) self.resizeColumnToContents(0) globalref.updateRightView() self.editedItem = None def findText(self, wordList, forward=True): """Select item containing words in searchStr in any field, starts with currentItem, return True if found""" return globalref.docRef.selection.findText(wordList, forward) def treeIncremSearch(self): """Begin iterative search""" self.incremSearchMode = True self.incremSearchStr = '' globalref.setStatusBar(_('Search for:'), 0, True) def doIncremSearch(self): """Search for searchStr in all titles""" globalref.setStatusBar(_('Search for: %s') % self.incremSearchStr, 0, True) if globalref.docRef.selection.findTitleText(self.incremSearchStr): globalref.setStatusBar(_('Search for: %s') % self.incremSearchStr, 0, True) else: globalref.setStatusBar(_('Search for: %s (not found)') % self.incremSearchStr, 0, True) def treeIncremNext(self): """Search for next occurance of increm string""" if self.incremSearchStr: if globalref.docRef.selection.findNextTitle(self.incremSearchStr, True): globalref.setStatusBar(_('Next: %s') % self.incremSearchStr, 0, True) else: globalref.setStatusBar(_('Next: %s (not found)') % self.incremSearchStr, 0, True) def treeIncremPrev(self): """Search for previous occurance of increm string""" if self.incremSearchStr: if globalref.docRef.selection.findNextTitle(self.incremSearchStr, False): globalref.setStatusBar(_('Previous: %s') % self.incremSearchStr, 0, True) else: globalref.setStatusBar(_('Previous: %s (not found)') % self.incremSearchStr, 0, True) def showTypeMenu(self): """Show popup menu for changing the item type""" self.scrollToItem(self.currentItem()) rect = self.visualItemRect(self.currentItem()) pt = self.mapToGlobal(QtCore.QPoint(rect.center().x(), rect.bottom())) globalref.mainWin.typeSubMenu.popup(pt) def mimeTypes(self): """Return list of supported mime types""" return ['text/xml'] def mimeData(self): """Return mime data for give TreeWidgetItems""" copyFormat = treedoc.TreeDoc.copyFormat if len(globalref.docRef.selection) > 1: globalref.docRef.treeFormats.addIfMissing(copyFormat) root = treeitem.TreeItem(None, copyFormat.name) for item in globalref.docRef.selection: root.childList.append(copy.copy(item)) root.childList[-1].parent = root else: root = globalref.docRef.selection[0] text = u'\n'.join(root.branchXml([copyFormat])) globalref.docRef.treeFormats.removeQuiet(copyFormat) mime = QtCore.QMimeData() mime.setData('text/xml', text.encode('utf-8')) # mime.setText(text) return mime def dropMimeData(self, parent, mimeData, isCopy=False): """Decode dropped data""" mainWin = self.parent().parent().parent().parent() oldMainWin = globalref.mainWin if globalref.treeControl.duplicateWindows(): oldMainWin.saveMultiWinTree() globalref.updateRefs(mainWin) text = unicode(mimeData.data('text/xml'), 'utf-8') root, newFormats = globalref.docRef.readXmlStringAndFormat(text) if not root: globalref.updateRefs(oldMainWin) return False if root.formatName == treedoc.TreeDoc.copyFormat.name: itemList = root.childList else: itemList = [root] undoParents = [parent] + filter(None, [item.parent for item in globalref.docRef.selection]) if newFormats: globalref.docRef.undoStore.addBranchUndo(undoParents) for format in newFormats: globalref.docRef.treeFormats.addIfMissing(format) globalref.docRef.treeFormats.updateDerivedTypes() globalref.docRef.treeFormats.updateUniqueID() if treemainwin.TreeMainWin.configDlg: treemainwin.TreeMainWin.configDlg.resetParam() else: globalref.docRef.undoStore.addChildListUndo(undoParents) for node in itemList: parent.addTree(node) if isCopy: node.setDescendantUniqueID(True) parent.open = True globalref.docRef.selection.replace(itemList) if newFormats: globalref.docRef.treeFormats.updateAutoChoices() globalref.updateViewAll() globalref.updateRefs(oldMainWin) if globalref.treeControl.duplicateWindows(): oldMainWin.updateMultiWinTree() return True def mousePressEvent(self, event): """Mouse press down event stores position to check for dragging and selects item on right-click for popup menu""" if self.incremSearchMode: self.incremSearchMode = False globalref.setStatusBar('') clickedItem = self.itemAt(event.pos()) if not clickedItem: # skip unselecting click on blank space self.dragStartPos = None return if self.noSelectClickCallback: self.noSelectClickCallback(clickedItem.docItemRef) self.noSelectClickCallback = None return if event.button() == QtCore.Qt.LeftButton: self.dragStartPos = QtCore.QPoint(event.pos()) elif event.button() == QtCore.Qt.RightButton: return # stop rename when context menu is used origX = self.horizontalScrollBar().value() QtGui.QTreeWidget.mousePressEvent(self, event) # work around Qt bug - can't set to old value directly? self.horizontalScrollBar().setValue(origX + 1) self.horizontalScrollBar().setValue(origX) def mouseReleaseEvent(self, event): """Mouse release event for popup menus""" self.dragStartPos = None clickedItem = self.itemAt(event.pos()) if not clickedItem: # skip unselecting click on blank space return QtGui.QTreeWidget.mouseReleaseEvent(self, event) def contextMenuEvent(self, event): """Show popup menu""" if event.reason() == QtGui.QContextMenuEvent.Mouse: clickedItem = self.itemAt(event.pos()) if not clickedItem: event.ignore() return if not self.isItemSelected(clickedItem): self.blockSignals(True) self.clearSelection() self.blockSignals(False) self.setItemSelected(clickedItem, True) pos = event.globalPos() else: # shown for menu key or other reason if not globalref.docRef.selection: event.ignore() return selectList = globalref.docRef.selection[:] if globalref.docRef.selection.currentItem in selectList: selectList.insert(0, globalref.docRef.selection.currentItem) posList = [self.visualItemRect(item.viewData).bottomLeft() for item in selectList] posList = [pos for pos in posList if self.rect().contains(pos)] if not posList: posList = [QtCore.QPoint(0, 0)] pos = self.mapToGlobal(posList[0]) parentList = [item for item in globalref.docRef.selection if item.childList] if parentList: menu = globalref.mainWin.parentPopup else: menu = globalref.mainWin.childPopup menu.popup(pos) event.accept() def mouseMoveEvent(self, event): """Mouse move event to start drag & drop""" if event.buttons() == QtCore.Qt.LeftButton and self.dragStartPos and \ globalref.docRef.selection and \ (event.pos() - self.dragStartPos).manhattanLength() > \ QtGui.QApplication.startDragDistance() and \ globalref.options.boolData('DragTree'): oldSelect = globalref.docRef.selection[:] drag = QtGui.QDrag(self) drag.setMimeData(self.mimeData()) dropAction = drag.start(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction) if dropAction == QtCore.Qt.MoveAction: if drag.target() == None: # move to different session if globalref.docRef.root in oldSelect: return # can't delete root undoParents = filter(None, [item.parent for item in globalref.docRef.selection]) globalref.docRef.undoStore.addChildListUndo(undoParents) globalref.docRef.selection.replace([undoParents[0]]) elif filter(None, [node.hasDescendant(globalref.docRef. selection[0]) for node in oldSelect]): return # don't delete if drag to descendant for item in oldSelect: item.delete() globalref.updateViewAll() def dragMoveEvent(self, event): """Drag move event to set proper (+) or (-) for drag & drop""" event.setDropAction(self.dropActionFromEvent(event)) event.accept() def dropEvent(self, event): """Drop event for drag & drop""" parentItem = self.itemAt(event.pos()) if parentItem and (not self.isItemSelected(parentItem) or not event.source()): event.setDropAction(self.dropActionFromEvent(event)) if self.dropMimeData(parentItem.docItemRef, event.mimeData(), event.dropAction() == QtCore.Qt.CopyAction): event.accept() return event.ignore() def dropActionFromEvent(self, event): """Return appropriate action based on modifier keys and drag source""" if event.keyboardModifiers() == QtCore.Qt.ControlModifier: action = QtCore.Qt.CopyAction elif event.keyboardModifiers() == QtCore.Qt.ShiftModifier: action = QtCore.Qt.MoveAction elif event.source() == self: action = QtCore.Qt.MoveAction else: action = QtCore.Qt.CopyAction return action def focusOutEvent(self, event): """Stop incremental search on focus loss""" if self.incremSearchMode: self.incremSearchMode = False globalref.setStatusBar('') QtGui.QTreeWidget.focusOutEvent(self, event) def keyPressEvent(self, event): """Bind keys to functions""" keyText = unicode(event.text()) if self.incremSearchMode: if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Escape): self.incremSearchMode = False globalref.setStatusBar('') elif event.key() == QtCore.Qt.Key_Backspace and \ self.incremSearchStr: self.incremSearchStr = self.incremSearchStr[:-1] self.doIncremSearch() elif keyText and keyText in string.printable: self.incremSearchStr += keyText self.doIncremSearch() event.accept() elif event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter) and \ event.modifiers() == QtCore.Qt.NoModifier and \ globalref.options.boolData('InsertOnEnter') and \ self.state() != QtGui.QAbstractItemView.EditingState: if len(globalref.docRef.selection) == 1 and \ not globalref.docRef.selection[0].parent: globalref.mainWin.editAddChild() # only root selected else: globalref.mainWin.editInAfter() event.accept() else: origX = self.horizontalScrollBar().value() QtGui.QTreeWidget.keyPressEvent(self, event) self.horizontalScrollBar().setValue(origX) TreeLine/source/optiondefaults.py0000644000175000017500000002667611651514477016211 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # optiondefaults.py, provides initial config option values # # TreeLine, an information storage program # Copyright (C) 2009, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import globalref # Limits: minPrintMargin = 0.1 maxPrintMargin = 3.0 maxNumCol = 9 maxNumLines = 200 maxIndentOffset = 250 maxPrintIndent = 3.0 maxNumToolbars = 20 menuKeyBindList = [(N_('FileNew'), 'Ctrl+N'), (N_('FileOpen'), 'Ctrl+O'), (N_('FileOpenSample'), ''), (N_('FileSave'), 'Ctrl+S'), (N_('FileSaveAs'), ''), (N_('FileExport'), ''), (N_('FilePrintOpt'), ''), (N_('FilePrintPreview'), ''), (N_('FilePrint'), 'Ctrl+P'), (N_('FileQuit'), 'Ctrl+Q'), (N_('EditUndo'), 'Ctrl+Z'), (N_('EditRedo'), 'Ctrl+Y'), (N_('EditCut'), 'Ctrl+X'), (N_('EditCopy'), 'Ctrl+C'), (N_('EditCopyText'), ''), (N_('EditPaste'), 'Ctrl+V'), (N_('EditPasteText'), ''), (N_('EditRename'), 'Ctrl+R'), (N_('EditInsertBefore'), 'Ctrl+B'), (N_('EditInsertAfter'), 'Ctrl+I'), (N_('EditAddChild'), 'Ctrl+A'), (N_('EditDelete'), 'Del'), (N_('EditIndent'), 'Ctrl+Shift+Right'), (N_('EditUnindent'), 'Ctrl+Shift+Left'), (N_('EditMoveUp'), 'Ctrl+Shift+Up'), (N_('EditMoveDown'), 'Ctrl+Shift+Down'), (N_('EditMoveFirst'), ''), (N_('EditMoveLast'), ''), (N_('ViewPreviousSelect'), 'Ctrl+Shift+P'), (N_('ViewNextSelect'), 'Ctrl+Shift+N'), (N_('ViewTree'), ''), (N_('ViewFlat'), ''), (N_('ViewDataOutput'), 'Ctrl+Shift+O'), (N_('ViewDataEdit'), 'Ctrl+Shift+E'), (N_('ViewTitleList'), 'Ctrl+Shift+L'), (N_('ViewShowChild'), 'Ctrl+Shift+C'), (N_('ViewShowDescend'), 'Ctrl+Shift+D'), (N_('ViewStatusBar'), ''), (N_('DataSetItemType'), 'Ctrl+T'), (N_('DataSetDescendType'), ''), (N_('DataConfigType'), ''), (N_('DataCopyTypes'), ''), (N_('DataSort'), ''), (N_('DataChange'), ''), (N_('DataNumber'), ''), (N_('DataFilterCond'), ''), (N_('DataFilterText'), ''), (N_('DataFilterClear'), ''), (N_('DataCategoryAdd'), ''), (N_('DataCategoryFlat'), ''), (N_('DataRefArrange'), ''), (N_('DataRefFlat'), ''), (N_('ToolsExpand'), 'Ctrl+Right'), (N_('ToolsCollapse'), 'Ctrl+Left'), (N_('ToolsFind'), 'Ctrl+F'), (N_('ToolsSpellCheck'), ''), (N_('ToolsRemXLST'), ''), (N_('ToolsGenOptions'), ''), (N_('ToolsFileOptions'), ''), (N_('ToolsTreeFont'), ''), (N_('ToolsOutputFont'), ''), (N_('ToolsEditFont'), ''), (N_('ToolsShortcuts'), ''), (N_('ToolsCustomToolbar'), ''), (N_('ToolsDefaultColor'), ''), (N_('ToolsBackColor'), ''), (N_('ToolsTextColor'), ''), (N_('WinNewWindow'), ''), (N_('WinCloseWindow'), ''), (N_('WinUpdateWindow'), 'Ctrl+W'), (N_('HelpContents'), 'F1'), (N_('HelpFullReadMe'), ''), (N_('HelpAbout'), ''), (N_('HelpPlugin'), ''), (N_('TextAddBoldTag'), 'Ctrl+Shift+B'), (N_('TextAddItalicsTag'), 'Ctrl+Shift+I'), (N_('TextAddUnderlineTag'), 'Ctrl+Shift+U'), (N_('TextAddSizeTag'), 'Ctrl+Shift+S'), (N_('TextAddColorTag'), 'Ctrl+Shift+T')] otherKeyBindList = [(N_('TreeFocusView'), 'Ctrl+G'), (N_('TreeSelectPrev'), 'Ctrl+K'), (N_('TreeSelectNext'), 'Ctrl+J'), (N_('TreePrevSibling'), 'Ctrl+Shift+K'), (N_('TreeNextSibling'), 'Ctrl+Shift+J'), (N_('TreeSelectParent'), 'Ctrl+U'), (N_('TreeOpenItem'), 'Ctrl+L'), (N_('TreeCloseItem'), 'Ctrl+H'), (N_('TreePageUp'), 'Ctrl+Shift+PgUp'), (N_('TreePageDown'), 'Ctrl+Shift+PgDown'), (N_('TreeIncremSearch'), 'Ctrl+/'), (N_('TreeIncremNext'), 'F3'), (N_('TreeIncremPrev'), 'Shift+F3'), (N_('RightChildPageUp'), 'Ctrl+PgUp'), (N_('RightChildPageDown'), 'Ctrl+PgDown'), (N_('RightParentPageUp'), 'Shift+PgUp'), (N_('RightParentPageDown'), 'Shift+PgDown')] cmdTranslationDict = dict([(cmd, _(cmd)) for cmd, key in menuKeyBindList + otherKeyBindList]) def defaultOutput(): """Return a list of defaults for the config file""" defaultList = ["# Options for TreeLine, an information storage program", "#", "# All options are set from within the program,", "# editing here is not recommended", "#", "##############################################################", "# Keyboard key bindings", "##############################################################", "#"] defaultList.extend(['%-20s %s' % key for key in menuKeyBindList]) defaultList.extend(['%-20s %s' % key for key in otherKeyBindList]) defaultList.extend([ "#", "##############################################################", "# Other options", "##############################################################", "#", "AutoFileOpen no", "StartShowChildren yes", "StartShowDescend no", "ShowStatusBar yes", "PersistTreeState yes", "SaveWindowGeom yes", "#", "ClickRename yes", "DragTree yes", "InsertOnEnter yes", "RenameNewNodes yes", "OpenSearchNodes yes", "ShowTreeIcons yes", "EnableExecLinks yes", "OpenNewWindow yes", "CompressNewFiles no", "EncryptNewFiles no", "HtmlNewFields no", "#", "IndentOffset 20", "MaxEditLines 7", "UndoLevels 8", "AutoSaveMinutes 0", "RecentFiles 4", "EditorPages 1", "SelectOrder tree", "#", "EditDateFormat mm/dd/yyyy", "EditTimeFormat H:MM", "#", "PrintWhat tree", "PrintRoot yes", "PrintLines no", "PrintOpenOnly no", "PrintKeepFirstChild yes", "PrintUseOutputFont yes", "PrintFont ", "PrintFontSize ", "PrintFontBold no", "PrintFontItalic no", "PrintFontUnderline no", "PrintFontStrikeOut no", "PrintPageSize Letter", "PrintLandscape no", "PrintNumCols 1", "PrintColSpace 0.30", "PrintIndentOffset 0.30", "HorizMargin 0.50", "VertMargin 0.70", "PrintUnits inch", "#", "UseDefaultColors yes", "BackgroundR 255", "BackgroundG 255", "BackgroundB 255", "ForegroundR 0", "ForegroundG 0", "ForegroundB 0", "#", "TreeFont ", "TreeFontSize ", "TreeFontBold no", "TreeFontItalic no", "TreeFontUnderline no", "TreeFontStrikeOut no", "OutputFont ", "OutputFontSize ", "OutputFontBold no", "OutputFontItalic no", "OutputFontUnderline no", "OutputFontStrikeOut no", "EditorFont ", "EditorFontSize ", "EditorFontBold no", "EditorFontItalic no", "EditorFontUnderline no", "EditorFontStrikeOut no", "#", "ToolbarQuantity 2", "ToolbarSize 16", "Toolbar0 FileNew,FileOpen,FileSave,FilePrintPreview,"\ "FilePrint,,EditCut,EditCopy,EditPaste,,ViewPreviousSelect,"\ "ViewNextSelect,,ViewShowChild,ViewShowDescend,,HelpContents", "Toolbar1 EditInsertAfter,EditAddChild,,EditDelete,"\ "EditIndent,EditUnindent,,EditMoveUp,EditMoveDown,,"\ "DataSetDescendType,DataConfigType,,DataSort", "ToolbarPosition ", "RecentFile1 ", "RecentFile2 ", "RecentFile3 ", "RecentFile4 ", "TreeState1 0:0:0:0", "TreeState2 0:0:0:0", "TreeState3 0:0:0:0", "TreeState4 0:0:0:0", "SpellCheckPath ", "ExtEditorPath ", "#", "WindowXSize 640", "WindowYSize 500", "WindowXPos -1000", "WindowYPos -1000", "TreeSplitPercent 40", "OutputSplitPercent 20", "EditorSplitPercent 25", "TitleSplitPercent 10", "ActiveRightView 0", "PrintPrevXSize 300", "PrintPrevYSize 560", "PrintPrevXPos 0", "PrintPrevYPos 0"]) return defaultList TreeLine/source/p3.py0000644000175000017500000001224711651514477013460 0ustar dougdoug# $Id: p3.py,v 1.2 2003/11/18 19:04:03 phr Exp phr $ # Simple p3 encryption "algorithm": it's just SHA used as a stream # cipher in output feedback mode. # Author: Paul Rubin, Fort GNOX Cryptography, . # Algorithmic advice from David Wagner, Richard Parker, Bryan # Olson, and Paul Crowley on sci.crypt is gratefully acknowledged. # Copyright 2002,2003 by Paul Rubin # Copying license: same as Python 2.3 license # Please include this revision number in any bug reports: $Revision: 1.2 $. from string import join from array import array from time import time import struct try: import hashlib shaHash = hashlib.sha1 except ImportError: import sha # stay compatible with Python 2.4 shaHash = sha.new class CryptError(Exception): pass def _hash(str): return shaHash(str).digest() _ivlen = 16 _maclen = 8 _state = _hash(`time()`) # added by Doug Bell for compatibility with 64-bit systems _arraytype = filter(lambda x: struct.calcsize(x) == 4, 'LIH')[0] try: import os _pid = `os.getpid()` except ImportError, AttributeError: _pid = '' def _expand_key(key, clen): blocks = (clen+19)/20 xkey=[] seed=key for i in xrange(blocks): seed=shaHash(key+seed).digest() xkey.append(seed) j = join(xkey,'') return array(_arraytype, j) def p3_encrypt(plain,key): global _state H = _hash # change _state BEFORE using it to compute nonce, in case there's # a thread switch between computing the nonce and folding it into # the state. This way if two threads compute a nonce from the # same data, they won't both get the same nonce. (There's still # a small danger of a duplicate nonce--see below). _state = 'X'+_state # Attempt to make nlist unique for each call, so we can get a # unique nonce. It might be good to include a process ID or # something, but I don't know if that's portable between OS's. # Since is based partly on both the key and plaintext, in the # worst case (encrypting the same plaintext with the same key in # two separate Python instances at the same time), you might get # identical ciphertexts for the identical plaintexts, which would # be a security failure in some applications. Be careful. nlist = [`time()`, _pid, _state, `len(plain)`,plain, key] nonce = H(join(nlist,','))[:_ivlen] _state = H('update2'+_state+nonce) k_enc, k_auth = H('enc'+key+nonce), H('auth'+key+nonce) n=len(plain) # cipher size not counting IV stream = array(_arraytype, plain+'0000'[n&3:]) # pad to fill 32-bit words xkey = _expand_key(k_enc, n+4) for i in xrange(len(stream)): stream[i] = stream[i] ^ xkey[i] ct = nonce + stream.tostring()[:n] auth = _hmac(ct, k_auth) return ct + auth[:_maclen] def p3_decrypt(cipher,key): H = _hash n=len(cipher)-_ivlen-_maclen # length of ciphertext if n < 0: raise CryptError, "invalid ciphertext" nonce,stream,auth = \ cipher[:_ivlen], cipher[_ivlen:-_maclen]+'0000'[n&3:],cipher[-_maclen:] k_enc, k_auth = H('enc'+key+nonce), H('auth'+key+nonce) vauth = _hmac (cipher[:-_maclen], k_auth)[:_maclen] if auth != vauth: raise CryptError, "invalid key or ciphertext" stream = array(_arraytype, stream) xkey = _expand_key (k_enc, n+4) for i in xrange (len(stream)): stream[i] = stream[i] ^ xkey[i] plain = stream.tostring()[:n] return plain # RFC 2104 HMAC message authentication code # This implementation is faster than Python 2.2's hmac.py, and also works in # old Python versions (at least as old as 1.5.2). from string import translate def _hmac_setup(): global _ipad, _opad, _itrans, _otrans _itrans = array('B',[0]*256) _otrans = array('B',[0]*256) for i in xrange(256): _itrans[i] = i ^ 0x36 _otrans[i] = i ^ 0x5c _itrans = _itrans.tostring() _otrans = _otrans.tostring() _ipad = '\x36'*64 _opad = '\x5c'*64 def _hmac(msg, key): if len(key)>64: key=shaHash(key).digest() ki = (translate(key,_itrans)+_ipad)[:64] # inner ko = (translate(key,_otrans)+_opad)[:64] # outer return shaHash(ko+shaHash(ki+msg).digest()).digest() # # benchmark and unit test # def _time_p3(n=1000,len=20): plain="a"*len t=time() for i in xrange(n): p3_encrypt(plain,"abcdefgh") dt=time()-t print "plain p3:", n,len,dt,"sec =",n*len/dt,"bytes/sec" def _speed(): _time_p3(len=5) _time_p3() _time_p3(len=200) _time_p3(len=2000,n=100) def _test(): e=p3_encrypt d=p3_decrypt plain="test plaintext" key = "test key" c1 = e(plain,key) c2 = e(plain,key) assert c1!=c2 assert d(c2,key)==plain assert d(c1,key)==plain c3 = c2[:20]+chr(1+ord(c2[20]))+c2[21:] # change one ciphertext character try: print d(c3,key) # should throw exception print "auth verification failure" except CryptError: pass try: print d(c2,'wrong key') # should throw exception print "test failure" except CryptError: pass _hmac_setup() # _test() # _speed() # uncomment to run speed test TreeLine/source/nodeformat.py0000644000175000017500000006360711651514477015302 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # nodeformat.py, provides non-GUI base classes for node formating info # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import re import copy import types import sys import os.path import stat import time from xml.sax.saxutils import escape if not sys.platform.startswith('win'): import pwd import fieldformat import conditional import gendate import gentime import treedoc import globalref class NodeFormat(object): """Stores node type field list and line formatting""" lineAttrRe = re.compile('line\d+$') fieldSplitRe = re.compile(r'({\*(?:\**|\?|!|&|#)[\w_\-.]+\*})', re.U) fieldPartRe = re.compile(r'{\*(\**|\?|!|&|#)([\w_\-.]+)\*}', re.U) levelFieldRe = re.compile(r'[^0-9]+([0-9]+)$') def __init__(self, name, formatAttrs={}, defaultField=''): self.name = name self.fieldList = [] self.lineList = [] self.numEmptyFields = 0 self.numFullFields = 0 self.uniqueIDFields = [] self.childType = formatAttrs.get(u'childtype', '') self.genericType = formatAttrs.get(u'generic', '') self.conditional = conditional.Conditional(formatAttrs. get(u'condition', '')) self.sibPrefix = formatAttrs.get(u'sibprefix', '') self.sibSuffix = formatAttrs.get(u'sibsuffix', '') self.iconName = formatAttrs.get(u'icon', '') self.refField = None for key in formatAttrs.keys(): if NodeFormat.lineAttrRe.match(key): self.insertLine(formatAttrs.get(key, ''), int(key[4:])) if defaultField: htmlAttrs = globalref.options.boolData('HtmlNewFields') and \ {'html': 'y'} or {} self.addNewField(defaultField, htmlAttrs) self.addLine(u'{*%s*}' % defaultField) self.addLine(u'{*%s*}' % defaultField) def duplicateSettings(self, otherFormat): """Set parameters to match other node format""" self.name = otherFormat.name self.fieldList = otherFormat.fieldList self.lineList = otherFormat.lineList self.childType = otherFormat.childType self.genericType = otherFormat.genericType self.conditional = otherFormat.conditional self.sibPrefix = otherFormat.sibPrefix self.sibSuffix = otherFormat.sibSuffix self.iconName = otherFormat.iconName self.refField = otherFormat.refField def __cmp__(self, other): """Comparison for equality and sorting""" return cmp(self.name, other.name) def fieldNames(self): """Return list of names of fields""" return [field.name for field in self.fieldList] def lineFields(self): """Return list of first fieldname in each line""" result = [] for line in self.lineList[1:]: strippedLine = [part for part in line if type(part) not in types.StringTypes] result.append(strippedLine and strippedLine[0].name or '') return result def findField(self, name): """Return field matching name or None""" try: return self.fieldList[self.fieldNames().index(name)] except ValueError: return None def addNewField(self, name, attrs={}): """Add new field with type, format, extra text in attrs dict""" typeName = '%sFormat' % attrs.get(u'type', 'Text') field = fieldformat.__dict__[typeName](name, attrs) self.fieldList.append(field) if attrs.get(u'ref', '').startswith('y'): self.refField = field elif not self.refField: self.refField = self.fieldList[0] sepName = field.sepName() # replace field name with the field self.lineList = [[part == sepName and field or part for part in line] for line in self.lineList] def addFieldIfNew(self, name, attrs={}): """Add new field if it doesn't exist, add to generic for a derived type""" if name not in self.fieldNames(): self.addNewField(name, attrs) if self.genericType: try: genericType = globalref.docRef.\ treeFormats[self.genericType] except KeyError: print 'Warning - generic type %s not found' % \ self.genericType.encode(globalref.localTextEncoding) self.genericType = '' return genericType.addNewField(name, attrs) def addLine(self, text): """Add format line to end of lineList, line 0 is the title""" newLine = self.parseLine(text) if newLine: self.lineList.append(newLine) def insertLine(self, text, pos): """Change line at pos of lineList, add blanks in earlier positions if req'd, line 0 is the title""" newLine = self.parseLine(text) if not newLine: newLine = [''] self.lineList.extend([''] * (pos - len(self.lineList) + 1)) self.lineList[pos] = newLine def changeTitleLine(self, text): """Change the title format line""" self.insertLine(text, 0) def changeOutputLines(self, lines): """Replace the output format lines with given list""" self.lineList = self.lineList[:1] if not self.lineList: self.lineList = [''] for line in lines: self.addLine(line) def updateLineFields(self): """Re-find fields to update for any changes in the fieldList""" self.lineList = [self.parseLine(line) for line in self.getLines()] def parseLine(self, text): """Parse text format line, return list of field types and text""" text = u' '.join(text.split()) lineList = filter(None, NodeFormat.fieldSplitRe.split(text)) newLine = [] for part in lineList: fieldMatch = NodeFormat.fieldPartRe.match(part) if fieldMatch: modifier = fieldMatch.group(1) fieldName = fieldMatch.group(2) if not modifier: field = self.findField(fieldName) newLine.append(field and field or part) elif modifier[0] == '*': newLine.append(fieldformat.ParentFormat(fieldName, len(modifier))) elif modifier == '?': newLine.append(fieldformat.AncestorFormat(fieldName)) elif modifier == '&': newLine.append(fieldformat.ChildFormat(fieldName)) elif modifier == '!': # file field field = globalref.docRef.fileInfoFormat.\ findField(fieldName) newLine.append(field and field or part) elif modifier == '#': match = NodeFormat.levelFieldRe.match(fieldName) if match: level = int(match.group(1)) if level > 0: newLine.append(fieldformat.CountFormat(fieldName, level)) else: newLine.append(part) else: newLine.append(part) else: newLine.append(part) return newLine def equalPrefix(self, other): """Return True if prefix and suffix are equivalent""" if self is other: return True return self.sibPrefix.strip().upper() == \ other.sibPrefix.strip().upper() and \ self.sibSuffix.strip().upper() == \ other.sibSuffix.strip().upper() def formatTitle(self, item): """Return string with formatted title data""" if not self.lineList: return '' line = u''.join([self.fieldText(text, item, True) for text in self.lineList[0]]) return line.strip().split('\n', 1)[0] # truncate to 1st line def formatText(self, item, skipEmpty=True, addPrefix=False, addSuffix=False, internal=False): """Return list of output strings from formatting data""" tagExp = re.compile('.*(|||)$') result = [] for lineData in self.lineList[1:]: self.numEmptyFields = 0 self.numFullFields = 0 line = u''.join([self.fieldText(text, item, False, internal) for text in lineData]) if self.numFullFields or not self.numEmptyFields or not skipEmpty: result.append(line.strip()) # add if tags are not empty else: tagMatch = tagExp.match(line) if tagMatch and result and globalref.docRef.formHtml: # html tag at removed line end result[-1] += tagMatch.group(1) # add to prev if addPrefix and self.sibPrefix: if result: result[0] = self.sibPrefix + result[0] else: result = [self.sibPrefix] if addSuffix and self.sibSuffix: if result: result[-1] += self.sibSuffix else: result = [self.sibSuffix] return result def formatAllTextLines(self, item): """Return a list of all text lines (title & output), add an empty string if a line has empty tags""" result = [] for lineData in self.lineList: self.numEmptyFields = 0 self.numFullFields = 0 line = u''.join([self.fieldText(text, item, False) for text in lineData]) if self.numFullFields or not self.numEmptyFields: result.append(line.strip()) # add if tags are not empty else: result.append('') return result def formatPlainTextLines(self, item): """Return a list of all text lines (title & output), using plain text (titleMode) for all""" result = [] for lineData in self.lineList: self.numEmptyFields = 0 self.numFullFields = 0 line = u''.join([self.fieldText(text, item, True, False, True) for text in lineData]) if not result or (line and \ (self.numFullFields or not self.numEmptyFields)): result.append(line.strip()) # add if tags are not empty return result def fieldText(self, field, item, titleMode=False, internal=False, stripFormatTags=False): """Return formatted text for field""" if type(field) in types.StringTypes: text = field if not titleMode and not globalref.docRef.formHtml: text = escape(text) if stripFormatTags and globalref.docRef.formHtml: text = fieldformat.TextFormat.stripTagRe.sub('', text) else: text = field.outputText(item, titleMode, internal) if text: self.numFullFields += 1 else: self.numEmptyFields += 1 return text def setTitle(self, title, item, addUndo=False): """Set data based on title string, add undo item if addUndo, return True if changed successfully""" fields = [] pattern = u'' extraText = u'' for seg in self.lineList[0]: if type(seg) in types.StringTypes: pattern += re.escape(seg) extraText += seg elif seg.parentLevel: text = self.fieldText(seg, item, True).split('\n', 1)[0] pattern += re.escape(text) extraText += text else: fields.append(seg) pattern += '(.*)' match = re.match(pattern, title) if not match and extraText.strip(): return False if addUndo: globalref.docRef.undoStore.addDataUndo(item, True) if match: for num, field in enumerate(fields): item.data[field.name] = match.group(num+1) else: # assign to 1st field if sep is only spaces item.data[fields[0].name] = title for field in fields[1:]: item.data[field.name] = '' if addUndo and globalref.pluginInterface: globalref.pluginInterface.execCallback(globalref.pluginInterface. dataChangeCallbacks, item, fields) return True def formatXml(self): """Return text list for use in xml file""" result = [] lines = self.getLines(True) for i, line in enumerate(lines): if line: result.append(u'line%d="%s"' % \ (i, escape(line, treedoc.escDict))) if self.childType: result.append(u'childtype="%s"' % self.childType) if self.genericType: result.append(u'generic="%s"' % self.genericType) if self.conditional: result.append(u'condition="%s"' % escape(self.conditional. conditionText(), treedoc.escDict)) if self.sibPrefix: result.append(u'sibprefix="%s"' % escape(self.sibPrefix, treedoc.escDict)) if self.sibSuffix: result.append(u'sibsuffix="%s"' % escape(self.sibSuffix, treedoc.escDict)) if self.iconName: result.append(u'icon="%s"' % self.iconName) return result def getLines(self, englishOnly=False): """Return text list of formatting lines""" lines = [u''.join([self.fieldName(part, englishOnly) for part in line]) for line in self.lineList] return lines and lines or [''] def fieldName(self, field, englishOnly=False): """Return field name with separators or line text part""" if type(field) in types.StringTypes: return field return field.sepName(englishOnly) def addTableFields(self, headingList): """Set fields based on import table headings""" headings = [self.correctFieldName(head) for head in headingList] self.addLine(u'{*%s*}' % headings[0]) for text in headings: self.addNewField(text) self.addLine(u'{*%s*}' % text) def xsltTemplate(self, indent, addAnchors=True): """Return list of lines for xslt template""" tagExp = re.compile('.*(|||)$') brExp = re.compile('<[bB][rR][ /]*>') brFormat = u'
' hrExp = re.compile('<[hH][rR][ /]*>') hrFormat = u'


' lineList = copy.deepcopy(self.lineList[1:]) output = [u'', u'' % self.name] if addAnchors: name = self.refField.name output.extend([u'' % name, u'' % name, u'', u'']) if self.sibPrefix: output.append(fieldformat.xslEscape(self.sibPrefix)) for lineDataRaw in lineList: lineData = [(type(field) not in types.StringTypes) and field or hrFormat.join(hrExp.split(brFormat. join(brExp.split(field)))) for field in lineDataRaw] fields = [field for field in lineData if type(field) not in types.StringTypes] if fields: text = u'' % \ u' or '.join([field.xslTestText() for field in fields]) output.append(text) tagMatch = tagExp.match(type(lineData[-1]) in types.StringTypes and lineData[-1] or '') if tagMatch: if len(tagMatch.group(1)) == len(lineData[-1]): del lineData[-1] else: lineData[-1] = lineData[-1][:-len(tagMatch.group(1))] line = u''.join([(type(text) in types.StringTypes) and fieldformat.xslEscape(text) or text.xslText() for text in lineData]) output.extend([line + brFormat, u'']) if tagMatch: output.append(tagMatch.group(1)) else: output.append(lineData[0] + brFormat) if self.sibSuffix: output.append(fieldformat.xslEscape(self.sibSuffix)) if globalref.docRef.spaceBetween: output.append(u'
') output.extend([u'
' % indent, u'', u'
', u'
']) return output def fixImportedFormat(self, defaultFieldName): """Add default field if there are no fields, and add title and output lines if missing""" if not self.lineList: if defaultFieldName in self.fieldNames(): self.addLine(u'{*%s*}' % defaultFieldName) else: self.addLine(self.name) for fieldName in self.fieldNames(): self.addLine(u'%s="{*%s*}"' % (fieldName, fieldName)) if not self.fieldList: self.addNewField(defaultFieldName) def correctFieldName(self, name): """Return name with only alphanumerics, underscores, dashes and periods allowed""" illegalRe = re.compile(r'[^\w_\-.]', re.U) name = illegalRe.sub('_', name.strip()) if not name: return u'X' if not name[0].isalpha() or name[:3].lower() == 'xml': name = u'X' + name return name def removeField(self, field): """Remove all occurances of field from lines return False if not found""" cnt = 0 for lineData in self.lineList: while field in lineData: lineData.remove(field) cnt += 1 self.lineList = filter(None, self.lineList) while len(self.lineList) < 2: self.lineList.append(['']) return cnt def findLinkField(self): """Return most likely field containing a bookmark URL""" availFields = [field for field in self.fieldList if \ field.typeName == u'URL'] if len(availFields) == 1: return availFields[0] if not availFields: return None for srchTerm in [globalref.docRef.linkFieldName, u'link', u'url', u'href']: for field in availFields: if field.name.lower() == srchTerm: return field for field in availFields: if field.name.lower().find(srchTerm) >= 0: return field return availFields[0] def findAutoChoiceFields(self): """Return a list of fields that need to have choices added""" return [field for field in self.fieldList if field.autoAddChoices] def findUniqueIDFields(self): """Set memeber variable and return list of unique ID fields""" self.uniqueIDFields = [field for field in self.fieldList if field.typeName == 'UniqueID'] return self.uniqueIDFields def setInitDefaultData(self, data, onlyIfBlank=False): """Add initial default data from fields into supplied dict""" for field in self.fieldList: text = field.getInitDefault() if text and (not onlyIfBlank or not data.get(field.name, '')): data[field.name] = text def updateFromGeneric(self): """Update the field names and types in self (a derived type) to match generic""" if not self.genericType: return try: genericType = globalref.docRef.treeFormats[self.genericType] except KeyError: print 'Warning - generic type %s not found' % \ self.genericType.encode(globalref.localTextEncoding) self.genericType = '' return newFields = [] for field in genericType.fieldList: matchingField = self.findField(field.name) if matchingField and field.typeName == matchingField.typeName: newFields.append(matchingField) else: newFields.append(copy.deepcopy(field)) self.fieldList = newFields if self.refField not in self.fieldList: self.refField = self.fieldList[0] self.updateLineFields() class FileInfoFormat(NodeFormat): """Stores file info type field list and header/footer formatting""" name = u'INT_TL_FILE_DATA_FORM' fileFieldName = N_('File_Name') pathFieldName = N_('File_Path') sizeFieldName = N_('File_Size') dateFieldName = N_('File_Mod_Date') timeFieldName = N_('File_Mod_Time') ownerFieldName = N_('File_Owner') pageNumFieldName = N_('Page_Number') numPagesFieldName = N_('Number_of_Pages') def __init__(self): NodeFormat.__init__(self, FileInfoFormat.name) self.addNewField(FileInfoFormat.fileFieldName) self.addNewField(FileInfoFormat.pathFieldName) self.addNewField(FileInfoFormat.sizeFieldName, {'type': 'Number'}) self.addNewField(FileInfoFormat.dateFieldName, {'type': 'Date'}) self.addNewField(FileInfoFormat.timeFieldName, {'type': 'Time'}) if not sys.platform.startswith('win'): self.addNewField(FileInfoFormat.ownerFieldName) # page info only for print header: self.addNewField(FileInfoFormat.pageNumFieldName) self.fieldList[-1].showInDialog = False self.addNewField(FileInfoFormat.numPagesFieldName) self.fieldList[-1].showInDialog = False for field in self.fieldList: field.useFileInfo = True def updateFileInfo(self): """Update data of file info item""" fileName = globalref.docRef.fileName item = globalref.docRef.fileInfoItem try: status = os.stat(fileName) except: item.data = {} return item.data[_(FileInfoFormat.fileFieldName)] = os.path.basename(fileName) item.data[_(FileInfoFormat.pathFieldName)] = os.path.dirname(fileName) item.data[_(FileInfoFormat.sizeFieldName)] = str(status[stat.ST_SIZE]) modTime = time.localtime(status[stat.ST_MTIME]) item.data[_(FileInfoFormat.dateFieldName)] = \ repr(gendate.GenDate(modTime)) item.data[_(FileInfoFormat.timeFieldName)] = \ repr(gentime.GenTime(modTime)) if not sys.platform.startswith('win'): try: item.data[_(FileInfoFormat.ownerFieldName)] = \ pwd.getpwuid(status[stat.ST_UID])[0] except KeyError: item.data[_(FileInfoFormat.ownerFieldName)] = \ repr(status[stat.ST_UID]) def getHeaderFooter(self, header=True): """Return formatted text for the header or the footer""" textLines = self.formatAllTextLines(globalref.docRef.fileInfoItem) if header: textLines = textLines[:3] else: textLines = textLines[3:6] textLines.extend([''] * (3 - len(textLines))) aligns = ['left', 'center', 'right'] result = [''] count = 0 for text, align in zip(textLines, aligns): if text: result.append('' % (align, text)) count += 1 result.append('
%s
') if count: return '\n'.join(result) return '' def replaceListFormat(self): """Copy settings from the treeFormats list to here and replace the list's file format object""" listFormat = globalref.docRef.treeFormats.get(self.name, None) if listFormat: for thisField, listField in zip(self.fieldList, listFormat.fieldList): thisField.duplicateSettings(listField) thisField.useFileInfo = True self.lineList = listFormat.lineList globalref.docRef.treeFormats[self.name] = self def translateFields(self): """Translate field names into the current language, called after reading the english version from the file""" for field in self.fieldList: transName = _(str(field.name)) if transName != field.name: field.enName = field.name field.name = transName class ChildCountFormat(NodeFormat): """Placeholder format for child count fields, should not show up in main format type list""" countFieldName = _('Level', 'child count field') def __init__(self): NodeFormat.__init__(self, u'CountFormat') for level in range(3): name = '%s%d' % (ChildCountFormat.countFieldName, level + 1) field = fieldformat.CountFormat(name, level + 1) self.fieldList.append(field) TreeLine/source/treeselection.py0000644000175000017500000002472311651514477016005 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treeselection.py, stores and controls node selections for TreeLine # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import globalref class TreeSelection(list): """A list for tree node selections""" maxHistoryLength = 100 def __init__(self, initList=[]): list.__init__(self, initList) self.currentItem = None # set by GUI when changed if self: self.currentItem = self[-1] self.searchOpenList = [] self.prevSelects = [] self.nextSelects = [] def __getslice__(self, i, j): """Modified to copy object with slice""" result = TreeSelection(list.__getslice__(self, i, j)) result.currentItem = self.currentItem result.searchOpenList = self.searchOpenList[:] result.prevSelects = self.prevSelects[:] result.nextSelects = self.nextSelects[:] return result def change(self, selectList): """Replace selection with items in list and update view""" self.addToHistory() self[:] = selectList globalref.updateViewSelection() def replace(self, selectList): """Replace selection with items in list""" self.addToHistory() self[:] = selectList def selectEmptyCurrent(self): """Add currentItem to an empty selection list""" if not self: self.append(self.currentItem) def addOrRemove(self, item, select=True): """Add or remove item from selection list""" self.addToHistory() if select: self.append(item) else: try: self.remove(item) except ValueError: pass def openSelection(self): """Open ancestors of all selected items""" for item in self: ancestor = item.parent while ancestor: ancestor.open = True ancestor = ancestor.parent def changeSearchOpen(self, selectList): """Replace selection with items in list, if open search enabled, close previously selected search items, open new selected search items""" if self == selectList: return openSearch = globalref.options.boolData('OpenSearchNodes') if openSearch: for item in self.searchOpenList: item.open = False globalref.updateViewItem(item) self.searchOpenList = [] for item in selectList: parents = item.openParents(openSearch) parents.reverse() # must start with a visible (loaded) node self.searchOpenList.extend(parents) for item in self.searchOpenList: globalref.updateViewItem(item) self.addToHistory() self[:] = selectList globalref.updateViewSelection() def addToHistory(self): """Add selection list to previous selection list""" if self: self.prevSelects.append(list(self)) if len(self.prevSelects) > TreeSelection.maxHistoryLength: del self.prevSelects[:5] self.nextSelects = [] def restorePrevSelect(self): """Go back to the most recent saved selection""" self.validateHistory() if self.prevSelects: for item in self.prevSelects[-1]: item.openParents(False) globalref.updateViewItem(item) self.nextSelects.append(list(self)) self[:] = self.prevSelects.pop(-1) globalref.updateViewSelection() else: globalref.updateViewMenuStat() def restoreNextSelect(self): """Go forward to the most recent saved selection""" self.validateHistory() if self.nextSelects: for item in self.nextSelects[-1]: item.openParents(False) globalref.updateViewItem(item) self.prevSelects.append(list(self)) self[:] = self.nextSelects.pop(-1) globalref.updateViewSelection() else: globalref.updateViewMenuStat() def validateHistory(self): """Clear previous and next lists if no longer valid""" if self.prevSelects: for item in self.prevSelects[-1]: if not item.isValid(): self.prevSelects = [] break if self.nextSelects: for item in self.nextSelects[-1]: if not item.isValid(): self.nextSelects = [] break def formatNames(self): """Return a list of format type names used in the selection""" currentTypes = [] for item in self: if item.formatName not in currentTypes: currentTypes.append(item.formatName) return currentTypes def uniqueBranches(self): """Return a list of unique branch top items""" result = [] for item in self: parent = item.parent while parent and parent not in self: parent = parent.parent if not parent: result.append(item) return result def treeSelectPrev(self): """Select previous item""" item = self.currentItem.prevItem() if item: self.change([item]) def treeSelectNext(self): """Select next item""" item = self.currentItem.nextItem() if item: self.change([item]) def treeOpenItem(self): """Set selection to open""" if not self: self.change([self.currentItem]) for item in self: item.open = True globalref.updateViewItem(item) def treeCloseItem(self): """Set selection to closed""" if not self: self.replace([self.currentItem]) newSelects = [] for item in self: if item.open and item.childList: if item not in newSelects: newSelects.append(item) elif item.parent and item.parent not in newSelects: newSelects.append(item.parent) for item in newSelects: item.open = False globalref.updateViewItem(item) self.change([item for item in newSelects if item.allAncestorsOpen()]) def treePrevSibling(self): """Select previous sibling item""" item = self.currentItem.prevSibling() if item: self.changeSearchOpen([item]) def treeNextSibling(self): """Select next sibling item""" item = self.currentItem.nextSibling() if item: self.changeSearchOpen([item]) def treeSelectParent(self): """Select parent item""" item = self.currentItem.parent if item: self.changeSearchOpen([item]) def treeTop(self): """Select root item""" item = globalref.docRef.root self.change([item]) def treeBottom(self): """Select last open item in tree""" item = globalref.docRef.root.lastDescendant(False) self.change([item]) def findText(self, wordList, forward=True): """Select item containing words in searchStr in any field, starts with currentItem, return item if found""" fullList = globalref.docRef.root.descendantList(True) currentPos = fullList.index(self.currentItem) fullList = fullList[currentPos+1:] + fullList[:currentPos] if not forward: fullList.reverse() for item in fullList: if item.matchWords(wordList): self.changeSearchOpen([item]) return item return None def findTitleText(self, searchStr): """Select item containing searchStr in the title, starts with currentItem (included in search), return True if found""" searchStr = searchStr.lower() next = self.currentItem while True: if next.title().lower().find(searchStr) >= 0: self.changeSearchOpen([next]) return True next = next.nextItem(True) if not next: next = globalref.docRef.root if next is self.currentItem: return False def findNextTitle(self, searchStr, forward=True): """Select item containing searchStr in the title, starts with currentItem (_not_ included in search), return True if found""" searchStr = searchStr.lower() next = self.currentItem while True: if forward: next = next.nextItem(True) if not next: next = globalref.docRef.root else: next = next.prevItem(True) if not next: next = globalref.docRef.root.lastDescendant(True) if next is self.currentItem: return False if next.title().lower().find(searchStr) >= 0: self.changeSearchOpen([next]) return True def findRefField(self, searchStr): """Select item containing searchStr in a line of the refField, starts with currentItem, return True if found""" next = self.currentItem while True: next = next.nextItem(True) if not next: next = globalref.docRef.root if next is self.currentItem: return False if next.matchRefText(searchStr): self.changeSearchOpen([next]) return True def letterSearch(self, char, forward=True): """Move to the next or previous visible item starting with letter""" fullList = globalref.docRef.root.descendantList(False) currentPos = fullList.index(self.currentItem) fullList = fullList[currentPos+1:] + fullList[:currentPos] matches = [item for item in fullList if item.title()[0].upper() == char] if matches: if forward: self.changeSearchOpen([matches[0]]) else: self.changeSearchOpen([matches[-1]]) TreeLine/source/cmdline.py0000644000175000017500000002507311651514477014552 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # cmdline.py, provides a class to read and execute command line arguments # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import sys import os.path from PyQt4 import QtGui import treedoc import globalref usage = [_('Usage:'), '', ' treeline [%s] [%s]' % (_('qt-options'), _('infile')), '', _('-or- (non-interactive):'), '', ' treeline [%s] [%s] [%s] %s [%s ...]' % (_('import-option'), _('export-option'), _('misc-options'), _('infile'), _('infile2')), '', _('Qt-options:'), '', ' %s' % _('see Qt documentation for valid Qt options'), '', _('Import-options:'), '', ' --import-tabbed %s' % _('import a tab indented text file'), ' --import-table %s' % _('import a tab-delimitted text table, one node per line'), ' --import-lines %s' % _('import plain text, one node per line'), ' --import-para %s' % _('import plain text, one node per paragraph'), ' --import-treepad %s' % _('import a Treepad text-node file'), ' --import-xbel %s' % _('import an XML bookmark file in XBEL format'), ' --import-mozilla %s' % _('import an html bookmark file in Mozilla format'), ' --import-xml %s' % _('import a generic XML file (non-TreeLine format)'), ' --import-odf %s' % _('import an ODF text document'), '', _('Export-options:'), '', ' --export-html %s' % _('export a single HTML file'), ' --export-dir %s' % _('export HTML in directories'), ' --export-xslt %s' % _('export an XSLT file'), ' --export-tabbed %s' % _('export a tab indented text file'), ' --export-table %s' % _('export a text table of the first children only'), ' --export-xbel %s' % _('export an XML bookmark file in XBEL format'), ' --export-mozilla %s' % _('export an html bookmark file in Mozilla format'), ' --export-xml %s' % _('export a generic XML file (non-TreeLine format)'), ' --export-odf %s' % _('export an ODF text document'), '', _('Misc-options:'), '', ' -o, --outfile=%-5s %s' % (_('FILE'), _('the output filename, not used with multiple infiles')), ' -n, --no-root %s' % _('exclude the root node form the output if applicable'), ' --add-header %s' % _('add a header and footer to HTML exports'), ' --indent=%-10s %s' % (_('NUM'), _('change the indent amount for HTML exports (default = 20)')), ' -q, --quiet %s' % _('supress normal status information, only give errors'), ' -h, --help %s' % _('display this information and exit'), '', _("No more than one import-option and one export-option may be\n"\ "specified. If either are not present, the native TreeLine\n"\ "file format is assumed."), '', _("The output filename option can only be specified if there is\n"\ "only one input file. If it is not specified, the input's base\n"\ "file name is used with the appropriate output file extension.\n"\ "If the extensions are the same, an underscore is added before\n"\ "the extension. Note that this avoids overwriting the input\n"\ "file, but other files may be overwritten without notification\n"\ "if they share the output file's name.")] class CmdLine(object): """Parses and executes command line arguments for file translations""" options = 'o:nqh' longOptions = ['import-tabbed', 'import-table', 'import-lines', 'import-para', 'import-treepad', 'import-xbel', 'import-mozilla', 'import-xml', 'import-odf', 'export-html', 'export-dir', 'export-xslt', 'export-tabbed', 'export-table', 'export-xbel', 'export-mozilla', 'export-xml', 'export-odf', 'outfile=', 'no-root', 'indent=', 'quiet', 'help'] def __init__(self, opts, args): self.doc = None self.outfile = '' self.includeRoot = True self.addHeader = False self.indent = 20 self.quiet = False importType = '' exportType = '' for opt, arg in opts: if opt in ('-h', '--help'): printUsage() return elif opt.startswith('--import-'): if importType: printUsage() sys.exit(2) importType = opt[9:] elif opt.startswith('--export-'): if exportType: printUsage() sys.exit(2) exportType = opt[9:] elif opt in ('-o', '--outfile'): self.outfile = arg if len(args) > 1: printUsage() sys.exit(2) elif opt in ('-n', '--no-root'): self.includeRoot = False elif opt == '--add-header': self.addHeader = True elif opt == '--indent': try: self.indent = int(arg) except ValueError: printUsage() sys.exit(2) elif opt in ('-q', '--quiet'): self.quiet = True if not args: printUsage() sys.exit(2) if not importType: importType = 'trl' if not exportType: exportType = 'trl' errorFlag = False pathEncoding = globalref.localTextEncoding for fileName in args: self.doc = treedoc.TreeDoc() try: self.importFile(fileName, importType) except (IOError, UnicodeError): print _('Error - could not read file %s', '%s is filename') % \ fileName.encode(pathEncoding) errorFlag = True continue except treedoc.ReadFileError, e: print _('Error in %(filename)s - %(details)s') % \ {'filename': fileName.encode(pathEncoding), 'details': e} errorFlag = True continue try: newFileName = self.exportFile(fileName, exportType) if not self.quiet: print _('File "%(infile)s" (%(intype)s type) was exported'\ ' to "%(outfile)s" (%(outtype)s type)') \ % {'infile': fileName.encode(pathEncoding), 'intype': importType, 'outfile': newFileName.encode(pathEncoding), 'outtype': exportType} except IOError: errorFlag = True if errorFlag: sys.exit(1) def importFile(self, fileName, importType): """Import file using importType""" if importType == 'trl': importType = 'File' getattr(self.doc, 'read%s' % importType.title())(fileName) def exportFile(self, oldFileName, exportType): """Export file using exportType, return new filename on success""" if exportType == 'trl': fileName = self.newFileName(oldFileName, '.trl') self.doc.writeFile(fileName) elif exportType == 'html': fileName = self.newFileName(oldFileName, '.html') self.doc.exportHtml(fileName, self.doc.root, self.includeRoot, False, self.indent, self.addHeader) elif exportType == 'dir': dirName = os.path.split(oldFileName)[0] if not dirName: dirName = '.' fileName = dirName self.doc.exportDir(dirName, [self.doc.root], self.addHeader) elif exportType == 'xslt': fileName = self.newFileName(oldFileName, '.xsl') self.doc.exportXslt(fileName, self.includeRoot, self.indent) elif exportType == 'tabbed': fileName = self.newFileName(oldFileName, '.txt') self.doc.exportTabbedTitles(fileName, [self.doc.root], True, self.includeRoot) elif exportType == 'table': fileName = self.newFileName(oldFileName, '.tbl') self.doc.exportTable(fileName, [self.doc.root]) elif exportType == 'xbel': fileName = self.newFileName(oldFileName, '.xml') self.doc.exportXbel(fileName, [self.doc.root]) elif exportType == 'mozilla': fileName = self.newFileName(oldFileName, '.html') self.doc.exportHtmlBookmarks(fileName, [self.doc.root]) elif exportType == 'xml': fileName = self.newFileName(oldFileName, '.xml') self.doc.exportGenericXml(fileName, [self.doc.root]) elif exportType == 'odf': fileName = self.newFileName(oldFileName, '.odt') fontInfo = QtGui.QFontInfo(QtGui.QApplication.font()) self.doc.exportOdf(fileName, [self.doc.root], fontInfo.family(), fontInfo.pointSize(), fontInfo.fixedPitch(), True, self.includeRoot) return fileName def newFileName(self, oldFileName, newExt): """Return a new filename with a new extension, add an underscore to base name if the names are the same""" if self.outfile: return self.outfile baseName, oldExt = os.path.splitext(oldFileName) if oldExt == newExt: baseName += '_' return baseName + newExt def printUsage(): """Print usage text""" print u'\n'.join(usage).encode(globalref.localTextEncoding) TreeLine/source/treeitem.py0000644000175000017500000013542411651514477014757 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treeitem.py, provides non-GUI base classes for tree items # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that i has with empty titles.t will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import os import sys import codecs import copy import re from xml.sax.saxutils import escape import treedoc import nodeformat import numbering import output import globalref try: from __main__ import __version__ except ImportError: __version__ = '' _defaultTitle = _('New') class TreeItem(object): """Data storage item for tree structure""" blankTitle = _('[BLANK TITLE]') maxLevel = 0 # for ODF export dirExportLinkRe = re.compile(r'
') dirExportDirRe = re.compile(r'[/\\"*?|<>:]') def __init__(self, parent, formatName, initText='', addDefaultData=False): self.parent = parent self.formatName = formatName self.data = {} self.childList = [] if initText: self.setTitle(initText) if addDefaultData: self.nodeFormat().setInitDefaultData(self.data) self.open = False self.viewData = None self.level = 0 # updated only by numbered descend list def setInitDefaultData(self): """Set initial default data to fields if any""" self.nodeFormat().setInitDefaultData(self.data) def nodeFormat(self): """Return this node's format object""" return globalref.docRef.treeFormats[self.formatName] def title(self): """Return title text""" text = self.nodeFormat().formatTitle(self) if not text: text = TreeItem.blankTitle return text def setTitle(self, title, addUndo=False): """Set title text, return True if changed successfully""" title = u' '.join(title.split()) if title: if self.nodeFormat().setTitle(title, self, addUndo): globalref.docRef.modified = True return True return False def changeType(self, newFormatName): """Change nodeFormat to new, update title fields if they'd be blank""" origTitle = self.nodeFormat().formatTitle(self) self.formatName = newFormatName format = self.nodeFormat() format.setInitDefaultData(self.data, True) if not format.formatTitle(self): format.setTitle(origTitle, self, False) def formatText(self, skipEmpty=True, addPrefix=False, addSuffix=False, internal=False): """Return list of formatted text lines""" return self.nodeFormat().formatText(self, skipEmpty, addPrefix, addSuffix, internal) def formatChildText(self, skipEmpty=True, internal=False): """Return list of all children's formatted text lines""" return self.formatTextItems(self.childList, skipEmpty, internal) def formatTextItems(self, itemList, skipEmpty=True, internal=False): """Return list of formatted text lines for items in list""" if not itemList: return [] result = [] sep = [] prefixAdded = False for item, next in map(None, itemList, itemList[1:]): addPrefix = not prefixAdded prefixAdded = True addSuffix = False if not next or not item.nodeFormat().\ equalPrefix(next.nodeFormat()): addSuffix = True prefixAdded = False result.extend(sep + item.formatText(skipEmpty, addPrefix, addSuffix, internal)) sep = globalref.docRef.spaceBetween and [u''] or [] return result def duplicateNode(self): """Return a copy of self with its children and uniqueIDs adjusted""" newNode = copy.deepcopy(self) newNode.setDescendantUniqueID(True) return newNode def refFieldText(self): """Return text from ref field""" return self.data.get(self.nodeFormat().refField.name, '') def outputItemList(self, includeRoot=True, openOnly=False, addAnchors=False, level=0): """Return list of OutputItems for all descendants""" outList = output.OutputGroup() if includeRoot: outList.append(self.outputItem(addAnchors, level)) else: level -= 1 if self.open or not openOnly: for child in self.childList: outList.extend(child.outputItemList(True, openOnly, addAnchors, level + 1)) return outList def outputItem(self, addAnchors=False, level=0): """Return OutputItem for self""" lines = self.formatText() if not lines: lines = [''] if addAnchors: for anchor in filter(None, self.refFieldText().split('\n')): lines[0] = u'%s' % (anchor, lines[0]) outItem = output.OutputItem(lines, level) outItem.prefix = self.nodeFormat().sibPrefix outItem.suffix = self.nodeFormat().sibSuffix return outItem def branchXml(self, typeList=None, writeOptions=False): """Return list of xml lines, include format info if not in typeList""" nodeFormat = self.nodeFormat() if typeList == None: # default writes no format info typeList = globalref.docRef.treeFormats.values() xmlList = [u'<%s item="y"' % escape(nodeFormat.name, treedoc.escDict)] if writeOptions: if not globalref.docRef.spaceBetween: xmlList[0] += u' nospace="y"' if not globalref.docRef.lineBreaks: xmlList[0] += u' nobreaks="y"' if not globalref.docRef.formHtml: xmlList[0] += u' nohtml="y"' if globalref.docRef.childFieldSep != \ treedoc.TreeDoc.childFieldSepDflt: xmlList[0] += u' childsep="%s"' % \ escape(globalref.docRef.childFieldSep, treedoc.escDict) if globalref.docRef.spellChkLang: xmlList[0] += u' spellchk="%s"' % \ escape(globalref.docRef.spellChkLang, treedoc.escDict) if globalref.docRef.xslCssLink: xmlList[0] += u' xslcss="%s"' % globalref.docRef.xslCssLink if __version__: xmlList[0] += u' tlversion="%s"' % \ escape(__version__, treedoc.escDict) addFormat = nodeFormat not in typeList # writes format on 1st only if addFormat: xmlList.extend(nodeFormat.formatXml()) typeList.append(nodeFormat) xmlList[-1] += u'>' for field in nodeFormat.fieldList: text = self.data.get(field.name, '') if text or addFormat: escKey = escape(field.englishName(), treedoc.escDict) fieldFormat = '' if addFormat: fieldFormat = field.writeXml() if field == nodeFormat.refField: fieldFormat += u' ref="y"' xmlList.append(u'<%s%s>%s' % (escKey, fieldFormat, escape(text, treedoc.escDict), escKey)) for child in self.childList: xmlList.extend(child.branchXml(typeList)) if writeOptions: # write format info for any unused formats for format in globalref.docRef.treeFormats.values(): if format not in typeList: name = escape(format.name, treedoc.escDict) xmlList.append(u'<%s item="n"' % name) xmlList.extend(format.formatXml()) xmlList[-1] += u'>' for field in format.fieldList: escKey = escape(field.englishName(), treedoc.escDict) fieldFormat = field.writeXml() if field == format.refField: fieldFormat += u' ref="y"' xmlList.append(u'<%s%s>' % (escKey, fieldFormat, escKey)) xmlList.append(u'' % name) xmlList.append(u'' % escape(nodeFormat.name, treedoc.escDict)) return xmlList def exportToText(self, level=0, openOnly=False): """Write tabbed list of descendants titles""" textList = [u'\t' * level + self.title()] if self.open or not openOnly: for child in self.childList: textList.extend(child.exportToText(level + 1, openOnly)) return textList def exportDirTable(self, linkDict, parentTitle=None, header='', footer=''): """Write dir structure with html tables""" if not self.childList: return try: dirName = self.exportDirName(True) if not os.access(dirName, os.R_OK): os.mkdir(dirName, 0755) os.chdir(dirName) except (OSError, ValueError, UnicodeError): print 'Error - cannot create directory', dirName raise IOError(_('Error - cannot create directory %s') % dirName) title = self.title() lines = [u'', u'', u'', u'', u'%s' % title, u'', u''] if header: lines.append(header) lines.append(u'

%s

' % title) if parentTitle: label = _('Parent: ') lines.append(u'

%s' '%s

' % (label, parentTitle)) lines.extend([u'', u'']) ### headings kludge???? headings = self.childList[0].nodeFormat().lineFields() lines.extend([u'' % cell for cell in headings]) lines.append(u'') for child in self.childList: textList = [] for line in child.formatText(False): for match in TreeItem.dirExportLinkRe.finditer(line): anchor = match.group(1) absPath = linkDict.get(anchor, '') if absPath: curPath = unicode(dirName, sys.getfilesystemencoding()) relPath = treedoc.relativePath(curPath, absPath) relPath = os.path.join(relPath, 'index.html') if os.sep != '/': relPath = relPath.replace(os.sep, '/') link = '' % (relPath, anchor) line = TreeItem.dirExportLinkRe.sub(link, line) textList.append(line) childDir = child.exportDirName(False) if child.childList: textList[0] = u'%s' % \ (childDir, textList[0]) for anchor in filter(None, child.refFieldText().split('\n')): textList[0] = u'%s' % (anchor, textList[0]) lines.extend([u'' % cell for cell in textList]) lines.append(u'') lines.extend([u'', u'
%s
%s
']) if footer: lines.append(footer) lines.extend([u'', u'']) try: f = codecs.open('index.html', 'w', 'utf-8') f.writelines([line + '\n' for line in lines]) except IOError: print 'Error - could not write file to', dirName raise IOError(_('Error - cannot write file to %s') % dirName) f.close() for child in self.childList: child.exportDirTable(linkDict, title, header, footer) os.chdir('..') def createDirTableLinkDict(self, linkDict, path): """Create dict to store parent directories for internal links""" for anchor in filter(None, self.refFieldText().split('\n')): linkDict[anchor] = path path = os.path.join(path, self.exportDirName(False)) for child in self.childList: child.createDirTableLinkDict(linkDict, path) def exportDirPage(self, linkDict, level=0): """Write directory structure with navigation bar and full pages""" title = self.title() lines = [u'', u'', u'', u'', u'' % ('../' * level), u'%s' % title, u'', u'', u'') textList = [] for line in self.formatText(True, True, True): for match in TreeItem.dirExportLinkRe.finditer(line): anchor = match.group(1) absPath = linkDict.get(anchor, '') if absPath: curPath = unicode(os.getcwd(), sys.getfilesystemencoding()) relPath = treedoc.relativePath(curPath, absPath) if os.sep != '/': relPath = relPath.replace(os.sep, '/') link = '' % relPath line = TreeItem.dirExportLinkRe.sub(link, line) textList.append(line) sep = globalref.docRef.lineBreaks and u'
\n' or u'\n' lines.append(sep.join(textList)) lines.extend([u'', u'']) dirName = self.exportDirName(True) fileName = '%s.html' % dirName try: f = codecs.open(fileName, 'w', 'utf-8') f.writelines([line + '\n' for line in lines]) except (IOError, UnicodeError): print 'Error - could not write file to %s', fileName raise IOError(_('Error - cannot write file to %s') % fileName) f.close() if self.childList: try: if not os.access(dirName, os.R_OK): os.mkdir(dirName, 0755) os.chdir(dirName) except (OSError, ValueError, UnicodeError): print 'Error - cannot create directory', dirName raise IOError(_('Error - cannot create directory %s') % dirName) for child in self.childList: child.exportDirPage(linkDict, level + 1) os.chdir('..') def createDirPageLinkDict(self, linkDict, path): """Create dict to store parent directories for internal links""" dirName = self.exportDirName(False) for anchor in filter(None, self.refFieldText().split('\n')): linkDict[anchor] = os.path.join(path, '%s.html' % dirName) path = os.path.join(path, dirName) for child in self.childList: child.createDirPageLinkDict(linkDict, path) def exportDirName(self, encode=False): """Return legal directory name for exporting to directories""" try: dirName = filter(None, self.refFieldText().split('\n'))[0] except IndexError: dirName = '' dirName = dirName.encode(sys.getfilesystemencoding(), 'replace') if not encode: dirName = unicode(dirName, sys.getfilesystemencoding()) dirName = TreeItem.dirExportDirRe.sub('', dirName) if not dirName: dirName = '___' return dirName def exportXbelBookmarks(self, level=0, addBranch=True): """Return text list with descendant bookmarks in XBEL format""" indentsPerLevel = 3 indent = ' ' * (indentsPerLevel * level) nextIndent = ' ' * (indentsPerLevel * (level + 1)) title = escape(self.title(), treedoc.escDict) if not self.childList and level > 0: nodeFormat = self.nodeFormat() field = nodeFormat.findLinkField() if field: link = escape(self.data.get(field.name, ''), treedoc.escDict) if link: return [u'%s' % (indent, link), u'%s%s' % (nextIndent, title), u'%s' % indent] elif not nodeFormat.fieldList or \ (len(nodeFormat.fieldList) == 1 and not self.data.get(nodeFormat.fieldList[0].name, '')): return [u'%s' % indent] result = [] if level > 0: result = [u'%s' % indent] result.append(u'%s%s' % (nextIndent, title)) if addBranch: for child in self.childList: result.extend(child.exportXbelBookmarks(level + 1)) if level > 0: result.append(u'%s' % indent) return result def exportHtmlBookmarks(self, level=0, addBranch=True): """Return text list with descendant bookmarks in Mozilla format""" indentsPerLevel = 4 indent = ' ' * (indentsPerLevel * level) title = escape(self.title()) if not self.childList and level > 0: nodeFormat = self.nodeFormat() field = nodeFormat.findLinkField() if field: link = self.data.get(field.name, '') if link: return [u'%s
%s' % (indent, link, title)] elif not nodeFormat.fieldList or \ (len(nodeFormat.fieldList) == 1 and not self.data.get(nodeFormat.fieldList[0].name, '')): return [u'%s
' % indent] result = [] if level > 0: result = [u'%s

%s

' % (indent, title)] if addBranch and self.childList: result.append(u'%s

' % indent) for child in self.childList: result.extend(child.exportHtmlBookmarks(level + 1)) result.append(u'%s

' % indent) return result def exportGenericXml(self, textFieldName, level=0, addBranch=True): """Return text list with descendant nodes in generic XML format""" indentsPerLevel = 3 indent = ' ' * (indentsPerLevel * level) nodeFormat = self.nodeFormat() result = u'%s<%s' % (indent, self.formatName) for fieldName in nodeFormat.fieldNames(): text = self.data.get(fieldName, '') if text and fieldName != textFieldName: result = u'%s %s="%s"' % (result, fieldName, escape(text, treedoc.escDict)) result += u'>' if textFieldName in nodeFormat.fieldNames(): text = self.data.get(textFieldName, '') if text: result += escape(text, treedoc.escDict) if not addBranch or not self.childList: return [u'%s' % (result, self.formatName)] result = [result] for child in self.childList: result.extend(child.exportGenericXml(textFieldName, level+1)) result.append(u'%s' % (indent, self.formatName)) return result def exportOdf(self, level=0, addBranch=True, includeRoot=True, openOnly=False): """Return text list with descendant nodes in an ODF format text file""" result = [] if includeRoot: TreeItem.maxLevel = max(level, TreeItem.maxLevel) lines = self.nodeFormat().formatPlainTextLines(self) title = lines[0] if not title: title = TreeItem.blankTitle output = lines[1:] result = [u'%s' % (level, level, escape(title, treedoc.escDict))] if output and title == output[0]: del output[0] # remove first line if same as title if output: for line in '\n'.join(output).split('\n'): result.append(u'%s'\ '' % (level, escape(line, treedoc.escDict))) else: level -= 1 if addBranch and self.childList and (self.open or not openOnly): for child in self.childList: result.extend(child.exportOdf(level + 1, addBranch, True, openOnly)) return result def loadTabbedChildren(self, bufList, level=0): """Recursive read of TreeItems from tabbed buffer""" while bufList: if bufList[0][0] == level + 1: buf = bufList.pop(0) child = TreeItem(self, self.formatName, buf[1]) self.childList.append(child) if not child.loadTabbedChildren(bufList, level + 1): return False elif 0 < bufList[0][0] <= level: return True else: return False return True def editChildList(self, textList): """Update child names and order from textList, update undos and view""" if len(textList) == len(self.childList): # assume rename if length is same for child, text in zip(self.childList, textList): if child.title() != text: child.setTitle(text, True) globalref.updateViewItem(child) else: # find new child positions if length differs oldLen = len(self.childList) nodeFormat = self.nodeFormat() newType = globalref.docRef.treeFormats.get(nodeFormat.childType, None) if not newType: newType = oldLen and self.childList[0].nodeFormat() or \ nodeFormat globalref.docRef.undoStore.addChildListUndo(self) newChildList = [] for text in textList: try: newChildList.append(self.childList.pop(\ [child.title() for child in self.childList].index(text))) except ValueError: newItem = TreeItem(self, newType.name) newItem.setInitDefaultData() newItem.setTitle(text) newItem.setUniqueID(True) newChildList.append(newItem) if oldLen == 0 and newChildList: self.open = True for child in self.childList: child.parent = None self.childList = newChildList globalref.updateLeftView() globalref.docRef.modified = True def descendantList(self, inclClosed=False, level=0): """Recursive list of TreeItems, default to open only, sets level num, returns list""" descendList = [self] self.level = level if self.open or inclClosed: for child in self.childList: descendList.extend(child.descendantList(inclClosed, level+1)) return descendList def descendantGen(self): """Return generator to step thru all descendants (including closed), include self""" yield self for child in self.childList: for item in child.descendantGen(): yield item def descendantGenNoRoot(self): """Return generator to step thru all descendants (including closed), do not include self""" for child in self.childList: yield child for item in child.descendantGenNoRoot(): yield item def ancestorList(self): """Return list all parents and grandparents of self""" item = self.parent result = [] while item: result.append(item) item = item.parent return result def allAncestorsOpen(self): """Returns True if all ancestors are set open""" closeList = [item for item in self.ancestorList() if not item.open] if closeList: return False return True def hasDescendant(self, child): """Return True if self has descendant child""" for item in self.descendantGen(): if item is child: return True return False def lastDescendant(self, inclClosed=False): """Return self's last descendant, required to be open if not inclClosed""" item = self while True: if item.childList and (item.open or inclClosed): item = item.childList[-1] else: return item def usesType(self, formatName): """Return True if dataType is used by self or descendants""" for item in self.descendantGen(): if item.formatName == formatName: return True return False def isValid(self): """Return True if self has root as an ancestor""" item = self while item.parent: item = item.parent return item == globalref.docRef.root def numChildren(self): """Return number of children""" return len(self.childList) def childPos(self, child): """Return the number of the referenced child or -1""" for num, item in enumerate(self.childList): if item is child: return num return -1 def childText(self): """Return list of child item strings (not recursive)""" return [child.title() for child in self.childList] def maxDescendLevel(self, thisLevel=0): """Return max number of levels below this node""" if not self.childList: return thisLevel return max([child.maxDescendLevel(thisLevel + 1) for child in self.childList]) def descendLevelList(self, level=1): """Return a list of descendants at the given level""" newList = [self] for i in range(level): oldList = newList newList = [] for item in oldList: newList.extend(item.childList) return newList def prevSibling(self): """Return nearest older sibling or None""" if self.parent: i = self.parent.childPos(self) if i > 0: return self.parent.childList[i-1] return None def nextSibling(self): """Return next younger sibling or None""" if self.parent: i = self.parent.childPos(self) + 1 if i < len(self.parent.childList): return self.parent.childList[i] return None def prevItem(self, inclClosed=False): """Return previous sibling or parent or None""" sib = self.prevSibling() if sib: while sib.numChildren() and (sib.open or inclClosed): sib = sib.childList[-1] return sib return self.parent def nextItem(self, inclClosed=False): """Return first child, next sibling or ancestors next sibling or None""" if self.childList and (self.open or inclClosed): return self.childList[0] ancestor = self while ancestor: sib = ancestor.nextSibling() if sib: return sib ancestor = ancestor.parent return None def delete(self): """Remove self from the tree structure - return parent on success""" parent = self.parent if not parent: return None parent.childList.remove(self) self.parent = None globalref.docRef.modified = True return parent def addChild(self, text=_defaultTitle, pos=-1): """Add new child before position, -1 is at end - return new item""" if pos < 0: pos = len(self.childList) newFormat = self.nodeFormat().childType if newFormat not in globalref.docRef.treeFormats: newFormat = self.childList and self.childList[0].formatName or \ self.formatName newItem = TreeItem(self, newFormat, text, True) newItem.setUniqueID(True) self.childList.insert(pos, newItem) globalref.docRef.modified = True return newItem def insertSibling(self, text=_defaultTitle, inAfter=False): """Add new sibling before or after self - return new item on success""" if not self.parent: return None pos = self.parent.childPos(self) if inAfter: pos += 1 newFormat = self.parent.nodeFormat().childType if newFormat not in globalref.docRef.treeFormats: newFormat = self.formatName newItem = TreeItem(self.parent, newFormat, text, True) newItem.setUniqueID(True) self.parent.childList.insert(pos, newItem) globalref.docRef.modified = True return newItem def addTree(self, rootItem, pos=-1): """Add new tree as a child before position, -1 is at end return item""" if pos < 0: pos = len(self.childList) self.childList.insert(pos, rootItem) rootItem.parent = self globalref.docRef.modified = True return rootItem def insertTree(self, rootItem, inAfter=False): """Add new tree before or after self - return item on success""" if not self.parent: return None pos = self.parent.childPos(self) if inAfter: pos += 1 self.parent.childList.insert(pos, rootItem) rootItem.parent = self.parent globalref.docRef.modified = True return rootItem def indent(self): """Becomes a child of the previous sibling - return self on success""" newParent = self.prevSibling() if not newParent: return None self.delete() newParent.addTree(self, -1) globalref.docRef.modified = True return self def unindent(self): """Becomes its parents next sibling - return self on success""" sibling = self.parent if not sibling or not sibling.parent: return None self.delete() sibling.insertTree(self, True) globalref.docRef.modified = True return self def move(self, amount=-1): """Switch self with sibling, -1=up, 1=down, return self on success""" if self.parent: i = self.parent.childPos(self) j = i + amount if 0 <= j < len(self.parent.childList): self.parent.childList[i], self.parent.childList[j] = \ self.parent.childList[j], self globalref.docRef.modified = True return self return None def moveFirst(self): """Move self to be first child of parent""" if self.parent: self.parent.childList.remove(self) self.parent.childList.insert(0, self) globalref.docRef.modified = True def moveLast(self): """Move self to be last child of parent""" if self.parent: self.parent.childList.remove(self) self.parent.childList.append(self) globalref.docRef.modified = True def openBranch(self, setOpen=True): """Recursive open/close of all descendants, close if setOpen=False""" self.open = setOpen for child in self.childList: child.openBranch(setOpen) def openParents(self, openSelf=True): """Open self's ancestors and self if openSelf, return list of changed items""" openList = [] item = openSelf and self or self.parent while item: if not item.open and item.childList: item.open = True openList.append(item) item = item.parent return openList def cmpItems(self, item1, item2): """Compare function for sorting, not case sensitive""" if not globalref.docRef.sortFields[0][0]: factor = globalref.docRef.sortFields[0][1] and 1 or -1 return factor * cmp(item1.title().lower(), item2.title().lower()) for field, direction in globalref.docRef.sortFields: field1 = item1.nodeFormat().findField(field) field2 = item2.nodeFormat().findField(field) factor = direction and 1 or -1 if field1.sortSequence == field2.sortSequence: result = cmp(field1.sortValue(item1.data), field2.sortValue(item2.data)) if result != 0: return factor * result else: return factor * cmp(field1.sortSequence, field2.sortSequence) return 0 def sortChildren(self): """Sort item children, not case sensitive""" self.childList.sort(self.cmpItems) def sortBranch(self): """Sort item descendants, not case sensitive""" self.childList.sort(self.cmpItems) for child in self.childList: child.sortBranch() def sortTypeChildren(self, formatNames): """Sort item children of the given formats""" childOfTypeList = [child for child in self.childList if child.formatName in formatNames] if childOfTypeList: childOfTypeList.sort(self.cmpItems) if len(childOfTypeList) < len(self.childList): childOfTypeList.extend([child for child in self.childList if child.formatName not in formatNames]) self.childList = childOfTypeList def sortTypeBranch(self, formatNames): """Sort item descendants of a given format type""" self.sortTypeChildren(formatNames) for child in self.childList: child.sortTypeBranch(formatNames) def matchWords(self, wordList): """Return True if all words are in data fields, not case sensitive""" dataStr = u' '.join(self.data.values()).lower() for word in wordList: if dataStr.find(word) == -1: return False return True def matchRefText(self, searchStr): """Return True if searchStr matches a line in ref field data""" lines = self.data.get(self.nodeFormat().refField.name, '').split('\n') if searchStr in lines: return True return False def cmpFields(self, fieldNames, item): """Return True if listed fields are the same in item and self""" for field in fieldNames: if self.data.get(field, '') != item.data.get(field, ''): return False return True def findEquivFields(self, fieldNames, itemList): """Return first item from list with same listed fields or None""" for item in itemList: if self.cmpFields(fieldNames, item): return item return None def editFields(self, valueDict): """Set values for fields based on dictionary""" for fieldName, value in valueDict.iteritems(): field = self.nodeFormat().findField(fieldName) if field: newValue, ok = field.storedText(value) if ok: value = newValue self.data[fieldName] = value globalref.docRef.modified = True def childTypes(self): """Return list of all type names found in children""" types = [] for item in self.childList: if item.formatName not in types: types.append(item.formatName) return types def descendTypes(self): """Return list of all type names found in descendants""" types = [] for item in self.descendantGenNoRoot(): if item.formatName not in types: types.append(item.formatName) return types def branchFields(self): """Return names of all fields found in self and descendents""" types = [] for item in self.descendantGen(): if item.formatName not in types: types.append(item.formatName) fieldNames = [] for type in types: for field in globalref.docRef.treeFormats[type].fieldNames(): if field not in fieldNames: fieldNames.append(field) return fieldNames def setConditionalType(self): """Set self to type based on auto condtional settings""" itemFormat = self.nodeFormat() genericName = itemFormat.genericType if not genericName: genericName = self.formatName formatList = globalref.docRef.treeFormats.derivedDict.\ get(genericName, [])[:] if not formatList: return formatList.remove(itemFormat) formatList.insert(0, itemFormat) # reorder to give priority neutralResult = None for format in formatList: if format.conditional: if format.conditional.evaluate(self.data): self.formatName = format.name return elif not neutralResult: neutralResult = format.name if neutralResult: self.formatName = neutralResult def setDescendantCondTypes(self): """Set children recursively to type based on condtional settings""" self.setConditionalType() for child in self.childList: child.setDescendantCondTypes() def setUniqueID(self, replaceExist=False): """Add a unique ID to UID fields if empty""" for field in self.nodeFormat().uniqueIDFields: if not self.data.get(field.name, '') or replaceExist: self.data[field.name] = field.nextValue() def setDescendantUniqueID(self, replaceExist=False): """Add a unique ID to descendant UID fields if empty""" self.setUniqueID(replaceExist) for child in self.childList: child.setDescendantUniqueID(replaceExist) def filterDescendants(self, typeName, expr): """Remove children of given type recursively if expr is false""" for child in self.childList[:]: if child.formatName != typeName or expr(child.data): child.filterDescendants(typeName, expr) else: self.childList.remove(child) child.parent = None globalref.docRef.modified = True def addChildCat(self, catList): """Add child's category items as a new child level to expand data""" catSuffix = _('TYPE', 'child category suffix') newType = u'%s_%s' % (catList[0], catSuffix) num = 1 while newType in globalref.docRef.treeFormats and \ globalref.docRef.treeFormats[newType].fieldNames() != catList: newType = u'%s_%s_%d' % (catList[0], catSuffix, num) num += 1 newFormat = nodeformat.NodeFormat(newType, {}, catList[0]) globalref.docRef.treeFormats[newType] = newFormat for field in catList[1:]: newFormat.addNewField(field) newItems = [] for child in self.childList: newParent = child.findEquivFields(catList, newItems) if not newParent: newParent = TreeItem(self, newType) newParent.setUniqueID(True) for field in catList: newParent.data[field] = child.data.get(field, '') newItems.append(newParent) newParent.childList.append(child) child.parent = newParent self.childList = newItems globalref.docRef.modified = True def flatChildCat(self): """Collapse data by merging fields""" origTreeFormats = copy.deepcopy(globalref.docRef.treeFormats) self.childList = [item for item in self.descendantGen() if not item.childList] for item in self.childList: origFields = origTreeFormats[item.formatName].fieldNames() addedFields = [] oldParent = item.parent while oldParent != self: for field in origTreeFormats[oldParent.formatName].\ fieldNames(): newField = field num = 1 while newField in origFields or newField in addedFields: newField = u'%s_%d' % (field, num) num += 1 item.data[newField] = oldParent.data.get(field, '') addedFields.append(newField) item.nodeFormat().addFieldIfNew(newField) oldParent = oldParent.parent item.parent = self globalref.docRef.modified = True def arrangeByRef(self, refField): """Arrange data using parent references""" descendList = self.descendantList(True)[1:] for item in descendList: item.childList = [] self.childList = [] for item in descendList: refText = item.data.get(refField, '') parentList = [parent for parent in descendList if parent.refFieldText() == refText] if len(parentList) > 1: # pick nearest parent above the item itemPos = descendList.index(item) while len(parentList) > 1 and \ descendList.index(parentList[1]) < itemPos: del parentList[0] if not parentList or parentList[0] == item: item.parent = self else: item.parent = parentList[0] item.parent.childList.append(item) globalref.docRef.modified = True def flatByRef(self, refField): """Collapse data after adding references to parents""" descendList = self.descendantList(True)[1:] self.childList = descendList for item in descendList: item.childList = [] item.data[refField] = item.parent.refFieldText() item.parent = self item.nodeFormat().addFieldIfNew(refField) globalref.docRef.modified = True def updateByRef(self, refRoot): """Update with new fields from reference file with matched ref field Return a tuple describing changes""" refData = {} for item in refRoot.descendantGen(): refData[item.refFieldText()] = item formatChgs = {} numNewEntries = 0 for item in self.descendantGen(): itemFormat = item.nodeFormat() fields = itemFormat.fieldNames() try: ref = refData[item.data[itemFormat.refField.name]] for field in ref.data.keys(): if field not in fields: item.data[field] = ref.data[field] numNewEntries += 1 formatChgs.setdefault(itemFormat.name, {}).\ setdefault(field, ref.nodeFormat().findField(field)) except KeyError: pass numChgTypes = 0 numNewFields = 0 for typeName in formatChgs.keys(): type = globalref.docRef.treeFormats[typeName] ref = formatChgs[typeName] numChgTypes += 1 for field in ref.values(): type.fieldList.append(field) numNewFields += 1 if numNewEntries: globalref.docRef.modified = True return (numNewEntries, numNewFields, numChgTypes) def addNumbering(self, field, format, rootIncluded, appendToParent, addField=True, singleLevel=False, startNum=1, currentLevel=0): """Add number field to this node and descendants""" if rootIncluded: if addField or self.nodeFormat().findField(field): self.data[field] = numbering.numSeries(startNum, startNum + 1, format[currentLevel])[0] self.nodeFormat().addFieldIfNew(field) globalref.docRef.modified = True currentLevel += 1 startNum = 1 if not self.childList: return globalref.docRef.modified = True numList = numbering.numSeries(startNum, len(self.childList) + startNum, format[currentLevel]) if appendToParent and currentLevel: numList = [self.data.get(field, '') + numText for numText in numList] for item in self.childList: if addField or item.nodeFormat().findField(field): item.data[field] = numList.pop(0) item.nodeFormat().addFieldIfNew(field) if not singleLevel: for item in self.childList: item.addNumbering(field, format, False, appendToParent, addField, False, 1, currentLevel + 1) TreeLine/source/gendate.py0000644000175000017500000003152611651514477014546 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # gendate.py, provides a class for date formating # # Copyright (C) 2008, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re import time import types class GenDate(object): """Stores & formats date values""" monthNames = [_('January'), _('February'), _('March'), _('April'), _('May'), _('June'), _('July'), _('August'), _('September'), _('October'), _('November'), _('December')] monthAbbrevs = [N_('Jan'), N_('Feb'), N_('Mar'), N_('Apr'), N_('May', 'abbrev'), N_('Jun'), N_('Jul'), N_('Aug'), N_('Sep'), N_('Oct'), N_('Nov'), N_('Dec')] monthChooser = dict([(mon.lower(), i+1) for i, mon in \ enumerate(monthAbbrevs)]) monthAbbrevs = [_(name) for name in monthAbbrevs] monthAbbrevLengths = list(set([len(name) for name in monthAbbrevs] + [3])) monthAbbrevLengths.sort() monthAbbrevLengths.reverse() monthChooser.update(dict([(mon.lower(), i+1) for i, mon in \ enumerate(monthAbbrevs)])) dayOfWeekNames = [_('Sunday'), _('Monday'), _('Tuesday'), _('Wednesday'), _('Thursday'), _('Friday'), _('Saturday')] dayOfWeekAbbrevs = [_('Sun'), _('Mon'), _('Tue'), _('Wed'), _('Thu'), _('Fri'), _('Sat')] formatChooser = {'yyyy':'year', 'yy':'year2digit', 'mmmm':'monthName', 'mmm':'monthAbbrev', 'mm':'month', 'm':'month', 'dd':'day', 'd':'day', 'wwww':'dayOfWeekName', 'www':'dayOfWeekAbbrev', 'w':'dayOfWeek'} daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] daysTo1900 = 693597 # days from Jan 1, 0001 to Jan 1, 1900 baseline maxDays = 2958463 minDays = -4345731 yearWrap = 35 # assumed wrap point for 2-digit year input timeType = type(time.localtime()) def __init__(self, genDate=None): """Accepts one of the following to initialize: 1. None - sets to current day 2. int or string in yyyymmdd format 3. string in y/m/d sequence only with various separators (.-/,; or spaces) & formats of fields 4. tuple in (y, m, d) - or localtime/gmtime 5. GenDate instance""" if genDate == None: self.setToToday() else: self.setDate(genDate) def setToToday(self): """Set date to current values""" self.setDate(time.localtime()) def setDate(self, genDate): """Sets the date value from int/str (yyyymmdd), string with various separators (.-/,; or spaces) in y/m/d order only, tuple (y, m, d), or GenDate class""" inType = type(genDate) if inType is types.IntType: self.date = genDate elif inType in (types.StringType, types.UnicodeType): try: self.date = int(genDate) except ValueError: # general separated string year, month, day = _parseDateStr(genDate) self.setFromCompStr(year, month, day) elif inType in (types.TupleType, types.ListType, GenDate.timeType): self.date = int("%04d%02d%02d" % genDate[:3]) elif inType is type(self): self.date = genDate.date else: raise GenDateError('Invalid date type') self._validate() def setFromStr(self, dateStr, format='y/m/d'): """Sets the date value from string with seperators (.-/,; or spaces). format sets the order only (type of sep. & fields don't matter), 2-digit year okay (till 2035) Returns self""" data = {} try: for pair in map(None, re.findall('y+|m+|d+', format), _parseDateStr(dateStr)): data[pair[0][0]] = pair[1] self.setFromCompStr(data['y'], data['m'], data['d']) except (KeyError, TypeError): raise GenDateError('Invalid date string or format') return self def setFromCompStr(self, yearStr, monthStr, dayStr): """Sets the date value from individual strings""" try: year = int(yearStr) if 0 <= year < 100 and len(yearStr) == 2: if year < GenDate.yearWrap: year += 2000 else: year += 1900 if monthStr.isdigit(): month = int(monthStr) else: month = self._monthFromName(monthStr) self.setDate((year, month, int(dayStr))) except (ValueError, KeyError, AttributeError): raise GenDateError('Invalid date string or format') def _monthFromName(self, monthStr): """Return month number from string abbreviation, raise ValueError if not found""" monthStr = monthStr.lower() for abbrevLength in GenDate.monthAbbrevLengths: month = GenDate.monthChooser.get(monthStr[:abbrevLength], 0) if month: return month raise ValueError def dateStr(self, format='yyyy/mm/dd'): """Return string based on the format, which includes series of 'y', 'm', 'd', 'w', repeated to indicate length. Backslashes will escape these letters""" return ''.join([self._substitute(part) for part in \ re.split(r'((? 9999 or year == 0: raise GenDateError('Invalid year value') if month < 1 or month > 12: raise GenDateError('Invalid month value') if day < 1 or day > GenDate.daysInMonth[month - 1]: if month != 2 or day != 29 or not isLeapYear(year): raise GenDateError('Invalid day value') if year == 1582 and month == 10 and 4 < day < 15: raise GenDateError('Invalid day value, 10 days dropped in 1582') def baselineDays(self): """Days since baseline date of Jan. 1, 1900""" year, month, day = self.toTuple() days = firstDayOfYear(year) for m in range(month - 1): days += GenDate.daysInMonth[m] if isLeapYear(year) and month > 2: days += 1 if year == 1582 and (month > 10 or (month == 10 and day > 4)): days -= 10 days += day - 1 return days def setFromBaseline(self, days): """Set date from days since baseline of Jan. 1, 1900""" if not (GenDate.minDays <= days <= GenDate.maxDays): raise GenDateError('Invalid day value, limits exceeded') year = int(round(days / 365.2425)) + 1900 if year < 1: year -= 1 # adjust for BC & no year 0 remainDays = days - firstDayOfYear(year) + 1 while remainDays <= 0: year -= 1 if year == 0: year = -1 remainDays = days - firstDayOfYear(year) + 1 while remainDays > 366 or (remainDays == 366 and not isLeapYear(year)): year += 1 remainDays = days - firstDayOfYear(year) + 1 daysInMonth = GenDate.daysInMonth[:] if isLeapYear(year): daysInMonth[1] = 29 month = 1 while remainDays > daysInMonth[month - 1]: remainDays -= daysInMonth[month - 1] month += 1 if year == 1582 and (month > 10 or (month == 10 and remainDays > 4)): remainDays += 10 if remainDays > daysInMonth[month - 1]: remainDays -= daysInMonth[month - 1] month += 1 self.setDate((year, month, remainDays)) def clone(self): """Return cloned instance""" return self.__class__(self.date) def __repr__(self): """Outputs in format [-]yyyy/mm/dd""" if self.date < 0: return "%05d/%02d/%02d" % self.toTuple() return "%04d/%02d/%02d" % self.toTuple() def __int__(self): """Return integer repr""" return self.date def __cmp__(self, other): """Compare operator""" if other is None or not isinstance(other, GenDate): return 1 return cmp(self.date, other.date) def __hash__(self): """Allow use as dictionary key""" return hash(self.date) def __add__(self, days): """Addition operator""" if not type(days) is types.IntType: raise GenDateError('Add operator requires an integer') copy = self.clone() copy.setFromBaseline(self.baselineDays() + days) return copy def __radd__(self, days): """Addition operator""" return self.__add__(days) def __sub__(self, other): """Subtraction operator for date or int days""" if type(other) is types.IntType: return self.__add__(-other) return self.baselineDays() - other.baselineDays() def __rsub__(self, other): """Subtraction operator for date""" if not isinstance(other, GenDate): raise GenDateError('Cannot subtract a date from an integer') return other.baselineDays() - self.baselineDays() class GenDateError(Exception): """Exception class for invalid date data""" pass #### Utility Functions #### def firstDayOfYear(year): """Return the baseline day (based on Jan. 1, 1900) for Jan. 1""" if year < 0: # BC calculation days = year * 365 - abs(year)/4 - GenDate.daysTo1900 else: # AD calculation lastYear = year - 1 days = lastYear * 365 + lastYear / 4 - GenDate.daysTo1900 if year > 1582: # adjust for new rules and 10 days lost in 1582 days -= abs(lastYear - 1600)/100 - abs(lastYear - 1600)/400 + 10 return days def isLeapYear(year): """Returns 1 if given year is a leap year""" if year % 4 != 0: return 0 if year < 1600 or year % 100 != 0: return 1 if year % 400 != 0: return 0 return 1 def _parseDateStr(dateStr): """Split string into three date parts, split by any of .-/,; or spaces. Return tuple""" try: return re.match(r'(.+?)[\s,\.;/-]+(.+?)[\s,\.;/-]+(.+)', dateStr).groups() except AttributeError: raise GenDateError('Invalid date string') if __name__ == '__main__': gd = GenDate() print 'Today is', gd.dateStr('wwww, mmmm dd, yyyy') gd.setFromIntStr('12/23/65', 'mm/dd/yy') print gd.dateStr('wwww, mmmm dd, yyyy') TreeLine/source/treemainwin.py0000644000175000017500000037457611656211623015470 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treemainwin.py, provides a class for the main window # # TreeLine, an information storage program # Copyright (C) 2009, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import sys import os.path import base64 from PyQt4 import QtCore, QtGui try: from __main__ import __version__, __author__, helpFilePath except ImportError: __version__ = __author__ = '??' helpFilePath = None import treedoc import treeview import treeflatview import treerightviews import treeeditviews import configdialog import treedialogs import printdata import globalref import optiondefaults import optiondlg import output import helpview import spellcheck import plugininterface class TreeMainWin(QtGui.QMainWindow): """Main window, menus, toolbar, and status""" toolIcons = None configDlg = None setTypeDlg = None sortDlg = None findDlg = None helpView = None tlPlainFileFilter = u'%s (*.trl *.xml)' % _('TreeLine Files - Plain') tlCompFileFilter = u'%s (*.trl *.trl.gz)' % \ _('TreeLine Files - Compressed') tlEncryptFileFilter = u'%s (*.trl)' % _('TreeLine Files - Encrypted') tlGenFileFilter = u'%s (*.trl *.xml *.trl.gz)' % _('TreeLine Files') allFileFilter = u'%s (*)' % _('All Files') textFileFilter = u'%s (*.txt)' % _('Text Files') treepadFileFilter = u'%s (*.hjt)' % _('Treepad Files') xbelFileFilter = u'%s (*.xml)' % _('XBEL Bookmarks') mozFileFilter = u'%s (*.html *.htm)' % _('Mozilla Bookmarks') htmlFileFilter = u'%s (*.html *.htm)' % _('Html Files') xsltFileFilter = u'%s (*.xsl *.xslt)' % _('XSLT Files') tableFileFilter = u'%s (*.tbl *.txt)' % _('Table Files') xmlFileFilter = u'%s (*.xml)' % _('XML Files') odfFileFilter = u'%s (*.odt)' % _('ODF Text Files') tagMenuEntries = [('TextAddBoldTag', _('&Bold'), ('', '')), ('TextAddItalicsTag', _('&Italics'), ('', '')), ('TextAddUnderlineTag', _('&Underline'), ('', '')), ('TextAddSizeTag', _('&Size...'), ('', '')), ('TextAddColorTag', _('&Color...'), ('', ''))] tagDict = dict([(text, tag) for name, text, tag in tagMenuEntries]) defaultWinSize = (640, 500) winCascade = 24 def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, parent) self.setAcceptDrops(True) self.setStatusBar(QtGui.QStatusBar()) self.showStatusBar = globalref.options.boolData('ShowStatusBar') self.viewStatusBar(self.showStatusBar) globalref.mainWin = self if globalref.options.boolData('SaveWindowGeom'): if globalref.treeControl.windowCount(): rect = globalref.treeControl.windowList[-1].geometry() rect.adjust(TreeMainWin.winCascade, TreeMainWin.winCascade, TreeMainWin.winCascade, TreeMainWin.winCascade) else: rect = QtCore.QRect(globalref.options.intData('WindowXPos', -1000, 10000), globalref.options.intData('WindowYPos', -1000, 10000), globalref.options.intData('WindowXSize', 10, 10000), globalref.options.intData('WindowYSize', 10, 10000)) if rect.x() != -1000 or rect.y() != -1000: desktop = QtGui.QApplication.desktop() if desktop.isVirtualDesktop(): availRect = desktop.screen().rect() else: availRect = desktop.availableGeometry(desktop. primaryScreen()) winRect = rect.intersected(availRect) self.setGeometry(winRect) else: self.resize(rect.size()) else: self.resize(*TreeMainWin.defaultWinSize) self.origPalette = QtGui.QApplication.palette() self.updateColors() self.autoSaveTimer = QtCore.QTimer(self) self.connect(self.autoSaveTimer, QtCore.SIGNAL('timeout()'), globalref.treeControl.autoSave) split = QtGui.QSplitter() self.setCentralWidget(split) self.leftTabs = QtGui.QTabWidget() self.leftTabs.tabBar().setFocusPolicy(QtCore.Qt.NoFocus) split.addWidget(self.leftTabs) self.leftTabs.setTabPosition(QtGui.QTabWidget.South) self.treeView = treeview.TreeView() self.leftTabs.addTab(self.treeView, _('Tree View')) self.flatView = treeflatview.FlatView() self.leftTabs.addTab(self.flatView, _('Flat View')) self.rightTabs = QtGui.QTabWidget() self.rightTabs.tabBar().setFocusPolicy(QtCore.Qt.NoFocus) split.addWidget(self.rightTabs) self.rightTabs.setTabPosition(QtGui.QTabWidget.South) self.dataOutSplit = QtGui.QSplitter(QtCore.Qt.Vertical) self.rightTabs.addTab(self.dataOutSplit, _('Data Output')) parentOutView = treerightviews.DataOutView(False) self.dataOutSplit.addWidget(parentOutView) childOutView = treerightviews.DataOutView(True) self.dataOutSplit.addWidget(childOutView) self.dataEditSplit = QtGui.QSplitter(QtCore.Qt.Vertical) self.rightTabs.addTab(self.dataEditSplit, _('Data Editor')) parentEditView = treeeditviews.DataEditView(False) self.dataEditSplit.addWidget(parentEditView) childEditView = treeeditviews.DataEditView(True) self.dataEditSplit.addWidget(childEditView) self.titleListSplit = QtGui.QSplitter(QtCore.Qt.Vertical) self.rightTabs.addTab(self.titleListSplit, _('Title List')) parentTitleView = treerightviews.TitleListView(False) self.titleListSplit.addWidget(parentTitleView) childTitleView = treerightviews.TitleListView(True) self.titleListSplit.addWidget(childTitleView) self.showItemChildren = globalref.options.boolData('StartShowChildren') childOutView.showDescendants = globalref.options.\ boolData('StartShowDescend') treeFont = self.getFontFromOptions('Tree') if treeFont: self.treeView.setFont(treeFont) self.flatView.setFont(treeFont) outFont = self.getFontFromOptions('Output') if outFont: parentOutView.setFont(outFont) childOutView.setFont(outFont) editFont = self.getFontFromOptions('Editor') if editFont: parentEditView.setFont(editFont) childEditView.setFont(editFont) parentTitleView.setFont(editFont) childTitleView.setFont(editFont) if globalref.options.boolData('SaveWindowGeom'): mainSplitPercent = globalref.options.intData('TreeSplitPercent', 0, 100) treeWidth = int(split.width() / 100.0 * mainSplitPercent) split.setSizes([treeWidth, split.width() - treeWidth]) outSplitPercent = globalref.options.intData('OutputSplitPercent', 0, 100) outHeight = int(self.dataOutSplit.height() / 100.0 * outSplitPercent) self.dataOutSplit.setSizes([outHeight, self.dataOutSplit.height() - outHeight]) editSplitPercent = globalref.options.intData('EditorSplitPercent', 0, 100) editHeight = int(self.dataEditSplit.height() / 100.0 * editSplitPercent) self.dataEditSplit.setSizes([editHeight, self.dataEditSplit.height() - editHeight]) titleSplitPercent = globalref.options.intData('TitleSplitPercent', 0, 100) titleHeight = int(self.titleListSplit.height() / 100.0 * titleSplitPercent) self.titleListSplit.setSizes([titleHeight, self.titleListSplit.height() - titleHeight]) else: childTitleView.oldViewHeight = 80 self.doc = None self.pluginInterface = None self.condFilter = None self.textFilter = [] self.fileImported = False self.duplicateSelect = None self.storedOpenNodes = [] self.linkTagEditor = None self.printData = printdata.PrintData() self.actions = {} self.shortcuts = {} self.toolbars = [] self.recentFileSep = None self.winMenu = None self.setupMenus() self.recentFileActions = [] globalref.treeControl.recentFiles.updateMenu() self.setupShortcuts() self.addActionIcons() self.setupToolbars() self.restoreToolbarPos() self.filterStatus = QtGui.QLabel() self.doc = treedoc.TreeDoc() self.show() # show window outline early and fix data edit view resizing self.updateForFileChange(False) self.connect(self.leftTabs, QtCore.SIGNAL('currentChanged(int)'), self.updateLeftView) self.connect(self.rightTabs, QtCore.SIGNAL('currentChanged(int)'), self.updateRightView) self.connect(QtGui.QApplication.clipboard(), QtCore.SIGNAL('dataChanged()'), self.setPasteAvail) self.setPasteAvail() if globalref.options.boolData('SaveWindowGeom'): viewNum = globalref.options.intData('ActiveRightView', 0, 2) self.rightTabs.setCurrentIndex(viewNum) self.setupPlugins() self.updateAddTagAvail() def getFontFromOptions(self, optionPrefix): """Return font if set in options or None""" fontName = globalref.options.strData('%sFont' % optionPrefix, True) if fontName: try: fontSize = int(globalref.options.strData('%sFontSize' % optionPrefix, True)) except ValueError: fontSize = 10 font = QtGui.QFont(fontName, fontSize) font.setBold(globalref.options.boolData('%sFontBold' % optionPrefix)) font.setItalic(globalref.options.boolData('%sFontItalic' % optionPrefix)) font.setUnderline(globalref.options.boolData('%sFontUnderline' % optionPrefix)) font.setStrikeOut(globalref.options.boolData('%sFontStrikeOut' % optionPrefix)) return font return None def saveFontToOptions(self, font, optionPrefix): """Store font in option settings""" globalref.options.changeData('%sFont' % optionPrefix, unicode(font.family()), True) globalref.options.changeData('%sFontSize' % optionPrefix, unicode(font.pointSize()), True) globalref.options.changeData('%sFontBold' % optionPrefix, font.bold() and 'yes' or 'no', True) globalref.options.changeData('%sFontItalic' % optionPrefix, font.italic() and 'yes' or 'no', True) globalref.options.changeData('%sFontUnderline' % optionPrefix, font.underline() and 'yes' or 'no', True) globalref.options.changeData('%sFontStrikeOut' % optionPrefix, font.strikeOut() and 'yes' or 'no', True) globalref.options.writeChanges() def updateViews(self): """Update left and right views""" QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) self.updateLeftView() QtGui.QApplication.restoreOverrideCursor() self.updateRightView() def updateLeftView(self, switchFlat=False): """Update the active left view, switchFlat is true if switching to the flat view""" QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) current = self.leftTabs.currentWidget() filters = [] if current == self.treeView: self.doc.selection.openSelection() self.actions['ViewTree'].setChecked(True) self.treeView.updateTree() else: self.actions['ViewFlat'].setChecked(True) self.flatView.updateTree(switchFlat) if self.condFilter: filters.append(_('Conditional Filter')) if self.textFilter: filters.append(_('Text Filter')) if filters: self.filterStatus.setText((u' %s ' % _('and')).join(filters)) self.statusBar().addWidget(self.filterStatus) self.filterStatus.show() self.statusBar().show() elif self.filterStatus.isVisible(): self.statusBar().removeWidget(self.filterStatus) if not self.showStatusBar: self.statusBar().hide() QtGui.QApplication.restoreOverrideCursor() def updateViewSelection(self): """Change the selection in the active left view""" self.leftTabs.currentWidget().updateSelect() self.updateRightView() def updateViewItem(self, item): """Update item display in the active left view""" self.leftTabs.currentWidget().updateTreeItem(item) def updateRightView(self): """Update given right-hand view or the active one""" QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) splitter = self.rightTabs.currentWidget() if splitter == self.dataOutSplit: self.actions['ViewDataOutput'].setChecked(True) elif splitter == self.dataEditSplit: self.actions['ViewDataEdit'].setChecked(True) else: self.actions['ViewTitleList'].setChecked(True) if splitter.width(): # not collapsed childView = splitter.widget(1) topView = splitter.widget(0) if len(self.doc.selection) != 1 or not self.showItemChildren or \ (not self.doc.selection[0].childList and childView.__class__ != treerightviews.TitleListView): if childView.height(): childView.oldViewHeight = int(childView.height() * 100.0 / (topView.height() + childView.height())) splitter.setSizes([100, 0]) elif childView.oldViewHeight: childHeight = int(splitter.height() / 100.0 * childView.oldViewHeight) splitter.setSizes([splitter.height() - childHeight, childHeight]) childView.oldViewHeight = 0 for index in range(2): if splitter.widget(index).height(): # not collapsed splitter.widget(index).setEnabled(True) splitter.widget(index).updateView() else: splitter.widget(index).setEnabled(False) if TreeMainWin.setTypeDlg and TreeMainWin.setTypeDlg.isVisible(): # could be updateDlg(), except needed after ConfigDialog apply TreeMainWin.setTypeDlg.loadList() self.updateCmdAvail() if TreeMainWin.sortDlg and TreeMainWin.sortDlg.isVisible(): TreeMainWin.sortDlg.updateDialog() QtGui.QApplication.restoreOverrideCursor() def saveMultiWinTree(self): """Save tree select and open nodes when multiple windows show the same file""" self.duplicateSelect = self.doc.selection[:] self.storedOpenNodes = [node for node in self.doc.root.descendantList() if node.open] def updateMultiWinTree(self): """Update the tree and restore tree select and open nodes when multiple windows show the same file""" if self.duplicateSelect: self.doc.selection = self.duplicateSelect for node in self.doc.root.descendantGen(): node.open = False for node in self.storedOpenNodes: node.open = True self.updateViews() def updateCmdAvail(self): """Update the enabled status of menus""" notRoot = len(self.doc.selection) and \ self.doc.root not in self.doc.selection hasPrevSib = len(self.doc.selection) and None not in \ [node.prevSibling() for node in self.doc.selection] hasNextSib = len(self.doc.selection) and None not in \ [node.nextSibling() for node in self.doc.selection] selectParents = [node.parent for node in self.doc.selection] numChildren = [len(node.childList) for node in self.doc.selection] self.selectReqdActGrp.setEnabled(len(self.doc.selection)) self.notRootActGrp.setEnabled(notRoot) self.selParentsActGrp.setEnabled(len(filter(None, numChildren))) self.actions['FileSave'].setEnabled(self.doc.modified) self.actions['EditUndo'].setEnabled(len(self.doc.undoStore.undoList)) self.actions['EditRedo'].setEnabled(len(self.doc.redoStore.undoList)) self.actions['EditRename'].setEnabled(len(self.doc.selection) == 1) self.actions['EditIndent'].setEnabled(hasPrevSib) self.actions['EditUnindent'].setEnabled(notRoot and self.doc.root not in selectParents) self.actions['EditMoveUp'].setEnabled(hasPrevSib) self.actions['EditMoveDown'].setEnabled(hasNextSib) self.actions['EditMoveFirst'].setEnabled(hasPrevSib) self.actions['EditMoveLast'].setEnabled(hasNextSib) self.actions['ViewPreviousSelect'].setEnabled(len(self.doc.selection. prevSelects)) self.actions['ViewNextSelect'].setEnabled(len(self.doc.selection. nextSelects)) self.actions['DataFilterClear'].setEnabled(self.condFilter != None or len(self.textFilter)) self.actions['ToolsRemXLST'].setEnabled(len(self.doc.xlstLink)) self.actions['WinUpdateWindow'].setEnabled(len(globalref.treeControl. duplicateWindows())) self.statusBar().clearMessage() if self.pluginInterface: self.pluginInterface.execCallback(self.pluginInterface. viewUpdateCallbacks) def updateForFileChange(self, addToRecent=True): """Update GUI after file new or open""" globalref.updateRefs(self) self.setMainCaption() if self.leftTabs.currentWidget() == self.flatView: self.leftTabs.setCurrentWidget(self.treeView) if addToRecent: globalref.treeControl.recentFiles.addEntry(self.doc.fileName) if addToRecent and globalref.options.boolData('PersistTreeState'): if not globalref.treeControl.recentFiles.\ restoreTreeState(self.treeView): self.updateViews() else: self.updateViews() self.updateNonModalDialogs() globalref.treeControl.updateWinMenu() self.updateCmdAvail() globalref.treeControl.resetAutoSave() def updateNonModalDialogs(self): """Update any open non-modal dialogs and their toggle actions""" if TreeMainWin.configDlg and TreeMainWin.configDlg.isVisible(): TreeMainWin.configDlg.resetParam(True) self.actions['DataConfigType'].setChecked(True) else: TreeMainWin.configDlg = None self.actions['DataConfigType'].setChecked(False) if TreeMainWin.setTypeDlg and TreeMainWin.setTypeDlg.isVisible(): TreeMainWin.setTypeDlg.loadList() self.actions['DataSetDescendType'].setChecked(True) else: TreeMainWin.setTypeDlg = None self.actions['DataSetDescendType'].setChecked(False) if TreeMainWin.sortDlg and TreeMainWin.sortDlg.isVisible(): TreeMainWin.sortDlg.updateDialog() self.actions['DataSort'].setChecked(True) else: TreeMainWin.sortDlg = None self.actions['DataSort'].setChecked(False) self.actions['ToolsFind'].setChecked(bool(TreeMainWin.findDlg and \ TreeMainWin.findDlg.isVisible())) def updateAddTagAvail(self): """Update enabled status of editor tag entry commands""" self.addTagGroup.setEnabled(self.focusWidgetWithAttr('addHtmlTag') != None) def setPasteAvail(self): """Check to see if text is available to paste""" text = self.clipText() self.actions['EditPaste'].setEnabled(len(text)) self.actions['EditPasteText'].setEnabled(len(text)) def clipText(self): """Return text from the clipboard""" try: # QString argument is work-around for bug in PyQt 4.6 text = unicode(QtGui.QApplication.clipboard().\ text(QtCore.QString('xml'))) if not text: text = unicode(QtGui.QApplication.clipboard().text()) except UnicodeError: text = '' return text def updateColors(self): """Adjust the colors to the current option settings""" if globalref.options.boolData('UseDefaultColors'): pal = self.origPalette else: pal = QtGui.QPalette() pal.setColor(QtGui.QPalette.Base, self.getOptionColor('Background')) pal.setColor(QtGui.QPalette.Text, self.getOptionColor('Foreground')) QtGui.QApplication.setPalette(pal) def getOptionColor(self, name): """Return a color from option storage""" return QtGui.QColor(globalref.options.intData('%sR' % name, 0, 255), globalref.options.intData('%sG' % name, 0, 255), globalref.options.intData('%sB' % name, 0, 255)) def setOptionColor(self, name, color): """Store given color in options""" globalref.options.changeData('%sR' % name, str(color.red()), True) globalref.options.changeData('%sG' % name, str(color.green()), True) globalref.options.changeData('%sB' % name, str(color.blue()), True) def focusWidgetWithAttr(self, attr): """Return the focused widget or it's ancestor that has the given attr or None""" widget = QtGui.QApplication.focusWidget() while widget and not hasattr(widget, attr): widget = widget.parent() return widget def setupPlugins(self): """Load plugin modules""" self.pluginInterface = plugininterface.PluginInterface(self) pluginPaths = [os.path.join(globalref.modPath, 'plugins')] userPluginPath = globalref.options.pluginPath if userPluginPath: pluginPaths.append(userPluginPath) pluginList = [] for pluginPath in pluginPaths: if os.access(pluginPath, os.R_OK): sys.path.insert(1, pluginPath) pluginList.extend([name[:-3] for name in os.listdir(pluginPath) if name.endswith('.py')]) self.pluginInstances = [] # saves returned ref - avoid garbage collect self.pluginDescript = [] errorList = [] for name in pluginList: try: module = __import__(name) if not hasattr(module, 'main'): raise ImportError self.pluginInstances.append(module.main(self.pluginInterface)) descript = module.__doc__ if descript: descript = [line for line in descript.split('\n') if line.strip()][0].strip() if not descript: descript = name self.pluginDescript.append(descript) except ImportError: errorList.append(name) if errorList: QtGui.QMessageBox.warning(self, 'TreeLine', _('Could not load plugin module %s') % ', '.join(errorList)) def setStatusMsg(self, text, timeout=0, forceShow=False): """Set the status bar message with optional timeout. Force a hidden bar to show if forceShow is true""" self.statusBar().showMessage(text, timeout) if text and forceShow: self.statusBar().show() if not text and not self.showStatusBar: self.statusBar().hide() def setMainCaption(self): """Set main window caption using doc filename path""" caption = '' if self.doc.fileName: caption = u'%s [%s] ' % (os.path.basename(self.doc.fileName), os.path.dirname(self.doc.fileName)) caption += u'- TreeLine' self.setWindowTitle(caption) def getSaveFileName(self, caption, defaultExt, filterList, currentFilter=''): """Return user specified file name for save as & export""" dir, name = os.path.split(self.doc.fileName) if not dir: dir = globalref.treeControl.recentFiles.firstPath() if not dir: dir = unicode(os.environ.get('HOME', ''), sys.getfilesystemencoding()) if name: dir = os.path.join(dir, os.path.splitext(name)[0] + defaultExt) currentFilter = QtCore.QString(currentFilter) fileName = QtGui.QFileDialog.getSaveFileName(self, caption, dir, ';;'.join(filterList), currentFilter) fileName = unicode(fileName) if fileName: selectedFilter = unicode(currentFilter) fileName = fileName.split(';*')[0] # fix windows all filter bug if '.' not in fileName and \ selectedFilter != TreeMainWin.allFileFilter: fileName += defaultExt if selectedFilter == TreeMainWin.tlPlainFileFilter: self.doc.compressFile = False self.doc.encryptFile = False elif selectedFilter == TreeMainWin.tlCompFileFilter: self.doc.compressFile = True self.doc.encryptFile = False elif selectedFilter == TreeMainWin.tlEncryptFileFilter: self.doc.encryptFile = True return fileName return '' def getOpenFileName(self, caption, filterList): """Return user specified file name for file open""" dfltPath = os.path.dirname(self.doc.fileName) if not dfltPath: dfltPath = globalref.treeControl.recentFiles.firstPath() if not dfltPath: dfltPath = unicode(os.environ.get('HOME', ''), sys.getfilesystemencoding()) if not dfltPath: dfltPath = '..' fileName = QtGui.QFileDialog.getOpenFileName(self, caption, dfltPath, ';;'.join(filterList)) return unicode(fileName) def fileNew(self, newWinOk=True): """New file command""" if globalref.treeControl.savePrompt(): dlg = treedialogs.TemplateDialog(self) if dlg.exec_() != QtGui.QDialog.Accepted: return globalref.treeControl.newFile(dlg.selectedPath(), newWinOk) def fileOpen(self): """Open a file""" if globalref.treeControl.savePrompt(): filters = [TreeMainWin.tlGenFileFilter, TreeMainWin.textFileFilter, TreeMainWin.treepadFileFilter, TreeMainWin.xbelFileFilter, TreeMainWin.mozFileFilter, TreeMainWin.odfFileFilter, TreeMainWin.allFileFilter] fileName = self.getOpenFileName('', filters) if fileName: globalref.treeControl.openFile(fileName) def fileOpenSample(self): """Open a sample template file""" if globalref.treeControl.savePrompt(): path = self.findHelpPath() if not path: QtGui.QMessageBox.warning(self, 'TreeLine', _('Sample directory not found')) return fileName = unicode(QtGui.QFileDialog.getOpenFileName(self, _('Open Sample Template File'), os.path.dirname(path), TreeMainWin.tlGenFileFilter)) if fileName: globalref.treeControl.openFile(fileName) def fileSave(self): """Save current file""" if self.doc.fileName and not self.fileImported: globalref.treeControl.saveFile(self.doc.fileName) else: self.fileSaveAs() def fileSaveAs(self): """Save file with a new name""" oldFileName = self.doc.fileName filterList = [TreeMainWin.tlPlainFileFilter, TreeMainWin.tlCompFileFilter, TreeMainWin.tlEncryptFileFilter, TreeMainWin.allFileFilter] currentFilter = self.doc.encryptFile and 2 or \ (self.doc.compressFile and 1 or 0) fileName = self.getSaveFileName(_('Save As'), '.trl', filterList, filterList[currentFilter]) if fileName and globalref.treeControl.saveFile(fileName): self.setMainCaption() globalref.treeControl.recentFiles.addEntry(fileName) globalref.treeControl.updateWinMenu() self.fileImported = False globalref.treeControl.delAutoSaveFile(oldFileName) def fileExport(self): """Export the file as html, a table or text. Return fileName or ''""" ExportDlg = treedialogs.ExportDlg dlg = ExportDlg(self) if dlg.exec_() != QtGui.QDialog.Accepted: return '' indent = globalref.options.intData('IndentOffset', 0, optiondefaults.maxIndentOffset) nodeList = self.doc.selection addBranches = True if ExportDlg.exportWhat == ExportDlg.entireTree: nodeList = [self.doc.root] elif ExportDlg.exportWhat == ExportDlg.selectNode: addBranches = False elif ExportDlg.exportType != ExportDlg.tableType: nodeList = self.doc.selection.uniqueBranches() if not nodeList: QtGui.QMessageBox.warning(self, 'TreeLine', _('Nothing to export')) return '' fileName = '' try: if ExportDlg.exportType == ExportDlg.htmlType: fileName = self.getSaveFileName(_('Export Html'), '.html', [TreeMainWin.htmlFileFilter, TreeMainWin.allFileFilter]) if not fileName: return '' outGroup = output.OutputGroup() if addBranches: for node in nodeList: branch = node.outputItemList(ExportDlg.includeRoot, ExportDlg.openOnly, True) outGroup.extend(branch) else: outGroup.extend([node.outputItem(True) for node in nodeList]) self.doc.exportHtmlColumns(fileName, outGroup, ExportDlg.numColumns, indent, ExportDlg.addHeader) elif ExportDlg.exportType == ExportDlg.dirTableType: defaultDir, fn = os.path.split(self.doc.fileName) if not defaultDir: defaultDir = unicode(os.environ.get('HOME', ''), sys.getfilesystemencoding()) dirName = QtGui.QFileDialog.getExistingDirectory(self, _('Export to Directory'), defaultDir) dirName = unicode(dirName) if dirName: self.doc.exportDirTable(dirName, nodeList, ExportDlg.addHeader) fileName = dirName elif ExportDlg.exportType == ExportDlg.dirPageType: defaultDir, fn = os.path.split(self.doc.fileName) if not defaultDir: defaultDir = unicode(os.environ.get('HOME', ''), sys.getfilesystemencoding()) dirName = QtGui.QFileDialog.getExistingDirectory(self, _('Export to Directory'), defaultDir) dirName = unicode(dirName) if dirName: self.doc.exportDirPage(dirName, nodeList) fileName = dirName elif ExportDlg.exportType == ExportDlg.xsltType: dlgText = _('A link to a stylesheet can be added to the '\ 'XSL file\nEnter a CSS filename (blank for none)') link, ok = QtGui.QInputDialog.getText(self, 'TreeLine', dlgText, QtGui.QLineEdit.Normal, self.doc.xslCssLink) if ok: fileName = self.getSaveFileName(_('Export XSLT'), '.xsl', [TreeMainWin.xsltFileFilter, TreeMainWin.allFileFilter]) if fileName: if self.doc.xslCssLink != unicode(link): self.doc.xslCssLink = unicode(link) self.doc.modified = True self.doc.exportXslt(fileName, ExportDlg.includeRoot, indent) self.actions['FileSave'].setEnabled(self.doc.modified) elif ExportDlg.exportType == ExportDlg.trlType: filterList = [TreeMainWin.tlPlainFileFilter, TreeMainWin.tlCompFileFilter, TreeMainWin.allFileFilter] origCompress = self.doc.compressFile fileName = self.getSaveFileName(_('Export Subtree'), '.trl', filterList, self.doc.compressFile) if fileName: self.doc.exportTrlSubtree(fileName, nodeList, addBranches) self.doc.compressFile = origCompress elif ExportDlg.exportType == ExportDlg.tableType: fileName = self.getSaveFileName(_('Export Table'), '.tbl', [TreeMainWin.tableFileFilter, TreeMainWin.allFileFilter]) if fileName: self.doc.exportTable(fileName, nodeList, addBranches) elif ExportDlg.exportType == ExportDlg.textType: fileName = self.getSaveFileName(_('Export Titles'), '.txt', [TreeMainWin.textFileFilter, TreeMainWin.allFileFilter]) if fileName: self.doc.exportTabbedTitles(fileName, nodeList, addBranches, ExportDlg.includeRoot, ExportDlg.openOnly) elif ExportDlg.exportType == ExportDlg.xbelType: fileName = self.getSaveFileName(_('Export XBEL Bookmarks'), '.xml', [TreeMainWin.xbelFileFilter, TreeMainWin.allFileFilter]) if fileName: self.doc.exportXbel(fileName, nodeList, addBranches) elif ExportDlg.exportType == ExportDlg.mozType: fileName = self.getSaveFileName(_('Export Html Bookmarks'), '.html', [TreeMainWin.mozFileFilter, TreeMainWin.allFileFilter]) if fileName: self.doc.exportHtmlBookmarks(fileName, nodeList, addBranches) elif ExportDlg.exportType == ExportDlg.xmlType: fileName = self.getSaveFileName(_('Export Generic XML'), '.xml', [TreeMainWin.xmlFileFilter, TreeMainWin.allFileFilter]) if fileName: self.doc.exportGenericXml(fileName, nodeList, addBranches) else: # ODF type fontInfo = QtGui.QFontInfo(self.dataOutSplit.widget(0).font()) fileName = self.getSaveFileName(_('Export ODF Text'), '.odt', [TreeMainWin.odfFileFilter, TreeMainWin.allFileFilter]) if fileName: self.doc.exportOdf(fileName, nodeList, fontInfo.family(), fontInfo.pointSize(), fontInfo.fixedPitch(), addBranches, ExportDlg.includeRoot, ExportDlg.openOnly) except IOError, e: if ExportDlg.exportType == ExportDlg.dirTableType: QtGui.QMessageBox.warning(self, 'TreeLine', unicode(e)) else: QtGui.QMessageBox.warning(self, 'TreeLine', _('Error - Could not write to %s') % \ fileName) return '' return fileName def editUndo(self): """Undo the previous action""" self.doc.undoStore.undo(self.doc.redoStore) if TreeMainWin.setTypeDlg and TreeMainWin.setTypeDlg.isVisible(): TreeMainWin.setTypeDlg.loadList() if TreeMainWin.configDlg: TreeMainWin.configDlg.resetParam() def editRedo(self): """Redo the previous undo""" self.doc.redoStore.undo(self.doc.undoStore) if TreeMainWin.setTypeDlg and TreeMainWin.setTypeDlg.isVisible(): TreeMainWin.setTypeDlg.loadList() if TreeMainWin.configDlg: TreeMainWin.configDlg.resetParam() def editCut(self): """Cut the branch or text to the clipboard""" widget = self.focusWidgetWithAttr('copyAvail') if (self.treeView.hasFocus() or self.flatView.hasFocus()) or \ not widget or not widget.copyAvail(): self.editCopyTree() self.editDelete() else: widget.cut() def editCopyTree(self): """Copy the tree branch to the clipboard""" if not self.doc.selection: return clip = QtGui.QApplication.clipboard() if clip.supportsSelection(): textList = [] if len(self.doc.selection) > 1: for node in self.doc.selection: textList.extend(node.exportToText()) else: textList = self.doc.selection[0].exportToText() clip.setText(u'\n'.join(textList), QtGui.QClipboard.Selection) self.mimeData = self.treeView.mimeData() # TEMP req'd for PyQt bug clip.setMimeData(self.mimeData) def editCopy(self): """Copy the branch or text to the clipboard""" split = self.rightTabs.currentWidget() if split == self.dataOutSplit: # check select in dataOut (no focus) views = [view for view in split.children() if \ hasattr(view, 'copyAvail') and view.copyAvail()] if views: views[0].copy() return widget = self.focusWidgetWithAttr('copyAvail') if (self.treeView.hasFocus() or self.flatView.hasFocus()) or \ not widget or not widget.copyAvail(): self.editCopyTree() else: widget.copy() def editCopyText(self): """Copy node title text to the clipboard""" if not self.doc.selection: return titles = [item.title() for item in self.doc.selection] clip = QtGui.QApplication.clipboard() if clip.supportsSelection(): clip.setText(u'\n'.join(titles), QtGui.QClipboard.Selection) clip.setText(u'\n'.join(titles)) def editPaste(self): """Paste items or text from the clipboard""" text = self.clipText() if not text: return leftFocus = self.treeView.hasFocus() or self.flatView.hasFocus() if leftFocus: item, newFormats = self.doc.readXmlStringAndFormat(text) if item: if not self.doc.selection: return if item.formatName == treedoc.TreeDoc.copyFormat.name: itemList = item.childList else: # copyFormat is dummy root of multi-select itemList = [item] if newFormats: self.doc.undoStore.addBranchUndo(self.doc.selection) for format in newFormats: self.doc.treeFormats.addIfMissing(format) self.doc.treeFormats.updateDerivedTypes() self.doc.treeFormats.updateUniqueID() if TreeMainWin.configDlg: TreeMainWin.configDlg.resetParam() else: self.doc.undoStore.addChildListUndo(self.doc.selection) selectList = [] for parent in self.doc.selection: for node in itemList: newNode = node.duplicateNode() parent.addTree(newNode) selectList.append(newNode) parent.open = True self.doc.selection.replace(selectList) if newFormats: self.doc.treeFormats.updateAutoChoices() self.updateViews() else: print 'Error reading XML string' else: item = self.doc.readXmlString(text) if item: text = item.title() widget = self.focusWidgetWithAttr('paste') if widget: widget.paste() def editPasteText(self): """Paste text from the clipboard""" text = self.clipText() if not text: return item = self.doc.readXmlString(text) if item and item.data: text = item.title() elif item and item.childList: text = item.childList[0].title() else: text = text.split(u'\n', 1)[0].strip() if self.treeView.hasFocus() or self.flatView.hasFocus(): for item in self.doc.selection: item.setTitle(text, True) self.updateViewItem(item) self.updateCmdAvail() else: widget = self.focusWidgetWithAttr('pasteText') if widget: widget.paste() def editRename(self): """Start rename editor in selected tree node""" view = self.leftTabs.currentWidget() view.editItem(self.doc.selection[0].viewData) def editInBefore(self): """Insert new sibling before selection""" newList = [] self.doc.undoStore.addParentListUndo(self.doc.selection) for sibling in self.doc.selection: newList.append(sibling.insertSibling()) if globalref.options.boolData('RenameNewNodes'): self.doc.selection.replace(newList) if len(newList) == 1: self.updateViews() view = self.leftTabs.currentWidget() view.editItem(newList[0].viewData) return self.updateViews() def editInAfter(self): """Insert new sibling after selection""" newList = [] self.doc.undoStore.addParentListUndo(self.doc.selection) for sibling in self.doc.selection: newList.append(sibling.insertSibling(inAfter=True)) if globalref.options.boolData('RenameNewNodes'): self.doc.selection.replace(newList) if len(newList) == 1: self.updateViews() view = self.leftTabs.currentWidget() view.editItem(newList[0].viewData) return self.updateViews() def editAddChild(self): """Add a new child to the selected parent""" newList = [] self.doc.undoStore.addChildListUndo(self.doc.selection) for parent in self.doc.selection: newList.append(parent.addChild()) parent.open = True if globalref.options.boolData('RenameNewNodes'): self.doc.selection.replace(newList) if len(newList) == 1: self.updateViews() view = self.leftTabs.currentWidget() view.editItem(newList[0].viewData) return self.updateViews() def editDelete(self): """Delete the selected items""" if not self.doc.selection or self.doc.root in self.doc.selection: return nextSel = filter(None, [item.parent for item in self.doc.selection]) nextSel.extend(filter(None, [item.prevSibling() for item in self.doc.selection])) nextSel.extend(filter(None, [item.nextSibling() for item in self.doc.selection])) self.doc.undoStore.addParentListUndo(self.doc.selection) for item in self.doc.selection: item.delete() try: self.flatView.rootItems.remove(item) except ValueError: pass while nextSel[-1] in self.doc.selection: del nextSel[-1] self.doc.selection.replace([nextSel[-1]]) self.doc.selection.currentItem = nextSel[-1] # Reqd if only root left self.doc.selection.validateHistory() self.updateViews() def sortedSelection(self): """Return a sorted selection list for indent & move commands""" sortedList = self.doc.selection[:] for item in sortedList: item.viewData.loadTempSortKey() sortedList.sort(lambda x,y: cmp(x.viewData.tempSortKey, y.viewData.tempSortKey)) return sortedList def editIndent(self): """Indent the selected items""" sortlist = self.sortedSelection() parentList = [item.parent for item in sortlist] siblingList = [item.prevSibling() for item in sortlist] self.doc.undoStore.addChildListUndo(parentList + siblingList) for item in sortlist: if item.indent(): item.parent.open = True self.updateViews() def editUnindent(self): """Unindent the selected item""" sortlist = self.sortedSelection() parentList = [item.parent for item in sortlist] gpList = [item.parent for item in parentList] self.doc.undoStore.addChildListUndo(parentList + gpList) sortlist.reverse() for item in sortlist: item.unindent() self.updateViews() def editMoveUp(self): """Move the selected item up""" sortlist = self.sortedSelection() self.doc.undoStore.addParentListUndo(sortlist, True) for item in sortlist: item.move(-1) self.updateViews() def editMoveDown(self): """Move the selected item down""" sortlist = self.sortedSelection() sortlist.reverse() self.doc.undoStore.addParentListUndo(sortlist, True) for item in sortlist: item.move(1) self.updateViews() def editMoveFirst(self): """Move the selected item to be first child""" sortlist = self.sortedSelection() sortlist.reverse() self.doc.undoStore.addParentListUndo(sortlist, True) for item in sortlist: item.moveFirst() self.updateViews() def editMoveLast(self): """Move the selected item to be first child""" sortlist = self.sortedSelection() self.doc.undoStore.addParentListUndo(sortlist, True) for item in sortlist: item.moveLast() self.updateViews() def viewPrevSelect(self): """View the previous tree selection""" self.doc.selection.restorePrevSelect() def viewNextSelect(self): """View the next tree selection""" self.doc.selection.restoreNextSelect() def viewLeftSelect(self, action): """Show left view given by action""" if action == self.actions['ViewTree']: self.leftTabs.setCurrentWidget(self.treeView) else: self.leftTabs.setCurrentWidget(self.flatView) def viewRightSelect(self, action): """Show right view given by action""" if action == self.actions['ViewDataOutput']: self.rightTabs.setCurrentWidget(self.dataOutSplit) elif action == self.actions['ViewDataEdit']: self.rightTabs.setCurrentWidget(self.dataEditSplit) else: self.rightTabs.setCurrentWidget(self.titleListSplit) def viewChildren(self, checked): """Set to view item alone, with children or with descendants""" self.showItemChildren = checked self.updateRightView() def viewDescendants(self, checked): """Toggle showing descendants in output view""" self.dataOutSplit.widget(1).showDescendants = checked self.updateRightView() def viewStatusBar(self, checked): """Toggle the display of the status bar""" if checked: self.statusBar().show() else: self.statusBar().hide() self.showStatusBar = checked def dataTypeChange(self, action): """Change type based on submenu selection""" if self.doc.selection: self.doc.undoStore.addTypeUndo(self.doc.selection) for item in self.doc.selection: item.changeType(unicode(action.toolTip())) self.doc.modified = True self.updateViews() def dataSet(self, show): """Show dialog for setting item data types""" if show: if not TreeMainWin.setTypeDlg: TreeMainWin.setTypeDlg = treedialogs.TypeSetDlg() self.connect(TreeMainWin.setTypeDlg, QtCore.SIGNAL('viewClosed'), globalref.treeControl.updateDialogs) else: TreeMainWin.setTypeDlg.loadList() TreeMainWin.setTypeDlg.setCurrentSel() TreeMainWin.setTypeDlg.updateDlg() TreeMainWin.setTypeDlg.show() else: TreeMainWin.setTypeDlg.hide() def dataConfig(self, show): """Show dialog for modifying data types""" if show: if not TreeMainWin.configDlg: TreeMainWin.configDlg = configdialog.ConfigDialog() self.connect(TreeMainWin.configDlg, QtCore.SIGNAL('dialogClosed'), globalref.treeControl.updateDialogs) elif not TreeMainWin.configDlg.isVisible(): TreeMainWin.configDlg.resetCurrent() TreeMainWin.configDlg.show() else: TreeMainWin.configDlg.close() def dataCopyTypes(self): """Copy the configuration from another TreeLine file""" dfltDir = os.path.dirname(self.doc.fileName) fileName = unicode(QtGui.QFileDialog.getOpenFileName(self, _('Open Configuration File'), dfltDir, TreeMainWin.tlGenFileFilter)) password = '' while fileName: QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) try: self.doc.treeFormats.configCopy(fileName, password) QtGui.QApplication.restoreOverrideCursor() if TreeMainWin.configDlg: TreeMainWin.configDlg.resetParam() return except treedoc.PasswordError: QtGui.QApplication.restoreOverrideCursor() dlg = treedialogs.PasswordEntry(False, self) if dlg.exec_() != QtGui.QDialog.Accepted: return password = dlg.password except (IOError, UnicodeError, treedoc.ReadFileError): QtGui.QApplication.restoreOverrideCursor() QtGui.QMessageBox.warning(self, 'TreeLine', _('Error - could not read file "%s"') % fileName) return def dataSort(self, show): """Open the dialog for sorting nodes""" if show: if not TreeMainWin.sortDlg: TreeMainWin.sortDlg = treedialogs.SortDlg() self.connect(TreeMainWin.sortDlg, QtCore.SIGNAL('viewClosed'), globalref.treeControl.updateDialogs) else: TreeMainWin.sortDlg.updateDialog() TreeMainWin.sortDlg.show() else: TreeMainWin.sortDlg.hide() def dataFilterCond(self): """Filter types with conditional rules""" if self.condFilter: type = self.condFilter.formatName else: typeList = self.doc.treeFormats.nameList(True) defaultTypeNum = 0 if self.doc.selection: defaultTypeNum = typeList.index(self.doc.selection[0]. formatName) type, ok = QtGui.QInputDialog.getItem(self, _('Filter Data'), _('Select data type'), typeList, defaultTypeNum, False) if not ok: return format = self.doc.treeFormats[unicode(type)] dlg = configdialog.ConditionDlg(_('Filter %s Data Type') % format.name, format, self) if self.condFilter: dlg.setConditions(self.condFilter) if dlg.exec_() == QtGui.QDialog.Accepted: self.condFilter = dlg.conditional() self.condFilter.setupFields(format) self.condFilter.formatName = format.name if self.leftTabs.currentWidget() == self.flatView: self.updateLeftView() else: self.leftTabs.setCurrentWidget(self.flatView) self.updateCmdAvail() def dataFilterText(self): """Filter with a text search string""" searchStr, ok = QtGui.QInputDialog.getText(self, _('Filter Data'), _('Enter key words'), QtGui.QLineEdit.Normal, ' '.join(self.textFilter)) if not ok: return self.textFilter = [text.lower() for text in unicode(searchStr).strip().split()] if self.leftTabs.currentWidget() == self.flatView: self.updateLeftView() else: self.leftTabs.setCurrentWidget(self.flatView) self.updateCmdAvail() def dataFilterClear(self): """Clear current filtering""" self.condFilter = None self.textFilter = [] if self.leftTabs.currentWidget() == self.flatView: self.updateLeftView() self.updateCmdAvail() def dataEditField(self): """Edit a child field in all selected nodes""" if not self.doc.selection: return fieldList = self.doc.treeFormats.commonFields(self.doc.selection) if fieldList: # has common fields dlg = treedialogs.EditFieldsDlg(fieldList, self) if dlg.exec_() != QtGui.QDialog.Accepted: return self.doc.undoStore.addDataUndo(self.doc.selection) for item in self.doc.selection: item.editFields(dlg.resultDict) if self.pluginInterface: fields = [item.nodeFormat().findField(name) for name in dlg.resultDict.keys()] self.pluginInterface.execCallback(globalref. pluginInterface. dataChangeCallbacks, item, fields) self.updateViews() else: QtGui.QMessageBox.warning(self, 'TreeLine', _('No common fields to set')) def dataNumbering(self): """Add numbering to a data field""" item = self.doc.selection[0] NumberingDlg = treedialogs.NumberingDlg dlg = NumberingDlg(item.branchFields(), item.maxDescendLevel(), self) if dlg.exec_() != QtGui.QDialog.Accepted: return self.doc.undoStore.addBranchUndo(item) if dlg.currentStyle == NumberingDlg.outlineType: item.addNumbering(dlg.getField(), dlg.currentFormat, dlg.includeRoot(), False, not dlg.existOnly(), False, dlg.startNumber()) elif dlg.currentStyle == NumberingDlg.sectionType: item.addNumbering(dlg.getField(), dlg.currentFormat, dlg.includeRoot(), True, not dlg.existOnly(), False, dlg.startNumber()) else: # singleType item.addNumbering(dlg.getField(), dlg.currentFormat, False, False, not dlg.existOnly(), True, dlg.startNumber()) self.updateViews() if TreeMainWin.configDlg: TreeMainWin.configDlg.resetParam() def dataAddCat(self): """Add child's category items as a new child level""" selectList = self.doc.selection.uniqueBranches() children = [] for item in selectList: children.extend(item.childList) fieldList = self.doc.treeFormats.commonFields(children) if fieldList: # has common fields dlg = treedialogs.FieldSelectDlg(fieldList, _('Category Fields'), _('Select fields for new level'), self) if dlg.exec_() != QtGui.QDialog.Accepted: return self.doc.undoStore.addBranchUndo(selectList) for item in selectList: item.addChildCat(dlg.getSelList()) self.updateViews() if TreeMainWin.configDlg: TreeMainWin.configDlg.resetParam() else: QtGui.QMessageBox.warning(self, 'TreeLine', _('Cannot expand without common fields')) def dataFlatCat(self): """Collapse data by merging fields""" selectList = self.doc.selection.uniqueBranches() self.doc.undoStore.addBranchUndo(selectList) for item in selectList: item.flatChildCat() self.updateViews() if TreeMainWin.configDlg: TreeMainWin.configDlg.resetParam() def dataArrangeRef(self): """Arrange data using parent references""" selectList = self.doc.selection.uniqueBranches() children = [] for item in selectList: children.extend(item.childList) fieldList = self.doc.treeFormats.commonFields(children) if not fieldList: QtGui.QMessageBox.warning(self, 'TreeLine', _('No common fields with parent '\ 'references')) return refField, ok = QtGui.QInputDialog.getItem(self, _('Reference Field'), _('Select field with parent'\ ' references'), fieldList, 0, False) if not ok: return self.doc.undoStore.addBranchUndo(selectList) for item in selectList: item.arrangeByRef(unicode(refField)) self.updateViews() def dataFlatRef(self): """Collapse data after adding references to parents""" selectList = self.doc.selection.uniqueBranches() dlg = configdialog.FieldEntry(_('Flatten by Reference'), _('Enter new field name for parent'\ ' references:'), '', [], self) if dlg.exec_() != QtGui.QDialog.Accepted: return self.doc.undoStore.addBranchUndo(selectList) for item in selectList: item.flatByRef(dlg.text) self.updateViews() if TreeMainWin.configDlg: TreeMainWin.configDlg.resetParam() def toolsExpand(self): """Expand all children of selected item""" for item in self.doc.selection: item.openBranch(True) self.updateViews() def toolsCollapse(self): """Collapse all children of selected item""" for item in self.doc.selection: item.openBranch(False) self.updateViews() def toolsFind(self, show): """Find item matching text string""" if show: if not TreeMainWin.findDlg: TreeMainWin.findDlg = treedialogs.FindTextEntry() self.connect(TreeMainWin.findDlg, QtCore.SIGNAL('viewClosed'), globalref.treeControl.updateDialogs) TreeMainWin.findDlg.entry.selectAll() TreeMainWin.findDlg.entry.setFocus() TreeMainWin.findDlg.show() else: TreeMainWin.findDlg.hide() def toolsSpellCheck(self): """Spell check the tree's text data strting in the selected branch""" spellPath = globalref.options.strData('SpellCheckPath', True) try: spCheck = spellcheck.SpellCheck(spellPath, self.doc.spellChkLang) except spellcheck.SpellCheckError: if sys.platform.startswith('win'): ans = QtGui.QMessageBox.warning(self, _('Spell Check Error'), _('Could not find either aspell.exe '\ 'or ispell.exe\nManually locate?'), _('&Browse'), _('&Cancel'), '', 0, 1) if ans != 0: return path = unicode(QtGui.QFileDialog.getOpenFileName(self, _('Locate aspell.exe or ipsell.exe'), '', _('Program (*.exe)'))) if path: path = path[:-4] if ' ' in path: path = '"%s"' % path globalref.options.changeData('SpellCheckPath', path.encode(sys.getfilesystemencoding()), True) globalref.options.writeChanges() self.toolsSpellCheck() return else: QtGui.QMessageBox.warning(self, 'TreeLine', _('TreeLine Spell Check Error\n'\ 'Make sure aspell or ispell is installed')) return if self.leftTabs.currentWidget() == self.flatView: self.leftTabs.setCurrentWidget(self.treeView) origSelect = self.doc.selection.uniqueBranches() dlg = treedialogs.SpellCheckDlg(spCheck, origSelect, self) if dlg.startSpellCheck(): if dlg.exec_() != QtGui.QDialog.Accepted: spCheck.close() return spCheck.close() if origSelect[0].parent: ans = QtGui.QMessageBox.information(self, _('TreeLine Spell Check'), _('Finished checking the branch\n'\ 'Continue from the root branch?'), _('&Yes'), _('&No'), '', 0, 1) if ans == 0: globalref.docRef.selection.changeSearchOpen([self.doc.root]) self.toolsSpellCheck() else: QtGui.QMessageBox.information(self, _('TreeLine Spell Check'), _('Finished checking the branch')) globalref.docRef.selection.changeSearchOpen(origSelect) def toolsRemXslt(self): """Delete reference to XSLT export""" if self.doc.xlstLink: self.doc.undoStore.addParamUndo([(self.doc, 'xlstLink')]) self.doc.xlstLink = '' self.doc.modified = True self.updateCmdAvail() def toolsGenOpt(self): """Set user preferences for all files""" oldAutoSave = globalref.options.intData('AutoSaveMinutes', 0, 999) dlg = optiondlg.OptionDlg(globalref.options, self) dlg.setWindowTitle(_('General Options')) dlg.startGroupBox(_('Startup Condition')) optiondlg.OptionDlgBool(dlg, 'AutoFileOpen', _('Automatically open last file used')) optiondlg.OptionDlgBool(dlg, 'StartShowChildren', _('Show children in right-hand view')) optiondlg.OptionDlgBool(dlg, 'StartShowDescend', _('Show descendants in output view')) optiondlg.OptionDlgBool(dlg, 'ShowStatusBar', _('Show status bar')) optiondlg.OptionDlgBool(dlg, 'PersistTreeState', _('Restore view states of recent files')) optiondlg.OptionDlgBool(dlg, 'SaveWindowGeom', _('Restore window geometry from last exit')) dlg.startGroupBox(_('Features Available')) optiondlg.OptionDlgBool(dlg, 'ClickRename', _('Click item to rename')) optiondlg.OptionDlgBool(dlg, 'DragTree', _('Tree drag && drop available')) optiondlg.OptionDlgBool(dlg, 'InsertOnEnter', _('Insert node with enter')) optiondlg.OptionDlgBool(dlg, 'RenameNewNodes', _('Rename new nodes when created')) optiondlg.OptionDlgBool(dlg, 'OpenSearchNodes', _('Automatically open search nodes')) optiondlg.OptionDlgBool(dlg, 'ShowTreeIcons', _('Show icons in the tree view')) optiondlg.OptionDlgBool(dlg, 'EnableExecLinks', _('Enable executable links')) optiondlg.OptionDlgBool(dlg, 'OpenNewWindow', _('Open files in new windows')) dlg.startNewColumn() dlg.startGroupBox(_('New Objects')) optiondlg.OptionDlgBool(dlg, 'CompressNewFiles', _('Set new files to compressed by default')) optiondlg.OptionDlgBool(dlg, 'EncryptNewFiles', _('Set new files to encrypted by default')) optiondlg.OptionDlgBool(dlg, 'HtmlNewFields', _('New fields default to HTML content')) dlg.endGroupBox() optiondlg.OptionDlgRadio(dlg, 'SelectOrder', _('Multiple Selection Sequence'), [('tree', _('Tree order')), ('select', _('Selection order'))]) dlg.startGroupBox(_('Data Editor Pages')) optiondlg.OptionDlgInt(dlg, 'EditorPages', _('Number of pages shown \n(set to 0 for all)'), 0, 999) dlg.startGroupBox(_('Undo Memory')) optiondlg.OptionDlgInt(dlg, 'UndoLevels', '%s ' % _('Number of undo levels'), 0, 99) dlg.startNewColumn() dlg.startGroupBox(_('Auto Save')) optiondlg.OptionDlgInt(dlg, 'AutoSaveMinutes', _('Minutes between saves \n' '(set to 0 to disable)'), 0, 999) dlg.startGroupBox(_('Recent Files')) optiondlg.OptionDlgInt(dlg, 'RecentFiles', _('Number of recent files \nin the File menu'), 0, 99) dlg.startGroupBox(_('Data Editor Formats')) optiondlg.OptionDlgStr(dlg, 'EditDateFormat', _('Dates')) optiondlg.OptionDlgStr(dlg, 'EditTimeFormat', _('Times')) dlg.startGroupBox(_('Appearance')) optiondlg.OptionDlgInt(dlg, 'IndentOffset', _('Child indent offset (points)'), 0, optiondefaults.maxIndentOffset) optiondlg.OptionDlgInt(dlg, 'MaxEditLines', _('Default max data editor lines'), 1, optiondefaults.maxNumLines) if dlg.exec_() == QtGui.QDialog.Accepted: if not globalref.options.boolData('PersistTreeState'): globalref.treeControl.recentFiles.clearTreeStates() globalref.options.writeChanges() if oldAutoSave != globalref.options.intData('AutoSaveMinutes', 0, 999): globalref.treeControl.resetAutoSave() self.doc.undoStore.levels = globalref.options.\ intData('UndoLevels', 0, 99) self.doc.redoStore.levels = globalref.options.\ intData('UndoLevels', 0, 99) globalref.treeControl.recentFiles.\ changeNumEntries(globalref.options. intData('RecentFiles', 0, 99)) self.treeView.updateGenOptions() self.flatView.updateGenOptions() self.updateViews() def toolsTreeFont(self): """Show dialog for setting custom tree font""" oldTreeFont = self.treeView.font() treeFont, ok = QtGui.QFontDialog.getFont(oldTreeFont, self) if ok and treeFont != oldTreeFont: self.treeView.setFont(treeFont) self.flatView.setFont(treeFont) self.saveFontToOptions(treeFont, 'Tree') self.updateViews() def toolsOutputFont(self): """Show dialog for setting custom output font""" oldOutputFont = self.dataOutSplit.widget(0).font() outputFont, ok = QtGui.QFontDialog.getFont(oldOutputFont, self) if ok and outputFont != oldOutputFont: self.dataOutSplit.widget(0).setFont(outputFont) self.dataOutSplit.widget(1).setFont(outputFont) self.saveFontToOptions(outputFont, 'Output') self.updateViews() def toolsEditFont(self): """Show dialog for setting custom editor font""" oldEditFont = self.dataEditSplit.widget(0).font() editFont, ok = QtGui.QFontDialog.getFont(oldEditFont, self) if ok and editFont != oldEditFont: for i in range(2): self.dataEditSplit.widget(i).setFont(editFont) self.titleListSplit.widget(i).setFont(editFont) self.saveFontToOptions(editFont, 'Editor') self.updateViews() def toolsFileOpt(self): """Set file preferences""" globalref.options.addData('SpaceBetween', self.doc.spaceBetween and 'yes' or 'no', False) globalref.options.addData('LineBreaks', self.doc.lineBreaks and 'yes' or 'no', False) globalref.options.addData('FormHtml', self.doc.formHtml and 'yes' or 'no', False) globalref.options.addData('CompressFile', self.doc.compressFile and 'yes' or 'no', False) globalref.options.addData('EncryptFile', self.doc.encryptFile and 'yes' or 'no', False) globalref.options.addData('ChildFieldSep', self.doc.childFieldSep, False) globalref.options.addData('SpellChkLang', self.doc.spellChkLang, False) dlg = optiondlg.OptionDlg(globalref.options, self) dlg.setWindowTitle(_('File Options')) dlg.startGroupBox(_('Output Formating')) optiondlg.OptionDlgBool(dlg, 'SpaceBetween', _('Add blank lines between nodes'), False) optiondlg.OptionDlgBool(dlg, 'LineBreaks', _('Add line breaks after each line'), False) optiondlg.OptionDlgBool(dlg, 'FormHtml', _('Allow HTML rich text in formats'), False) dlg.startGroupBox(_('File Storage')) optiondlg.OptionDlgBool(dlg, 'CompressFile', _('Use file compression'), False) optiondlg.OptionDlgBool(dlg, 'EncryptFile', _('Use file encryption'), False) dlg.startGroupBox(_('Embedded Child Fields')) optiondlg.OptionDlgStr(dlg, 'ChildFieldSep', _('Separator String'), False) dlg.startGroupBox(_('Spell Check Language')) optiondlg.OptionDlgStr(dlg, 'SpellChkLang', '%s\n%s' % (_('2-letter code (blank'), _('for system default)')), False) if dlg.exec_() == QtGui.QDialog.Accepted: space = globalref.options.boolData('SpaceBetween') breaks = globalref.options.boolData('LineBreaks') html = globalref.options.boolData('FormHtml') compress = globalref.options.boolData('CompressFile') encrypt = globalref.options.boolData('EncryptFile') childFieldSep = globalref.options.strData('ChildFieldSep', True) spellChkLang = globalref.options.strData('SpellChkLang', True) if space != self.doc.spaceBetween or \ breaks != self.doc.lineBreaks or \ html != self.doc.formHtml or \ compress != self.doc.compressFile or \ encrypt != self.doc.encryptFile or \ childFieldSep != self.doc.childFieldSep or \ spellChkLang != self.doc.spellChkLang: self.doc.undoStore.addParamUndo([(self.doc, 'spaceBetween'), (self.doc, 'lineBreaks'), (self.doc, 'formHtml'), (self.doc, 'compressFile'), (self.doc, 'encryptFile'), (self.doc, 'childFieldSep')]) self.doc.spaceBetween = space self.doc.lineBreaks = breaks self.doc.formHtml = html self.doc.compressFile = compress self.doc.encryptFile = encrypt self.doc.childFieldSep = childFieldSep self.doc.spellChkLang = spellChkLang self.doc.modified = True self.updateViews() self.updateCmdAvail() def toolsShortcuts(self): """Start dialog to customize keyboard shorcuts""" dlg = treedialogs.ShortcutDlg(self) if dlg.exec_() == QtGui.QDialog.Accepted: self.setupShortcuts() def toolsCustomToolbar(self): """Start dialog to customize toolbar buttons""" dlg = treedialogs.ToolbarDlg(self.setupToolbars, TreeMainWin.toolIcons, self) if dlg.exec_() == QtGui.QDialog.Accepted: pass def toolsDefaultColor(self, checked): """Toggle default color setting""" setting = checked and 'yes' or 'no' globalref.options.changeData('UseDefaultColors', setting, True) globalref.options.writeChanges() self.updateColors() def toolsBkColor(self): """Set view background color""" background = self.getOptionColor('Background') newColor = QtGui.QColorDialog.getColor(background, self) if newColor.isValid() and newColor != background: self.setOptionColor('Background', newColor) globalref.options.writeChanges() self.updateColors() def toolsTxtColor(self): """Set view text color""" foreground = self.getOptionColor('Foreground') newColor = QtGui.QColorDialog.getColor(foreground, self) if newColor.isValid() and newColor != foreground: self.setOptionColor('Foreground', newColor) globalref.options.writeChanges() self.updateColors() def helpContents(self): """View the Using section of the ReadMe file""" self.helpReadMe() if TreeMainWin.helpView: TreeMainWin.helpView.textView.scrollToAnchor('using') def findHelpPath(self): """Return the full path of the help files or ''""" pathList = [helpFilePath, os.path.join(globalref.modPath, '../doc/'), globalref.modPath, os.path.join(globalref.modPath, 'doc/')] fileList = ['README.html'] if globalref.lang and globalref.lang != 'C': fileList[0:0] = ['README_%s.html' % globalref.lang, 'README_%s.html' % globalref.lang[:2]] for path in filter(None, pathList): for fileName in fileList: fullPath = os.path.join(path, fileName) if os.access(fullPath, os.R_OK): return fullPath return '' def helpReadMe(self): """View the ReadMe file""" if not TreeMainWin.helpView: path = self.findHelpPath() if path: TreeMainWin.toolIcons.loadIcons(['helpback', 'helpforward', 'helphome', 'helpprevious', 'helpnext']) TreeMainWin.helpView = helpview.HelpView(path, _('TreeLine README File'), TreeMainWin.toolIcons) else: QtGui.QMessageBox.warning(self, 'TreeLine', _('Read Me file not found')) return TreeMainWin.helpView.show() TreeMainWin.helpView.textView.home() def helpAbout(self): """About this program""" QtGui.QMessageBox.about(self, 'TreeLine', _('TreeLine, Version %(ver)s\n by %(author)s') % {'ver':__version__, 'author':__author__}) def helpPlugin(self): """Show loaded plugin modules""" dlg = treedialogs.PluginListDlg(self.pluginDescript, self) dlg.exec_() def addTextTag(self): """Add html tag to active data editor""" editor = self.focusWidgetWithAttr('addHtmlTag') if not editor: return label = unicode(self.sender().text()) openTag, closeTag = TreeMainWin.tagDict[label] if label == _('&Size...'): num, ok = QtGui.QInputDialog.getInteger(self, _('Font Size'), _('Enter size factor '\ '(-6 to +6)'), 1, -6, 6) if not ok or num == 0: return openTag = openTag % num elif label == _('&Color...'): color = QtGui.QColorDialog.getColor(QtGui.QColor(), self) if not color.isValid(): return openTag = openTag % color.name() editor.addHtmlTag(openTag, closeTag) def inlineLinkTagPrompt(self): """Prompt user to pick node for inline internal link""" self.linkTagEditor = self.focusWidgetWithAttr('addHtmlLinkTag') if not self.linkTagEditor: return view = self.leftTabs.currentWidget() view.noSelectClickCallback = self.addInlineLinkTag globalref.setStatusBar(_('Click on tree node for link destination'), 0, True) for action in self.actions.values(): action.setEnabled(False) def addInlineLinkTag(self, item): """Add link to item to active data editor""" globalref.setStatusBar('') self.linkTagEditor.addHtmlLinkTag(item.refFieldText(), item.title()) self.linkTagEditor = None for action in self.actions.values(): action.setEnabled(True) self.updateCmdAvail() def focusLeftView(self): """Focus active view in the left pane""" self.leftTabs.currentWidget().setFocus() def treeSelectPrev(self): """Select the pevious tree item""" self.doc.selection.treeSelectPrev() def treeSelectNext(self): """Select the next tree item""" self.doc.selection.treeSelectNext() def treePrevSibling(self): """Select the pevious sibling item""" self.doc.selection.treePrevSibling() def treeNextSibling(self): """Select the next sibling item""" self.doc.selection.treeNextSibling() def treeSelectParent(self): """Select the parnt item""" self.doc.selection.treeSelectParent() def treeOpenItem(self): """Set selection to open""" self.doc.selection.treeOpenItem() def treeCloseItem(self): """Set selection to closed""" self.doc.selection.treeCloseItem() def treePageUp(self): """Page up in the left view""" view = self.leftTabs.currentWidget() view.keyPressEvent(QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_PageUp, QtCore.Qt.NoModifier)) def treePageDown(self): """Page down in the left view""" view = self.leftTabs.currentWidget() view.keyPressEvent(QtGui.QKeyEvent(QtCore.QEvent.KeyPress, QtCore.Qt.Key_PageDown, QtCore.Qt.NoModifier)) def treeIncremSearch(self): """Begin an incremental title search""" leftView = self.leftTabs.currentWidget() leftView.setFocus() leftView.treeIncremSearch() def treeIncremNext(self): """Go to next item in title search (incremental)""" self.leftTabs.currentWidget().treeIncremNext() def treeIncremPrev(self): """Go to previous item in title search (incremental)""" self.leftTabs.currentWidget().treeIncremPrev() def rightChildPageUp(self): """Page up the right-hand child view""" self.rightTabs.currentWidget().widget(1).scrollPage(-1) def rightChildPageDown(self): """Page down the right-hand child view""" self.rightTabs.currentWidget().widget(1).scrollPage(1) def rightParentPageUp(self): """Page up the right-hand parent view""" self.rightTabs.currentWidget().widget(0).scrollPage(-1) def rightParentPageDown(self): """Page down the right-hand parent view""" self.rightTabs.currentWidget().widget(0).scrollPage(1) def closeEvent(self, event): """Ask for save if doc modified""" if globalref.treeControl.savePrompt(True): globalref.treeControl.recentFiles.writeList() toolbarPos = base64.b64encode(self.saveState().data()) globalref.options.changeData('ToolbarPosition', toolbarPos, True) if globalref.options.boolData('SaveWindowGeom'): globalref.options.changeData('WindowXSize', self.width(), True) globalref.options.changeData('WindowYSize', self.height(), True) globalref.options.changeData('WindowXPos', self.geometry().x(), True) globalref.options.changeData('WindowYPos', self.geometry().y(), True) treeWidth = self.leftTabs.width() rightWidth = self.rightTabs.width() treePercent = int(treeWidth * 100.0 / (treeWidth + rightWidth)) globalref.options.changeData('TreeSplitPercent', treePercent, True) mainHeight, childHeight = self.dataOutSplit.sizes() outPercent = int(mainHeight * 100.0 / (mainHeight + childHeight)) outChildView = self.rightTabs.widget(0).widget(1) if outPercent == 100 and outChildView.oldViewHeight: outPercent = 100 - outChildView.oldViewHeight globalref.options.changeData('OutputSplitPercent', outPercent, True) mainHeight, childHeight = self.dataEditSplit.sizes() editPercent = int(mainHeight * 100.0 / (mainHeight + childHeight)) editChildView = self.rightTabs.widget(1).widget(1) if editPercent == 100 and editChildView.oldViewHeight: editPercent = 100 - editChildView.oldViewHeight globalref.options.changeData('EditorSplitPercent', editPercent, True) mainHeight, childHeight = self.titleListSplit.sizes() titlePercent = int(mainHeight * 100.0 / (mainHeight + childHeight)) globalref.options.changeData('TitleSplitPercent', titlePercent, True) tabNum = self.rightTabs.currentIndex() globalref.options.changeData('ActiveRightView', tabNum, True) globalref.options.writeChanges() globalref.treeControl.removeWin(self) # make clipboard data persistent and fix error message on windows if not globalref.treeControl.windowCount(): clip = QtGui.QApplication.clipboard() clipEvent = QtCore.QEvent(QtCore.QEvent.Clipboard) QtGui.QApplication.sendEvent(clip, clipEvent) event.accept() else: event.ignore() def dragEnterEvent(self, event): """Accept drags of files to main window""" if event.mimeData().hasUrls(): event.accept() def dropEvent(self, event): """Drop a file onto window""" fileList = event.mimeData().urls() if fileList and globalref.treeControl.savePrompt(): globalref.treeControl.openFile(unicode(fileList[0].toLocalFile()), False) def loadTypeSubMenu(self): """Update type select submenu with type names and check marks""" self.typeSubMenu.clear() if not self.doc.selection: return selectionTypes = self.doc.selection.formatNames() names = self.doc.treeFormats.nameList(True) usedShortcuts = [] for name in names: shortcutPos = 0 try: while name[shortcutPos] in usedShortcuts: shortcutPos += 1 usedShortcuts.append(name[shortcutPos]) text = u'%s&%s' % (name[:shortcutPos], name[shortcutPos:]) except IndexError: text = name action = self.typeSubMenu.addAction(text) action.setCheckable(True) if name in selectionTypes: action.setChecked(True) def setupShortcuts(self): """Add shortcuts from options to actions""" for name, action in self.actions.iteritems(): seq = globalref.options.strData(name, True) seq = '+'.join(seq.split()) # for legacy config files action.setShortcut(QtGui.QKeySequence(seq)) for name, shortcut in self.shortcuts.iteritems(): seq = globalref.options.strData(name, True) seq = '+'.join(seq.split()) # for legacy config files shortcut.setKey(QtGui.QKeySequence(seq)) # add shortcut to flyout Item Type menu setTypeKey = globalref.options.strData('DataSetItemType', True) setTypeKey = '+'.join(setTypeKey.split()) self.typeSubMenu.setTitle('%s (%s)' % (_('Set &Item Type'), setTypeKey)) def addActionIcons(self): """Add icons to actions for menus and toolbars""" for name, action in self.actions.iteritems(): icon = TreeMainWin.toolIcons.getIcon(name.lower()) if icon: action.setIcon(icon) def setupToolbars(self): """Add actions defined in options to toolbars""" for toolbar in self.toolbars: toolbar.hide() self.removeToolBar(toolbar) self.toolbars = [] numToolbars = globalref.options.intData('ToolbarQuantity', 0, optiondefaults.maxNumToolbars) iconSize = globalref.options.intData('ToolbarSize', 1, 128) for num in range(numToolbars): toolbar = self.addToolBar(_('Toolbar %d' % num)) toolbar.setObjectName('tb%d' % num) toolbar.setIconSize(QtCore.QSize(iconSize, iconSize)) self.toolbars.append(toolbar) commands = globalref.options.strData('Toolbar%d' % num, True) commandList = commands.split(',') for command in commandList: if command: try: toolbar.addAction(self.actions[command]) except KeyError: pass else: toolbar.addSeparator() def restoreToolbarPos(self): """Recall the positions of the toolbars""" toolbarPos = globalref.options.strData('ToolbarPosition', True) if toolbarPos: self.restoreState(base64.b64decode(toolbarPos)) def setupMenus(self): """Add menu and toolbar items""" fileMenu = self.menuBar().addMenu(_('&File')) self.parentPopup = QtGui.QMenu(self) self.childPopup = QtGui.QMenu(self) self.selectReqdActGrp = QtGui.QActionGroup(self) self.notRootActGrp = QtGui.QActionGroup(self) self.selParentsActGrp = QtGui.QActionGroup(self) fileNewAct = QtGui.QAction(_('&New...'), self) fileNewAct.setToolTip(_('New File')) fileNewAct.setStatusTip(_('Start a new file')) fileMenu.addAction(fileNewAct) self.actions['FileNew'] = fileNewAct self.connect(fileNewAct, QtCore.SIGNAL('triggered()'), self.fileNew) fileOpenAct = QtGui.QAction(_('&Open...'), self) fileOpenAct.setToolTip(_('Open File')) fileOpenAct.setStatusTip(_('Open a file from disk')) fileMenu.addAction(fileOpenAct) self.actions['FileOpen'] = fileOpenAct self.connect(fileOpenAct, QtCore.SIGNAL('triggered()'), self.fileOpen) fileOpenSampleAct = QtGui.QAction(_('Open Sa&mple...'), self) fileOpenSampleAct.setStatusTip(_('Open a sample template file')) fileMenu.addAction(fileOpenSampleAct) self.actions['FileOpenSample'] = fileOpenSampleAct self.connect(fileOpenSampleAct, QtCore.SIGNAL('triggered()'), self.fileOpenSample) fileMenu.addSeparator() fileSaveAct = QtGui.QAction(_('&Save'), self) fileSaveAct.setToolTip(_('Save File')) fileSaveAct.setStatusTip(_('Save changes to the current file')) fileMenu.addAction(fileSaveAct) self.actions['FileSave'] = fileSaveAct self.connect(fileSaveAct, QtCore.SIGNAL('triggered()'), self.fileSave) fileSaveAsAct = QtGui.QAction(_('Save &As...'), self) fileSaveAsAct.setStatusTip(_('Save the file with a new name')) fileMenu.addAction(fileSaveAsAct) self.actions['FileSaveAs'] = fileSaveAsAct self.connect(fileSaveAsAct, QtCore.SIGNAL('triggered()'), self.fileSaveAs) fileExportAct = QtGui.QAction(_('&Export...'), self) fileExportAct.setStatusTip(_('Export the file as html, as a table '\ 'or as text')) fileMenu.addAction(fileExportAct) self.actions['FileExport'] = fileExportAct self.connect(fileExportAct, QtCore.SIGNAL('triggered()'), self.fileExport) fileMenu.addSeparator() filePrintOptAct = QtGui.QAction(_('P&rint Options...'), self) filePrintOptAct.setStatusTip(_('Set margins, page size and other '\ 'options for printing')) fileMenu.addAction(filePrintOptAct) self.actions['FilePrintOpt'] = filePrintOptAct self.connect(filePrintOptAct, QtCore.SIGNAL('triggered()'), self.printData.filePrintOpt) filePrintPreviewAct = QtGui.QAction(_('Print Pre&view...'), self) filePrintPreviewAct.setStatusTip(_('Show a preview of printing '\ 'results')) fileMenu.addAction(filePrintPreviewAct) self.actions['FilePrintPreview'] = filePrintPreviewAct self.connect(filePrintPreviewAct, QtCore.SIGNAL('triggered()'), self.printData.filePrintPreview) filePrintAct = QtGui.QAction(_('&Print...'), self) filePrintAct.setStatusTip(_('Print starting at the selected node')) fileMenu.addAction(filePrintAct) self.actions['FilePrint'] = filePrintAct self.connect(filePrintAct, QtCore.SIGNAL('triggered()'), self.printData.filePrint) fileMenu.addSeparator() self.recentFileSep = fileMenu.addSeparator() fileQuitAct = QtGui.QAction(_('&Quit'), self) fileQuitAct.setStatusTip(_('Exit the application')) fileMenu.addAction(fileQuitAct) self.actions['FileQuit'] = fileQuitAct self.connect(fileQuitAct, QtCore.SIGNAL('triggered()'), self.close) editMenu = self.menuBar().addMenu(_('&Edit')) editUndoAct = QtGui.QAction(_('&Undo'), self) editUndoAct.setStatusTip(_('Undo the previous action')) editMenu.addAction(editUndoAct) self.actions['EditUndo'] = editUndoAct self.connect(editUndoAct, QtCore.SIGNAL('triggered()'), self.editUndo) editRedoAct = QtGui.QAction(_('&Redo'), self) editRedoAct.setStatusTip(_('Redo the previous undo')) editMenu.addAction(editRedoAct) self.actions['EditRedo'] = editRedoAct self.connect(editRedoAct, QtCore.SIGNAL('triggered()'), self.editRedo) editMenu.addSeparator() editCutAct = QtGui.QAction(_('Cu&t'), self) editCutAct.setStatusTip(_('Cut the branch or text to the clipboard')) editMenu.addAction(editCutAct) self.parentPopup.addAction(editCutAct) self.childPopup.addAction(editCutAct) self.actions['EditCut'] = editCutAct self.connect(editCutAct, QtCore.SIGNAL('triggered()'), self.editCut) editCopyAct = QtGui.QAction(_('&Copy'), self) editCopyAct.setStatusTip(_('Copy the branch or text to the clipboard')) editMenu.addAction(editCopyAct) self.parentPopup.addAction(editCopyAct) self.childPopup.addAction(editCopyAct) self.actions['EditCopy'] = editCopyAct self.connect(editCopyAct, QtCore.SIGNAL('triggered()'), self.editCopy) editCopyTextAct = QtGui.QAction(_('Cop&y Title Text'), self) editCopyTextAct.setStatusTip(_('Copy node title text to the '\ 'clipboard')) editMenu.addAction(editCopyTextAct) self.actions['EditCopyText'] = editCopyTextAct self.connect(editCopyTextAct, QtCore.SIGNAL('triggered()'), self.editCopyText) editPasteAct = QtGui.QAction(_('&Paste'), self) editPasteAct.setStatusTip(_('Paste nodes or text from the clipboard')) editMenu.addAction(editPasteAct) self.parentPopup.addAction(editPasteAct) self.childPopup.addAction(editPasteAct) self.actions['EditPaste'] = editPasteAct self.connect(editPasteAct, QtCore.SIGNAL('triggered()'), self.editPaste) editPasteTextAct = QtGui.QAction(_('Pa&ste Text'), self) editPasteTextAct.setStatusTip(_('Paste text from the clipboard')) editMenu.addAction(editPasteTextAct) self.actions['EditPasteText'] = editPasteTextAct self.connect(editPasteTextAct, QtCore.SIGNAL('triggered()'), self.editPasteText) editRenameAct = QtGui.QAction(_('Re&name'), self) editRenameAct.setStatusTip(_('Rename the current tree entry')) editMenu.addAction(editRenameAct) self.parentPopup.addAction(editRenameAct) self.childPopup.addAction(editRenameAct) self.actions['EditRename'] = editRenameAct self.connect(editRenameAct, QtCore.SIGNAL('triggered()'), self.editRename) editMenu.addSeparator() self.parentPopup.addSeparator() self.childPopup.addSeparator() editInBeforeAct = QtGui.QAction(_('Insert Sibling &Before'), self.notRootActGrp) editInBeforeAct.setStatusTip(_('Insert new sibling before selection')) editMenu.addAction(editInBeforeAct) self.parentPopup.addAction(editInBeforeAct) self.childPopup.addAction(editInBeforeAct) self.actions['EditInsertBefore'] = editInBeforeAct self.connect(editInBeforeAct, QtCore.SIGNAL('triggered()'), self.editInBefore) editInAfterAct = QtGui.QAction(_('Insert Sibling &After'), self.notRootActGrp) editInAfterAct.setStatusTip(_('Insert new sibling after selection')) editMenu.addAction(editInAfterAct) self.parentPopup.addAction(editInAfterAct) self.childPopup.addAction(editInAfterAct) self.actions['EditInsertAfter'] = editInAfterAct self.connect(editInAfterAct, QtCore.SIGNAL('triggered()'), self.editInAfter) editAddChildAct = QtGui.QAction(_('Add C&hild'), self.selectReqdActGrp) editAddChildAct.setStatusTip(_('Add a new child to the selected '\ 'parent')) editMenu.addAction(editAddChildAct) self.parentPopup.addAction(editAddChildAct) self.childPopup.addAction(editAddChildAct) self.actions['EditAddChild'] = editAddChildAct self.connect(editAddChildAct, QtCore.SIGNAL('triggered()'), self.editAddChild) editMenu.addSeparator() self.parentPopup.addSeparator() self.childPopup.addSeparator() editDeleteAct = QtGui.QAction(_('&Delete Node'), self.notRootActGrp) editDeleteAct.setStatusTip(_('Delete the selected nodes')) editMenu.addAction(editDeleteAct) self.parentPopup.addAction(editDeleteAct) self.childPopup.addAction(editDeleteAct) self.actions['EditDelete'] = editDeleteAct self.connect(editDeleteAct, QtCore.SIGNAL('triggered()'), self.editDelete) editIndentAct = QtGui.QAction(_('&Indent Node'), self) editIndentAct.setStatusTip(_('Indent the selected nodes')) editMenu.addAction(editIndentAct) self.parentPopup.addAction(editIndentAct) self.childPopup.addAction(editIndentAct) self.actions['EditIndent'] = editIndentAct self.connect(editIndentAct, QtCore.SIGNAL('triggered()'), self.editIndent) editUnindentAct = QtGui.QAction(_('Unind&ent Node'), self) editUnindentAct.setStatusTip(_('Unindent the selected nodes')) editMenu.addAction(editUnindentAct) self.parentPopup.addAction(editUnindentAct) self.childPopup.addAction(editUnindentAct) self.actions['EditUnindent'] = editUnindentAct self.connect(editUnindentAct, QtCore.SIGNAL('triggered()'), self.editUnindent) editMenu.addSeparator() self.parentPopup.addSeparator() self.childPopup.addSeparator() editMoveUpAct = QtGui.QAction(_('&Move Up'), self) editMoveUpAct.setStatusTip(_('Move the selected nodes up')) editMenu.addAction(editMoveUpAct) self.parentPopup.addAction(editMoveUpAct) self.childPopup.addAction(editMoveUpAct) self.actions['EditMoveUp'] = editMoveUpAct self.connect(editMoveUpAct, QtCore.SIGNAL('triggered()'), self.editMoveUp) editMoveDownAct = QtGui.QAction(_('M&ove Down'), self) editMoveDownAct.setStatusTip(_('Move the selected nodes down')) editMenu.addAction(editMoveDownAct) self.parentPopup.addAction(editMoveDownAct) self.childPopup.addAction(editMoveDownAct) self.actions['EditMoveDown'] = editMoveDownAct self.connect(editMoveDownAct, QtCore.SIGNAL('triggered()'), self.editMoveDown) editMoveFirstAct = QtGui.QAction(_('Move &First'), self) editMoveFirstAct.setStatusTip(_('Move the selected nodes to be the '\ 'first children')) editMenu.addAction(editMoveFirstAct) self.actions['EditMoveFirst'] = editMoveFirstAct self.connect(editMoveFirstAct, QtCore.SIGNAL('triggered()'), self.editMoveFirst) editMoveLastAct = QtGui.QAction(_('Move &Last'), self) editMoveLastAct.setStatusTip(_('Move the selected nodes to be the '\ 'last children')) editMenu.addAction(editMoveLastAct) self.actions['EditMoveLast'] = editMoveLastAct self.connect(editMoveLastAct, QtCore.SIGNAL('triggered()'), self.editMoveLast) self.parentPopup.addSeparator() self.childPopup.addSeparator() viewMenu = self.menuBar().addMenu(_('&View')) viewPrevSelAct = QtGui.QAction(_('&Previous Selection'), self) viewPrevSelAct.setStatusTip(_('View the previous tree selection')) viewMenu.addAction(viewPrevSelAct) self.actions['ViewPreviousSelect'] = viewPrevSelAct self.connect(viewPrevSelAct, QtCore.SIGNAL('triggered()'), self.viewPrevSelect) viewNextSelAct = QtGui.QAction(_('&Next Selection'), self) viewNextSelAct.setStatusTip(_('View the next tree selection')) viewMenu.addAction(viewNextSelAct) self.actions['ViewNextSelect'] = viewNextSelAct self.connect(viewNextSelAct, QtCore.SIGNAL('triggered()'), self.viewNextSelect) viewMenu.addSeparator() viewLeftViewGrp = QtGui.QActionGroup(self) viewTreeAct = QtGui.QAction(_('Show &Tree View'), viewLeftViewGrp) viewTreeAct.setStatusTip(_('Show the tree in the right view')) viewTreeAct.setCheckable(True) viewMenu.addAction(viewTreeAct) self.actions['ViewTree'] = viewTreeAct viewFlatAct = QtGui.QAction(_('Show &Flat View'), viewLeftViewGrp) viewFlatAct.setStatusTip(_('Show a flat list in the right view')) viewFlatAct.setCheckable(True) viewMenu.addAction(viewFlatAct) self.actions['ViewFlat'] = viewFlatAct self.connect(viewLeftViewGrp, QtCore.SIGNAL('triggered(QAction*)'), self.viewLeftSelect) viewMenu.addSeparator() viewRightViewGrp = QtGui.QActionGroup(self) viewOutAct = QtGui.QAction(_('Show Data &Output'), viewRightViewGrp) viewOutAct.setStatusTip(_('Show data output in right view')) viewOutAct.setCheckable(True) viewMenu.addAction(viewOutAct) self.actions['ViewDataOutput'] = viewOutAct viewEditAct = QtGui.QAction(_('Show Data &Editor'), viewRightViewGrp) viewEditAct.setStatusTip(_('Show data editor in right view')) viewEditAct.setCheckable(True) viewMenu.addAction(viewEditAct) self.actions['ViewDataEdit'] = viewEditAct viewTitleAct = QtGui.QAction(_('Show Title &List'), viewRightViewGrp) viewTitleAct.setStatusTip(_('Show title list in right view')) viewTitleAct.setCheckable(True) viewMenu.addAction(viewTitleAct) self.actions['ViewTitleList'] = viewTitleAct self.connect(viewRightViewGrp, QtCore.SIGNAL('triggered(QAction*)'), self.viewRightSelect) viewMenu.addSeparator() viewChildAct = QtGui.QAction(_('Show &Child Pane'), self) viewChildAct.setStatusTip(_('Toggle splitting right-hand view to '\ 'show children')) viewChildAct.setCheckable(True) viewMenu.addAction(viewChildAct) self.actions['ViewShowChild'] = viewChildAct viewChildAct.setChecked(self.showItemChildren) self.connect(viewChildAct, QtCore.SIGNAL('toggled(bool)'), self.viewChildren) viewDescendAct = QtGui.QAction(_('Show Output &Descendants'), self) viewDescendAct.setStatusTip(_('Toggle showing descendants in output '\ 'view')) viewDescendAct.setCheckable(True) viewMenu.addAction(viewDescendAct) self.actions['ViewShowDescend'] = viewDescendAct viewDescendAct.setChecked(self.dataOutSplit.widget(1).showDescendants) self.connect(viewDescendAct, QtCore.SIGNAL('toggled(bool)'), self.viewDescendants) viewMenu.addSeparator() viewStatusAct = QtGui.QAction(_('Show Status Bar'), self) viewStatusAct.setStatusTip(_('Toggle the display of the status bar')) viewStatusAct.setCheckable(True) viewMenu.addAction(viewStatusAct) self.actions['ViewStatusBar'] = viewStatusAct viewStatusAct.setChecked(self.showStatusBar) self.connect(viewStatusAct, QtCore.SIGNAL('toggled(bool)'), self.viewStatusBar) dataMenu = self.menuBar().addMenu(_('&Data')) self.typeSubMenu = QtGui.QMenu(self) dataMenu.addMenu(self.typeSubMenu) self.parentPopup.addMenu(self.typeSubMenu) self.childPopup.addMenu(self.typeSubMenu) self.connect(self.typeSubMenu, QtCore.SIGNAL('triggered(QAction*)'), self.dataTypeChange) self.connect(self.typeSubMenu, QtCore.SIGNAL('aboutToShow()'), self.loadTypeSubMenu) typeSubMenuShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeView.showTypeMenu) self.shortcuts['DataSetItemType'] = typeSubMenuShortcut dataSetAct = QtGui.QAction(_('&Set Descendant Types...'), self.selParentsActGrp) dataSetAct.setCheckable(True) dataSetAct.setStatusTip(_('Set data type of selections and children')) dataMenu.addAction(dataSetAct) self.actions['DataSetDescendType'] = dataSetAct self.connect(dataSetAct, QtCore.SIGNAL('triggered(bool)'), self.dataSet) dataConfigAct = QtGui.QAction(_('&Configure Data Types...'), self) dataConfigAct.setCheckable(True) dataConfigAct.\ setStatusTip(_('Modify data types, fields & output lines')) dataMenu.addAction(dataConfigAct) self.actions['DataConfigType'] = dataConfigAct self.connect(dataConfigAct, QtCore.SIGNAL('triggered(bool)'), self.dataConfig) dataCopyAct = QtGui.QAction(_('C&opy Types from File...'), self) dataCopyAct.setStatusTip(_('Copy the configuration from another '\ 'TreeLine file')) dataMenu.addAction(dataCopyAct) self.actions['DataCopyTypes'] = dataCopyAct self.connect(dataCopyAct, QtCore.SIGNAL('triggered()'), self.dataCopyTypes) dataMenu.addSeparator() self.parentPopup.addSeparator() self.childPopup.addSeparator() dataSortAct = QtGui.QAction(_('Sort &Nodes...'), self) dataSortAct.setCheckable(True) dataSortAct.setStatusTip(_('Open the dialog for sorting nodes')) dataMenu.addAction(dataSortAct) self.parentPopup.addAction(dataSortAct) self.childPopup.addAction(dataSortAct) self.actions['DataSort'] = dataSortAct self.connect(dataSortAct, QtCore.SIGNAL('triggered(bool)'), self.dataSort) dataEditFieldAct = QtGui.QAction(_('C&hange Selected Data...'), self.selectReqdActGrp) dataEditFieldAct.setStatusTip(_('Edit data values for all selected '\ 'nodes')) dataMenu.addAction(dataEditFieldAct) self.actions['DataChange'] = dataEditFieldAct self.connect(dataEditFieldAct, QtCore.SIGNAL('triggered()'), self.dataEditField) dataNumberingAct = QtGui.QAction(_('N&umbering...'), self.selectReqdActGrp) dataNumberingAct.setStatusTip(_('Add numbering to a given data field')) dataMenu.addAction(dataNumberingAct) self.actions['DataNumber'] = dataNumberingAct self.connect(dataNumberingAct, QtCore.SIGNAL('triggered()'), self.dataNumbering) dataMenu.addSeparator() dataFilterCondAct = QtGui.QAction(_('Con&ditional Filter...'), self) dataFilterCondAct.setStatusTip(_('Filter types with conditional'\ ' rules')) dataMenu.addAction(dataFilterCondAct) self.actions['DataFilterCond'] = dataFilterCondAct self.connect(dataFilterCondAct, QtCore.SIGNAL('triggered()'), self.dataFilterCond) dataFilterTextAct = QtGui.QAction(_('Te&xt Filter...'), self) dataFilterTextAct.setStatusTip(_('Filter with a text search string')) dataMenu.addAction(dataFilterTextAct) self.actions['DataFilterText'] = dataFilterTextAct self.connect(dataFilterTextAct, QtCore.SIGNAL('triggered()'), self.dataFilterText) dataFilterClearAct = QtGui.QAction(_('Cl&ear Filtering'), self) dataFilterClearAct.setStatusTip(_('Clear current filtering')) dataMenu.addAction(dataFilterClearAct) self.actions['DataFilterClear'] = dataFilterClearAct self.connect(dataFilterClearAct, QtCore.SIGNAL('triggered()'), self.dataFilterClear) dataMenu.addSeparator() dataAddCatAct = QtGui.QAction(_('&Add Category Level...'), self.selParentsActGrp) dataAddCatAct.setStatusTip(_('Insert category nodes above children')) dataMenu.addAction(dataAddCatAct) self.actions['DataCategoryAdd'] = dataAddCatAct self.connect(dataAddCatAct, QtCore.SIGNAL('triggered()'), self.dataAddCat) dataFlatCatAct = QtGui.QAction(_('&Flatten by Category'), self.selParentsActGrp) dataFlatCatAct.setStatusTip(_('Collapse data by merging fields')) dataMenu.addAction(dataFlatCatAct) self.actions['DataCategoryFlat'] = dataFlatCatAct self.connect(dataFlatCatAct, QtCore.SIGNAL('triggered()'), self.dataFlatCat) dataMenu.addSeparator() dataArrangeRefAct = QtGui.QAction(_('Arrange by &Reference...'), self.selParentsActGrp) dataArrangeRefAct.setStatusTip(_('Arrange data using parent '\ 'references')) dataMenu.addAction(dataArrangeRefAct) self.actions['DataRefArrange'] = dataArrangeRefAct self.connect(dataArrangeRefAct, QtCore.SIGNAL('triggered()'), self.dataArrangeRef) dataFlatRefAct = QtGui.QAction(_('F&latten by Reference...'), self.selParentsActGrp) dataFlatRefAct.setStatusTip(_('Collapse data after adding references')) dataMenu.addAction(dataFlatRefAct) self.actions['DataRefFlat'] = dataFlatRefAct self.connect(dataFlatRefAct, QtCore.SIGNAL('triggered()'), self.dataFlatRef) toolsMenu = self.menuBar().addMenu(_('&Tools')) toolsExpandAct = QtGui.QAction(_('&Expand Full Branch'), self.selParentsActGrp) toolsExpandAct.setStatusTip(_('Expand all children of selected node')) toolsMenu.addAction(toolsExpandAct) self.parentPopup.addAction(toolsExpandAct) self.actions['ToolsExpand'] = toolsExpandAct self.connect(toolsExpandAct, QtCore.SIGNAL('triggered()'), self.toolsExpand) toolsCollapseAct = QtGui.QAction(_('&Collapse Full Branch'), self.selParentsActGrp) toolsCollapseAct.setStatusTip(_('Collapse all children of the '\ 'selected node')) toolsMenu.addAction(toolsCollapseAct) self.parentPopup.addAction(toolsCollapseAct) self.actions['ToolsCollapse'] = toolsCollapseAct self.connect(toolsCollapseAct, QtCore.SIGNAL('triggered()'), self.toolsCollapse) toolsMenu.addSeparator() toolsFindAct = QtGui.QAction(_('&Find...'), self) toolsFindAct.setCheckable(True) toolsFindAct.setStatusTip(_('Find node matching text string')) toolsMenu.addAction(toolsFindAct) self.actions['ToolsFind'] = toolsFindAct self.connect(toolsFindAct, QtCore.SIGNAL('triggered(bool)'), self.toolsFind) toolsSpellCheckAct = QtGui.QAction(_('&Spell Check'), self.selectReqdActGrp) toolsSpellCheckAct.setStatusTip(_('Spell check the tree\'s text data')) toolsMenu.addAction(toolsSpellCheckAct) self.actions['ToolsSpellCheck'] = toolsSpellCheckAct self.connect(toolsSpellCheckAct, QtCore.SIGNAL('triggered()'), self.toolsSpellCheck) toolsRemXsltAct = QtGui.QAction(_('&Remove XSLT Ref'), self) toolsRemXsltAct.setStatusTip(_('Delete reference to XSLT export')) toolsMenu.addAction(toolsRemXsltAct) self.actions['ToolsRemXLST'] = toolsRemXsltAct self.connect(toolsRemXsltAct, QtCore.SIGNAL('triggered()'), self.toolsRemXslt) toolsMenu.addSeparator() toolsGenOptAct = QtGui.QAction(_('&General Options...'), self) toolsGenOptAct.setStatusTip(_('Set user preferences for all files')) toolsMenu.addAction(toolsGenOptAct) self.actions['ToolsGenOptions'] = toolsGenOptAct self.connect(toolsGenOptAct, QtCore.SIGNAL('triggered()'), self.toolsGenOpt) toolsFileOptAct = QtGui.QAction(_('File &Options...'), self) toolsFileOptAct.setStatusTip(_('Set preferences for this file')) toolsMenu.addAction(toolsFileOptAct) self.actions['ToolsFileOptions'] = toolsFileOptAct self.connect(toolsFileOptAct, QtCore.SIGNAL('triggered()'), self.toolsFileOpt) fontMenu = toolsMenu.addMenu(_('Set Fo&nts')) toolsTreeFontAct = QtGui.QAction(_('&Tree Font...'), self) toolsTreeFontAct.setToolTip(_('Set Tree Font')) toolsTreeFontAct.setStatusTip(_('Sets font for tree & flat views')) fontMenu.addAction(toolsTreeFontAct) self.actions['ToolsTreeFont'] = toolsTreeFontAct self.connect(toolsTreeFontAct, QtCore.SIGNAL('triggered()'), self.toolsTreeFont) toolsOutputFontAct = QtGui.QAction(_('&Data Output Font...'), self) toolsOutputFontAct.setToolTip(_('Set Data Output Font')) toolsOutputFontAct.setStatusTip(_('Sets font for output views')) fontMenu.addAction(toolsOutputFontAct) self.actions['ToolsOutputFont'] = toolsOutputFontAct self.connect(toolsOutputFontAct, QtCore.SIGNAL('triggered()'), self.toolsOutputFont) toolsEditFontAct = QtGui.QAction(_('&Editor Font...'), self) toolsEditFontAct.setToolTip(_('Set Editor Font')) toolsEditFontAct.setStatusTip(_('Sets font for edit views')) fontMenu.addAction(toolsEditFontAct) self.actions['ToolsEditFont'] = toolsEditFontAct self.connect(toolsEditFontAct, QtCore.SIGNAL('triggered()'), self.toolsEditFont) toolsShortcutAct = QtGui.QAction(_('Set &Keyboard Shortcuts...'), self) toolsShortcutAct.setStatusTip(_('Customize keyboard commands')) toolsMenu.addAction(toolsShortcutAct) self.actions['ToolsShortcuts'] = toolsShortcutAct self.connect(toolsShortcutAct, QtCore.SIGNAL('triggered()'), self.toolsShortcuts) toolsToolbarAct = QtGui.QAction(_('Custo&mize Toolbars...'), self) toolsToolbarAct.setStatusTip(_('Customize toolbar buttons')) toolsMenu.addAction(toolsToolbarAct) self.actions['ToolsCustomToolbar'] = toolsToolbarAct self.connect(toolsToolbarAct, QtCore.SIGNAL('triggered()'), self.toolsCustomToolbar) toolsMenu.addSeparator() toolsDfltColorAct = QtGui.QAction(_('&Use Default System Colors'), self) toolsDfltColorAct.setStatusTip(_('Use system colors, not custom')) toolsDfltColorAct.setCheckable(True) toolsMenu.addAction(toolsDfltColorAct) self.actions['ToolsDefaultColor'] = toolsDfltColorAct toolsDfltColorAct.setChecked(globalref.options. boolData('UseDefaultColors')) self.connect(toolsDfltColorAct, QtCore.SIGNAL('toggled(bool)'), self.toolsDefaultColor) toolsBkColorAct = QtGui.QAction(_('&Background Color...'), self) toolsBkColorAct.setStatusTip(_('Set view background color')) toolsMenu.addAction(toolsBkColorAct) self.actions['ToolsBackColor'] = toolsBkColorAct self.connect(toolsBkColorAct, QtCore.SIGNAL('triggered()'), self.toolsBkColor) toolsTxtColorAct = QtGui.QAction(_('&Text Color...'), self) toolsTxtColorAct.setStatusTip(_('Set view text color')) toolsMenu.addAction(toolsTxtColorAct) self.actions['ToolsTextColor'] = toolsTxtColorAct self.connect(toolsTxtColorAct, QtCore.SIGNAL('triggered()'), self.toolsTxtColor) self.winMenu = self.menuBar().addMenu(_('&Window')) winNewAct = QtGui.QAction(_('&New Window'), self) winNewAct.setStatusTip(_('Open a new window viewing the same file')) self.winMenu.addAction(winNewAct) self.actions['WinNewWindow'] = winNewAct self.connect(winNewAct, QtCore.SIGNAL('triggered()'), globalref.treeControl.newWindow) winCloseAct = QtGui.QAction(_('&Close Window'), self) winCloseAct.setStatusTip(_('Close the current window')) self.winMenu.addAction(winCloseAct) self.actions['WinCloseWindow'] = winCloseAct self.connect(winCloseAct, QtCore.SIGNAL('triggered()'), globalref.treeControl.closeWindow) winUpdateAct = QtGui.QAction(_('&Update Other Window'), self) winUpdateAct.setStatusTip(_('Update the contents of an alternate ' 'window')) self.winMenu.addAction(winUpdateAct) self.actions['WinUpdateWindow'] = winUpdateAct self.connect(winUpdateAct, QtCore.SIGNAL('triggered()'), globalref.treeControl.forceUpdateWindow) self.winMenu.addSeparator() helpMenu = self.menuBar().addMenu(_('&Help')) helpContentsAct = QtGui.QAction(_('&Help Contents'), self) helpContentsAct.setStatusTip(_('View information about using TreeLine')) helpMenu.addAction(helpContentsAct) self.actions['HelpContents'] = helpContentsAct self.connect(helpContentsAct, QtCore.SIGNAL('triggered()'), self.helpContents) helpReadMeAct = QtGui.QAction(_('&View Full ReadMe'), self) helpReadMeAct.setStatusTip(_('View the entire ReadMe file')) helpMenu.addAction(helpReadMeAct) self.actions['HelpFullReadMe'] = helpReadMeAct self.connect(helpReadMeAct, QtCore.SIGNAL('triggered()'), self.helpReadMe) helpAboutAct = QtGui.QAction(_('&About TreeLine'), self) helpAboutAct.setStatusTip(_('About this program')) helpMenu.addAction(helpAboutAct) self.actions['HelpAbout'] = helpAboutAct self.connect(helpAboutAct, QtCore.SIGNAL('triggered()'), self.helpAbout) helpPluginAct = QtGui.QAction(_('About &Plugins'), self) helpPluginAct.setStatusTip(_('Show loaded plugin modules')) helpMenu.addAction(helpPluginAct) self.actions['HelpPlugin'] = helpPluginAct self.connect(helpPluginAct, QtCore.SIGNAL('triggered()'), self.helpPlugin) self.pulldownMenuList = [fileMenu, editMenu, viewMenu, dataMenu, toolsMenu, helpMenu] self.tagSubMenu = QtGui.QMenu(_('&Add Font Tags'), self) self.addTagGroup = QtGui.QActionGroup(self) for name, text, tags in TreeMainWin.tagMenuEntries: action = QtGui.QAction(text, self.addTagGroup) self.tagSubMenu.addAction(action) self.addAction(action) self.actions[name] = action self.connect(action, QtCore.SIGNAL('triggered()'), self.addTextTag) treeFocusShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.focusLeftView) self.shortcuts['TreeFocusView'] = treeFocusShortcut treeSelectPrevShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeSelectPrev) self.shortcuts['TreeSelectPrev'] = treeSelectPrevShortcut treeSelectNextShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeSelectNext) self.shortcuts['TreeSelectNext'] = treeSelectNextShortcut treePrevSiblingShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treePrevSibling) self.shortcuts['TreePrevSibling'] = treePrevSiblingShortcut treeNextSiblingShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeNextSibling) self.shortcuts['TreeNextSibling'] = treeNextSiblingShortcut treeSelectParentShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeSelectParent) self.shortcuts['TreeSelectParent'] = treeSelectParentShortcut treeOpenItemShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeOpenItem) self.shortcuts['TreeOpenItem'] = treeOpenItemShortcut treeCloseItemShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeCloseItem) self.shortcuts['TreeCloseItem'] = treeCloseItemShortcut treePageUpShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treePageUp) self.shortcuts['TreePageUp'] = treePageUpShortcut treePageDownShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treePageDown) self.shortcuts['TreePageDown'] = treePageDownShortcut treeIncremSearchShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeIncremSearch) self.shortcuts['TreeIncremSearch'] = treeIncremSearchShortcut treeIncremNextShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeIncremNext) self.shortcuts['TreeIncremNext'] = treeIncremNextShortcut treeIncremPrevShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.treeIncremPrev) self.shortcuts['TreeIncremPrev'] = treeIncremPrevShortcut rightChildUpShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.rightChildPageUp) self.shortcuts['RightChildPageUp'] = rightChildUpShortcut rightChildDownShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.rightChildPageDown) self.shortcuts['RightChildPageDown'] = rightChildDownShortcut rightParentUpShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.rightParentPageUp) self.shortcuts['RightParentPageUp'] = rightParentUpShortcut rightParentDownShortcut = QtGui.QShortcut(QtGui.QKeySequence(), self, self.rightParentPageDown) self.shortcuts['RightParentPageDown'] = rightParentDownShortcut TreeLine/source/numbering.py0000644000175000017500000000602411651514477015120 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # numbering.py, provides functions to format numbering # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re def numSeries(start, stop, format): """Return a list of formatted numbers""" formatText = re.match('(.*)([1AaiI])(.*)', format) if not formatText: return [format] * (stop - start) # return format if can't number if formatText.group(2) == '1': getNum = lambda x,y: `x` elif formatText.group(2) in 'Ii': getNum = writeRoman else: getNum = writeAlpha upperCase = formatText.group(2).isupper() return ['%s%s%s' % (formatText.group(1), getNum(num, upperCase), formatText.group(3)) for num in range(start, stop)] def readAlpha(text): """Return integer based on A,B,C...AA,AB order""" textList = list(text.upper()) factor = 1 result = 0 while textList: result += factor * (ord(textList.pop()) - ord('A') + 1) factor *= 26 return result def writeAlpha(num, upperCase=1): """Return alpha string for integer num""" if num <= 0: return '' result = '' while num: digit = num % 26 if digit == 0: digit = 26 result = chr(digit - 1 + ord('A')) + result num = (num - digit) / 26 return upperCase and result or result.lower() readRomanDict = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000} def readRoman(text): """Return integer from text roman number, or 0 on error""" textList = list(text.upper()) result = 0 try: while textList: num = readRomanDict[textList.pop(0)] if textList and num < readRomanDict[textList[0]]: result -= num else: result += num except KeyError: return 0 return result writeRomanDict = {0: '', 1: 'I', 2: 'II', 3: 'III', 4: 'IV', 5: 'V', 6: 'VI', 7: 'VII', 8: 'VIII', 9: 'IX', 10: 'X', 20: 'XX', 30: 'XXX', 40: 'XL', 50: 'L', 60: 'LX', 70: 'LXX', 80: 'LXXX', 90: 'XC', 100: 'C', 200: 'CC', 300: 'CCC', 400: 'CD', 500: 'D', 600: 'DC', 700: 'DCC', 800: 'DCCC', 900: 'CM', 1000: 'M', 2000: 'MM', 3000: 'MMM'} def writeRoman(num, upperCase=1): """Return roman number text for integer num""" if num <= 0 or num >= 4000: return '' result = '' factor = 1000 while num: digit = num - (num % factor) result += writeRomanDict[digit] factor /= 10 num -= digit return upperCase and result or result.lower() TreeLine/source/globalref.py0000644000175000017500000000400611651514477015065 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # globalref.py, provides accessible global variables for TreeLine # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** treeControl = None docRef = None mainWin = None options = None treeIcons = None pluginInterface = None lang = '' localTextEncoding = 'utf-8' modPath = '' def dummyFunction(*args, **kw): """Placeholder for update callbacks, generally replaced with a real function reference""" pass updateViewAll = dummyFunction updateLeftView = dummyFunction updateRightView = dummyFunction updateViewSelection = dummyFunction updateViewItem = dummyFunction # called with item parameter updateViewMenuStat = dummyFunction setStatusBar = dummyFunction # called with text and optional duration (ms) focusTree = dummyFunction def updateRefs(win): """Update references based on current main window""" global mainWin mainWin = win global docRef docRef = mainWin.doc global pluginInterface pluginInterface = mainWin.pluginInterface global updateViewAll updateViewAll = mainWin.updateViews global updateLeftView updateLeftView = mainWin.updateLeftView global updateRightView updateRightView = mainWin.updateRightView global updateViewSelection updateViewSelection = mainWin.updateViewSelection global updateViewItem updateViewItem = mainWin.updateViewItem global updateViewMenuStat updateViewMenuStat = mainWin.updateCmdAvail global setStatusBar setStatusBar = mainWin.setStatusMsg global focusTree focusTree = mainWin.focusLeftView TreeLine/source/treedialogs.py0000644000175000017500000026311511651514477015442 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treedialogs.py, provides many dialog interfaces # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re import string import os import sys from PyQt4 import QtCore, QtGui try: from __main__ import templatePath except ImportError: templatePath = None import configdialog import optiondefaults import globalref stdWinFlags = QtCore.Qt.Dialog | QtCore.Qt.WindowTitleHint | \ QtCore.Qt.WindowSystemMenuHint class TypeSetDlg(QtGui.QDialog): """Dialog for setting items to a type""" def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) self.setAttribute(QtCore.Qt.WA_QuitOnClose, False) self.setWindowFlags(QtCore.Qt.Window) self.setWindowTitle(_('Set Data Types')) topLayout = QtGui.QVBoxLayout(self) self.groupBox = QtGui.QGroupBox() topLayout.addWidget(self.groupBox) innerLayout = QtGui.QGridLayout(self.groupBox) self.listBox = QtGui.QListWidget() innerLayout.addWidget(self.listBox, 0, 0, 4, 1) self.itemButton = QtGui.QPushButton(_('Set &Selection')) innerLayout.addWidget(self.itemButton, 0, 1) self.connect(self.itemButton, QtCore.SIGNAL('clicked()'), self.setItem) self.childButton = QtGui.QPushButton(_('Set S&election\'s Children')) innerLayout.addWidget(self.childButton, 1, 1) self.connect(self.childButton, QtCore.SIGNAL('clicked()'), self.setChild) self.descendButton = QtGui.QPushButton(_('Set All &Descendants')) innerLayout.addWidget(self.descendButton, 2, 1) self.connect(self.descendButton, QtCore.SIGNAL('clicked()'), self.setDescend) self.conditionButton = QtGui.QPushButton(_('Set Descendants '\ 'C&ondtionally...')) innerLayout.addWidget(self.conditionButton, 3, 1) self.connect(self.conditionButton, QtCore.SIGNAL('clicked()'), self.setCondition) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) closeButton = QtGui.QPushButton(_('&Close')) ctrlLayout.addWidget(closeButton) self.connect(closeButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('close()')) self.loadList() self.connect(self.listBox, QtCore.SIGNAL('itemSelectionChanged()'), self.updateDlg) self.listBox.setFocus() def loadList(self): """Load types into list box""" names = globalref.docRef.treeFormats.nameList(True) self.listBox.blockSignals(True) self.listBox.clear() self.listBox.addItems(names) self.listBox.setCurrentItem(self.listBox.item(0)) self.listBox.blockSignals(False) self.updateDlg() def updateDlg(self): """Update label text & button availability""" selTypes = globalref.docRef.selection.formatNames() childTypes = [] descendTypes = [] for item in globalref.docRef.selection: childTypes.extend(item.childTypes()) descendTypes.extend(item.descendTypes()) if len(globalref.docRef.selection) == 1: self.groupBox.setTitle(_('Selection = "%s"') % globalref.docRef.selection[0].title()) elif len(globalref.docRef.selection) > 1: self.groupBox.setTitle(_('Multiple Selection')) else: self.groupBox.setTitle(_('No Selection')) currentType = unicode(self.listBox.selectedItems()[0].text()) self.itemButton.setEnabled(len(selTypes) and (min(selTypes) != max(selTypes) or selTypes[0] != currentType)) self.childButton.setEnabled(len(childTypes) and (min(childTypes) != max(childTypes) or childTypes[0] != currentType)) descendEnable = len(descendTypes) and \ (min(descendTypes) != max(descendTypes) or \ descendTypes[0] != currentType) self.descendButton.setEnabled(descendEnable) self.conditionButton.setEnabled(descendEnable) def updateViews(self): """Update main views due to type setting changes""" globalref.docRef.modified = True globalref.updateViewAll() def setCurrentSel(self): """Set type list selection to current item on initial open""" selTypes = globalref.docRef.selection.formatNames() names = globalref.docRef.treeFormats.nameList(True) if len(selTypes) == 1: selectNum = names.index(selTypes[0]) self.listBox.setCurrentItem(self.listBox.item(selectNum)) def setItem(self): """Set types for selected item""" newFormat = unicode(self.listBox.selectedItems()[0].text()) globalref.docRef.undoStore.addTypeUndo(globalref.docRef.selection) for item in globalref.docRef.selection: item.changeType(newFormat) self.updateDlg() self.updateViews() def setChild(self): """Set types for selected item's children""" newFormat = unicode(self.listBox.selectedItems()[0].text()) childList = [] for parent in globalref.docRef.selection: for child in parent.childList: childList.append(child) globalref.docRef.undoStore.addTypeUndo(childList) for item in childList: item.changeType(newFormat) self.updateDlg() self.updateViews() def setDescend(self): """Set types for selected item's descendants""" newFormat = unicode(self.listBox.selectedItems()[0].text()) itemList = [] for parent in globalref.docRef.selection: for item in parent.descendantGenNoRoot(): itemList.append(item) globalref.docRef.undoStore.addTypeUndo(itemList) for item in itemList: item.changeType(newFormat) self.updateDlg() self.updateViews() def setCondition(self): """Set types for selected item's descendants conditionally""" typeList = [] for item in globalref.docRef.selection: for type in item.descendTypes(): if type not in typeList: typeList.append(type) if len(typeList) > 1: type, ok = QtGui.QInputDialog.getItem(self, _('Select Type'), _('Change from data type'), typeList, 0, False) if not ok: return type = unicode(type) else: type = typeList[0] format = globalref.docRef.treeFormats[type] dlg = configdialog.ConditionDlg(_('Set Descendants Conditionally'), format, self) if dlg.exec_() != QtGui.QDialog.Accepted: return cond = dlg.conditional() cond.setupFields(format) newFormat = unicode(self.listBox.selectedItems()[0].text()) itemList = [] for parent in globalref.docRef.selection: for item in parent.descendantGenNoRoot(): if item.formatName == type and cond.evaluate(item.data): itemList.append(item) globalref.docRef.undoStore.addTypeUndo(itemList) for item in itemList: item.changeType(newFormat) self.updateDlg() self.updateViews() def keyPressEvent(self, event): """Close on escape key""" if event.key() == QtCore.Qt.Key_Escape: self.close() else: QtGui.QDialog.keyPressEvent(self, event) def closeEvent(self, event): """Signal that view is closing""" self.hide() self.emit(QtCore.SIGNAL('viewClosed'), False) event.accept() class FieldSelectList(QtGui.QTreeWidget): """List view that shows direction of sort, changes with right-click or left/right arrows""" directionText = [_('descend', 'sort direction'), _('ascend', 'sort direction')] def __init__(self, parent=None): QtGui.QTreeWidget.__init__(self, parent) self.setHeaderLabels(['#', _('Fields'), _('Direction')]) self.setRootIsDecorated(False) self.setSortingEnabled(False) self.setSelectionMode(QtGui.QAbstractItemView.MultiSelection) self.resizeColumnToContents(0) self.connect(self, QtCore.SIGNAL('itemSelectionChanged()'), self.updateSelection) def loadFields(self, fieldList, selectList=None): """Load fields into list view, add sequence and direction info from selectList if given""" self.blockSignals(True) self.clear() self.blockSignals(False) for field in fieldList: QtGui.QTreeWidgetItem(self, ['', field]) self.resizeColumnToContents(1) if selectList: for field, dir in selectList: try: item = self.topLevelItem(fieldList.index(field)) self.setItemSelected(item, True) if not dir: self.toggleDirection(item) except ValueError: pass def selectList(self): """Return sort list, a list of tuples (field, direction)""" itemList = [self.topLevelItem(i) for i in range(self.topLevelItemCount())] fullList = [(unicode(item.text(0)), unicode(item.text(1)), unicode(item.text(2))) for item in itemList] selList = [grp for grp in fullList if grp[0]] selList.sort() return [(grp[1], FieldSelectList.directionText.index(grp[2])) for grp in selList] def updateSelection(self): """Update selected fields based on current selections""" selFields = [grp[0] for grp in self.selectList()] itemList = [self.topLevelItem(i) for i in range(self.topLevelItemCount())] for item in itemList: fieldName = unicode(item.text(1)) if self.isItemSelected(item): if fieldName not in selFields: item.setText(2, FieldSelectList.directionText[1]) selFields.append(fieldName) elif unicode(item.text(1)) in selFields: selFields.remove(fieldName) item.setText(2, '') for item in itemList: if self.isItemSelected(item): item.setText(0, str(selFields.index(unicode(item.text(1))) + 1)) else: item.setText(0, '') self.emit(QtCore.SIGNAL('selectChanged')) def toggleDirection(self, item): """Toggle sort direction for viewItem""" origText = unicode(item.text(2)) if origText: dirNum = FieldSelectList.directionText.index(origText) item.setText(2, FieldSelectList.directionText[not dirNum]) def mousePressEvent(self, event): """Signal right-click""" if event.button() == QtCore.Qt.RightButton: item = self.itemAt(event.pos()) if item: self.toggleDirection(item) else: QtGui.QTreeWidget.mousePressEvent(self, event) def keyPressEvent(self, event): """Signal direction change based on arrow click""" if event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Right): item = self.currentItem() if item: self.toggleDirection(item) else: QtGui.QTreeWidget.keyPressEvent(self, event) class SortDlg(QtGui.QDialog): """Dialog to control sorting options""" entireTree, selectBranch, selectChildren, selectSiblings = range(4) sortWhat = selectBranch allTypes, chooseTypes, titlesAscend, titlesDescend = range(4) method = allTypes def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) self.setAttribute(QtCore.Qt.WA_QuitOnClose, False) self.setWindowFlags(QtCore.Qt.Window) self.setWindowTitle(_('Sorting')) topLayout = QtGui.QGridLayout(self) whatGroup = QtGui.QGroupBox(_('What to Sort')) topLayout.addWidget(whatGroup, 0, 0) whatLayout = QtGui.QVBoxLayout(whatGroup) self.whatButtons = QtGui.QButtonGroup(self) treeButton = QtGui.QRadioButton(_('&Entire tree')) self.whatButtons.addButton(treeButton, SortDlg.entireTree) whatLayout.addWidget(treeButton) branchButton = QtGui.QRadioButton(_('Selected &branches')) self.whatButtons.addButton(branchButton, SortDlg.selectBranch) whatLayout.addWidget(branchButton) childButton = QtGui.QRadioButton(_('Selection\'s childre&n')) self.whatButtons.addButton(childButton, SortDlg.selectChildren) whatLayout.addWidget(childButton) siblingButton = QtGui.QRadioButton(_('Selection\'s &siblings')) self.whatButtons.addButton(siblingButton, SortDlg.selectSiblings) whatLayout.addWidget(siblingButton) self.whatButtons.button(SortDlg.sortWhat).setChecked(True) self.connect(self.whatButtons, QtCore.SIGNAL('buttonClicked(int)'), self.updateTypeList) methodGroup = QtGui.QGroupBox(_('Sort Method')) topLayout.addWidget(methodGroup, 0, 1) methodLayout = QtGui.QVBoxLayout(methodGroup) self.methodButtons = QtGui.QButtonGroup(self) allButton = QtGui.QRadioButton(_('All &types')) self.methodButtons.addButton(allButton, SortDlg.allTypes) methodLayout.addWidget(allButton) chooseButton = QtGui.QRadioButton(_('C&hoose types')) self.methodButtons.addButton(chooseButton, SortDlg.chooseTypes) methodLayout.addWidget(chooseButton) titleAscendButton = QtGui.QRadioButton(_('Titles only, ascendin&g')) self.methodButtons.addButton(titleAscendButton, SortDlg.titlesAscend) methodLayout.addWidget(titleAscendButton) titleDescendButton = QtGui.QRadioButton(_('Titles only, &descending')) self.methodButtons.addButton(titleDescendButton, SortDlg.titlesDescend) methodLayout.addWidget(titleDescendButton) self.methodButtons.button(SortDlg.method).setChecked(True) self.connect(self.methodButtons, QtCore.SIGNAL('buttonClicked(int)'), self.updateTypeList) self.typeGroup = QtGui.QGroupBox(_('Choose T&ype(s)')) topLayout.addWidget(self.typeGroup, 1, 0) typeLayout = QtGui.QVBoxLayout(self.typeGroup) self.typeListBox = QtGui.QListWidget() typeLayout.addWidget(self.typeListBox) self.typeListBox.setSelectionMode(QtGui.QAbstractItemView. ExtendedSelection) self.connect(self.typeListBox, QtCore.SIGNAL('itemSelectionChanged()'), self.updateFieldList) self.fieldGroup = QtGui.QGroupBox(_('Select &Fields in Order as '\ 'Sort Keys')) topLayout.addWidget(self.fieldGroup, 1, 1) fieldLayout = QtGui.QVBoxLayout(self.fieldGroup) self.fieldListBox = FieldSelectList() fieldLayout.addWidget(self.fieldListBox) self.connect(self.fieldListBox, QtCore.SIGNAL('selectChanged'), self.updateControls) self.statusLabel = QtGui.QLabel() self.statusLabel.setFrameStyle(QtGui.QFrame.Panel | QtGui.QFrame.Sunken) topLayout.addWidget(self.statusLabel, 2, 0, 1, 2) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout, 3, 0, 1, 2) ctrlLayout.addStretch(0) self.okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.connect(self.okButton, QtCore.SIGNAL('clicked()'), self.sortAndClose) self.applyButton = QtGui.QPushButton(_('&Apply')) ctrlLayout.addWidget(self.applyButton) self.connect(self.applyButton, QtCore.SIGNAL('clicked()'), self.sortNodes) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('close()')) self.updateDialog() def updateDialog(self): """Update dialog entries based on current node selection""" numChildren = sum([len(node.childList) for node in globalref.docRef.selection]) numSiblings = sum([node.parent and len(node.parent.childList) or 0 for node in globalref.docRef.selection]) self.whatButtons.button(SortDlg.selectBranch).setEnabled(numChildren) self.whatButtons.button(SortDlg.selectChildren).setEnabled(numChildren) if numChildren == 0 and self.whatButtons.checkedId() in \ (SortDlg.selectBranch, SortDlg.selectChildren): self.whatButtons.button(SortDlg.selectSiblings).setChecked(True) self.whatButtons.button(SortDlg.selectSiblings).setEnabled(numSiblings) if numSiblings == 0 and self.whatButtons.checkedId() == \ SortDlg.selectSiblings: if numChildren: self.whatButtons.button(SortDlg.selectChildren).\ setChecked(True) else: self.whatButtons.button(SortDlg.entireTree).setChecked(True) self.updateTypeList() def updateTypeList(self): """Update list of available types""" SortDlg.sortWhat = self.whatButtons.checkedId() SortDlg.method = self.methodButtons.checkedId() allTypes = [] if SortDlg.sortWhat in (SortDlg.entireTree, SortDlg.selectBranch): selectList = [globalref.docRef.root] if SortDlg.sortWhat == SortDlg.selectBranch: selectList = globalref.docRef.selection.uniqueBranches() for item in selectList: for typeName in item.descendTypes(): if typeName not in allTypes: allTypes.append(typeName) else: selectList = globalref.docRef.selection[:] if SortDlg.sortWhat == SortDlg.selectSiblings: selectList = [item.parent for item in selectList if item.parent] for parent in selectList: for child in parent.childList: if child.formatName not in allTypes: allTypes.append(child.formatName) allTypes.sort() if SortDlg.method == SortDlg.allTypes: selectedTypes = allTypes elif SortDlg.method == SortDlg.chooseTypes: selectedTypes = [unicode(item.text()) for item in self.typeListBox.selectedItems()] if not selectedTypes: selectedTypes = allTypes[:1] selectedTypes = [nodeType for nodeType in selectedTypes if nodeType in allTypes] else: selectedTypes = [] self.typeListBox.blockSignals(True) self.typeListBox.clear() for nodeType in allTypes: item = QtGui.QListWidgetItem(nodeType, self.typeListBox) if nodeType in selectedTypes: self.typeListBox.setCurrentItem(item) self.typeListBox.setItemSelected(item, True) self.typeListBox.blockSignals(False) self.typeGroup.setEnabled(SortDlg.method in (SortDlg.allTypes, SortDlg.chooseTypes)) self.updateFieldList() def updateFieldList(self): """Update list of available fields""" selectedTypes = [unicode(item.text()) for item in self.typeListBox.selectedItems()] if SortDlg.method == SortDlg.allTypes and \ len(selectedTypes) < self.typeListBox.count(): SortDlg.method = SortDlg.chooseTypes self.methodButtons.button(SortDlg.method).setChecked(True) commonFields = [] if selectedTypes: commonFields = globalref.docRef.treeFormats[selectedTypes.pop(0)].\ fieldNames() for nodeType in selectedTypes: typeFields = globalref.docRef.treeFormats[nodeType].\ fieldNames() for field in commonFields[:]: if field not in typeFields: commonFields.remove(field) oldSelList = self.fieldListBox.selectList() self.fieldListBox.loadFields(commonFields, oldSelList) self.fieldListBox.setEnabled(SortDlg.method in (SortDlg.allTypes, SortDlg.chooseTypes)) self.updateControls() def updateControls(self): """Update control availablitiy and status label""" selectList = self.fieldListBox.selectList() titleMethod = SortDlg.method in (SortDlg.titlesAscend, SortDlg.titlesDescend) if titleMethod: self.statusLabel.setText(_('Sorting by titles')) elif not self.typeListBox.selectedItems(): self.statusLabel.setText(_('Select types to sort')) elif not self.fieldListBox.topLevelItemCount(): self.statusLabel.setText(_('No common fields found in selected '\ 'types')) elif not selectList: self.statusLabel.setText(_('Select fields as sort keys')) else: self.statusLabel.setText(_('To change a field direction, use a'\ ' right mouse click or the '\ 'left/right keys')) self.okButton.setEnabled(len(selectList) or titleMethod) self.applyButton.setEnabled(len(selectList) or titleMethod) def sortNodes(self): """Sort with the current options""" selectedTypes = [] if SortDlg.method == SortDlg.titlesAscend: globalref.docRef.sortFields = [('', True)] elif SortDlg.method == SortDlg.titlesDescend: globalref.docRef.sortFields = [('', False)] else: globalref.docRef.sortFields = self.fieldListBox.selectList() selectedTypes = [unicode(item.text()) for item in self.typeListBox.selectedItems()] if len(selectedTypes) == self.typeListBox.count(): selectedTypes = [] selectItems = globalref.docRef.selection if SortDlg.sortWhat == SortDlg.entireTree: selectItems = [globalref.docRef.root] elif SortDlg.sortWhat == SortDlg.selectBranch: selectItems = globalref.docRef.selection.uniqueBranches() elif SortDlg.sortWhat == SortDlg.selectSiblings: selectItems = [item.parent for item in selectItems if item.parent] if SortDlg.sortWhat in (SortDlg.entireTree, SortDlg.selectBranch): undoList = [] for item in selectItems: undoList.extend([parent for parent in item.descendantGen() if parent.childList]) globalref.docRef.undoStore.addChildListUndo(undoList) if selectedTypes: for item in selectItems: item.sortTypeBranch(selectedTypes) else: for item in selectItems: item.sortBranch() else: globalref.docRef.undoStore.addChildListUndo(selectItems) if selectedTypes: for item in selectItems: item.sortTypeChildren(selectedTypes) else: for item in selectItems: item.sortChildren() globalref.docRef.modified = True globalref.updateViewAll() def sortAndClose(self): """Sort with the current options and close the dialog""" self.sortNodes() self.close() def closeEvent(self, event): """Signal that view is closing""" self.hide() self.emit(QtCore.SIGNAL('viewClosed'), False) event.accept() class FieldSelectDlg(QtGui.QDialog): """Dialog for selecting from a field list in order""" def __init__(self, fieldList, caption, label, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(caption) self.availFields = fieldList[:] self.selFields = [] topLayout = QtGui.QVBoxLayout(self) groupBox = QtGui.QGroupBox(label) topLayout.addWidget(groupBox) boxLayout = QtGui.QVBoxLayout(groupBox) self.listView = QtGui.QTreeWidget() boxLayout.addWidget(self.listView) self.listView.setHeaderLabels(['#', _('Fields')]) # if directional: # label = QtGui.QLabel(_('(Use a right mouse click or the '\ # 'left/right keys\nto change direction)')) # boxLayout.addWidget(label) self.listView.setRootIsDecorated(False) self.listView.setSortingEnabled(False) self.listView.setSelectionMode(QtGui.QAbstractItemView.MultiSelection) self.listView.resizeColumnToContents(0) self.listView.setFocus() self.connect(self.listView, QtCore.SIGNAL('itemSelectionChanged()'), self.updateSelFields) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) self.okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.setEnabled(False) self.connect(self.okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) self.loadFields() def loadFields(self): """Load fields into list view""" for field in self.availFields: QtGui.QTreeWidgetItem(self.listView, ['', field]) self.listView.resizeColumnToContents(1) def getSelList(self): """Return sort list of fields""" return self.selFields[:] def updateSelFields(self): """Update selected fields based on current selections""" itemList = [self.listView.topLevelItem(i) for i in range(self.listView.topLevelItemCount())] for item in itemList: if self.listView.isItemSelected(item): if unicode(item.text(1)) not in self.selFields: self.selFields.append(unicode(item.text(1))) elif unicode(item.text(1)) in self.selFields: self.selFields.remove(unicode(item.text(1))) for item in itemList: if self.listView.isItemSelected(item): item.setText(0, str(self.selFields.index(unicode(item.text(1))) + 1)) else: item.setText(0, '') self.okButton.setEnabled(len(self.selFields)) class EditFieldsDlg(QtGui.QDialog): """Dialog for editing all instances of the selected nodes field""" def __init__(self, fieldList, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Change Selection')) self.resultDict = {} topLayout = QtGui.QVBoxLayout(self) groupBox = QtGui.QGroupBox(_('&Field')) topLayout.addWidget(groupBox) boxLayout = QtGui.QVBoxLayout(groupBox) self.fieldBox = QtGui.QComboBox() boxLayout.addWidget(self.fieldBox) self.fieldBox.addItems(fieldList) self.connect(self.fieldBox, QtCore.SIGNAL('currentIndexChanged(const QString &)'), self.changeField) groupBox = QtGui.QGroupBox(_('&New Value')) topLayout.addWidget(groupBox) boxLayout = QtGui.QVBoxLayout(groupBox) self.editBox = QtGui.QLineEdit() boxLayout.addWidget(self.editBox) self.connect(self.editBox, QtCore.SIGNAL('textChanged(const QString &)'), self.updateText) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) self.okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.setEnabled(False) self.connect(self.okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) self.fieldBox.setFocus() def changeField(self, newField): """Update the value editor based on new field name""" self.editBox.blockSignals(True) self.editBox.setText(self.resultDict.get(unicode(newField), '')) self.editBox.blockSignals(False) def updateText(self, newText): """Update the stored values based on editor change""" self.resultDict[unicode(self.fieldBox.currentText())] = \ unicode(newText) self.okButton.setEnabled(True) class TemplateItem(object): """Helper class to store template paths and info""" nameExp = re.compile(r'(\d+)([a-zA-Z]+?)_(.+)') allLanguageName = _('ALL', 'all languages selection for templates') def __init__(self, path, fileName): self.fullPath = os.path.join(path, fileName) self.fileName = fileName self.number = sys.maxint self.langCode = '' noExtName = os.path.splitext(fileName)[0] match = TemplateItem.nameExp.match(noExtName) if match: self.number = int(match.group(1)) self.langCode = match.group(2) self.displayName = match.group(3) else: self.displayName = noExtName self.displayName = self.displayName.replace('_', ' ') def __cmp__(self, other): """Compare function for sorting""" return cmp(self.number, other.number) class TemplateDialog(QtGui.QDialog): """Dialog for selecting template files""" pathList = [] def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('New File')) self.fullTemplateList = [] self.filterTemplateList = [] topLayout = QtGui.QVBoxLayout(self) groupBox = QtGui.QGroupBox(_('&Select Template')) topLayout.addWidget(groupBox) boxLayout = QtGui.QGridLayout(groupBox) self.listBox = QtGui.QListWidget() boxLayout.addWidget(self.listBox, 0, 0, 1, 2) self.listBox.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) self.languageLabel = QtGui.QLabel(_('&Language filter')) boxLayout.addWidget(self.languageLabel, 1, 0) self.languageLabel.setAlignment(QtCore.Qt.AlignRight) self.languageCombo = QtGui.QComboBox() boxLayout.addWidget(self.languageCombo, 1, 1) self.languageLabel.setBuddy(self.languageCombo) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) if not TemplateDialog.pathList: self.setPath() self.readList() self.loadLanguages() self.loadListBox() self.connect(self.languageCombo, QtCore.SIGNAL('activated(const QString&)'), self.filterLanguages) def setPath(self): """Find path for templates""" TemplateDialog.pathList = [] if globalref.options.templatePath: TemplateDialog.pathList = [globalref.options.templatePath] paths = [templatePath, os.path.join(globalref.modPath, u'../templates/'), os.path.join(globalref.modPath, u'templates/')] for path in filter(None, paths): try: for name in os.listdir(path): if name.endswith(u'.trl'): TemplateDialog.pathList.append(path) return except OSError: pass def readList(self): """Read template names from directories""" for path in TemplateDialog.pathList: try: for name in os.listdir(path): if name.endswith(u'.trl') and name not in \ [item.fileName for item in self.fullTemplateList]: self.fullTemplateList.append(TemplateItem(path, name)) except OSError: print 'Warning - can not read template directory' defaultItem = TemplateItem('', '') defaultItem.number = -1 if globalref.lang and globalref.lang != 'C': defaultItem.langCode = globalref.lang[:2] else: defaultItem.langCode = 'en' defaultItem.displayName = _('Default - No template - Single line text') self.fullTemplateList.append(defaultItem) self.fullTemplateList.sort() self.filterTemplateList = self.fullTemplateList[:] def loadLanguages(self): """Populate language combo box""" availLanguages = set([item.langCode for item in self.fullTemplateList]) availLanguages = list(availLanguages) availLanguages.sort() self.languageCombo.addItems(availLanguages) if len(availLanguages) > 1: self.languageCombo.insertItem(0, TemplateItem.allLanguageName) self.languageCombo.setCurrentIndex(0) else: self.languageLabel.setEnabled(False) self.languageCombo.setEnabled(False) def filterLanguages(self, language): """Remove templates that don't match language""" lang = unicode(language) if lang == TemplateItem.allLanguageName: self.filterTemplateList = self.fullTemplateList[:] else: self.filterTemplateList = [item for item in self.fullTemplateList if item.langCode == lang] self.loadListBox() def loadListBox(self, language=''): """Load template names into list view""" self.listBox.clear() self.listBox.addItems([item.displayName for item in self.filterTemplateList]) self.listBox.setCurrentRow(0) def selectedPath(self): """Return the full path of the currently selected item, return '' for the default item""" item = self.filterTemplateList[self.listBox.currentRow()] if item.number < 0: return '' return item.fullPath class ExportDlg(QtGui.QDialog): """Dialog for selecting type of file export""" htmlType, dirTableType, dirPageType, xsltType, trlType, textType, \ tableType, xbelType, mozType, xmlType, odfType = range(11) exportType = htmlType entireTree, selectBranch, selectNode = range(3) exportWhat = entireTree includeRoot = False openOnly = False addHeader = False numColumns = 1 def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Export File')) topLayout = QtGui.QVBoxLayout(self) horizLayout = QtGui.QHBoxLayout() topLayout.addLayout(horizLayout) typeGroupBox = QtGui.QGroupBox(_('Export Type')) horizLayout.addWidget(typeGroupBox) typeLayout = QtGui.QVBoxLayout(typeGroupBox) self.typeButtons = QtGui.QButtonGroup(self) htmlButton = QtGui.QRadioButton(_('&HTML single file output')) self.typeButtons.addButton(htmlButton, ExportDlg.htmlType) typeLayout.addWidget(htmlButton) dirButton = QtGui.QRadioButton(_('HTML &directory tables')) self.typeButtons.addButton(dirButton, ExportDlg.dirTableType) typeLayout.addWidget(dirButton) dirPageButton = QtGui.QRadioButton(_('HTML directory &pages')) self.typeButtons.addButton(dirPageButton, ExportDlg.dirPageType) typeLayout.addWidget(dirPageButton) xsltButton = QtGui.QRadioButton(_('&XSLT output')) self.typeButtons.addButton(xsltButton, ExportDlg.xsltType) typeLayout.addWidget(xsltButton) trlButton = QtGui.QRadioButton(_('TreeLine &subtree')) self.typeButtons.addButton(trlButton, ExportDlg.trlType) typeLayout.addWidget(trlButton) textButton = QtGui.QRadioButton(_('&Tabbed title text')) self.typeButtons.addButton(textButton, ExportDlg.textType) typeLayout.addWidget(textButton) tableButton = QtGui.QRadioButton(_('T&able of node or child data')) self.typeButtons.addButton(tableButton, ExportDlg.tableType) typeLayout.addWidget(tableButton) xbelButton = QtGui.QRadioButton(_('XBEL boo&kmarks')) self.typeButtons.addButton(xbelButton, ExportDlg.xbelType) typeLayout.addWidget(xbelButton) mozButton = QtGui.QRadioButton(_('&Mozilla HTML bookmarks')) self.typeButtons.addButton(mozButton, ExportDlg.mozType) typeLayout.addWidget(mozButton) xmlButton = QtGui.QRadioButton(_('&Generic XML')) self.typeButtons.addButton(xmlButton, ExportDlg.xmlType) typeLayout.addWidget(xmlButton) odfButton = QtGui.QRadioButton(_('ODF Text Fo&rmat')) self.typeButtons.addButton(odfButton, ExportDlg.odfType) typeLayout.addWidget(odfButton) self.typeButtons.button(ExportDlg.exportType).setChecked(True) self.connect(self.typeButtons, QtCore.SIGNAL('buttonClicked(int)'), self.updateCmdAvail) rightLayout = QtGui.QVBoxLayout() horizLayout.addLayout(rightLayout) whatGroupBox = QtGui.QGroupBox(_('What to Export')) rightLayout.addWidget(whatGroupBox) whatLayout = QtGui.QVBoxLayout(whatGroupBox) self.whatButtons = QtGui.QButtonGroup(self) treeButton = QtGui.QRadioButton(_('&Entire tree')) self.whatButtons.addButton(treeButton, ExportDlg.entireTree) whatLayout.addWidget(treeButton) branchButton = QtGui.QRadioButton(_('Selected &branches')) self.whatButtons.addButton(branchButton, ExportDlg.selectBranch) whatLayout.addWidget(branchButton) nodeButton = QtGui.QRadioButton(_('Selected &nodes')) self.whatButtons.addButton(nodeButton, ExportDlg.selectNode) whatLayout.addWidget(nodeButton) self.whatButtons.button(ExportDlg.exportWhat).setChecked(True) self.connect(self.whatButtons, QtCore.SIGNAL('buttonClicked(int)'), self.updateCmdAvail) optionBox = QtGui.QGroupBox(_('Export Options')) rightLayout.addWidget(optionBox) optionLayout = QtGui.QVBoxLayout(optionBox) self.rootButton = QtGui.QCheckBox(_('&Include root node')) optionLayout.addWidget(self.rootButton) self.rootButton.setChecked(ExportDlg.includeRoot) self.openOnlyButton = QtGui.QCheckBox(_('Only o&pen node children')) optionLayout.addWidget(self.openOnlyButton) self.openOnlyButton.setChecked(ExportDlg.openOnly) self.headerButton = QtGui.QCheckBox(_('Include print header && '\ '&footer')) optionLayout.addWidget(self.headerButton) self.headerButton.setChecked(ExportDlg.addHeader) columnLayout = QtGui.QHBoxLayout() optionLayout.addLayout(columnLayout) self.numColSpin = QtGui.QSpinBox() columnLayout.addWidget(self.numColSpin) self.numColSpin.setRange(1, optiondefaults.maxNumCol) self.numColSpin.setMaximumWidth(40) self.numColSpin.setValue(ExportDlg.numColumns) self.colLabel = QtGui.QLabel(_('Co&lumns')) columnLayout.addWidget(self.colLabel) self.colLabel.setBuddy(self.numColSpin) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) self.updateCmdAvail() typeGroupBox.setFocus() def updateCmdAvail(self): """Update options available""" exportType = self.typeButtons.checkedId() exportWhat = self.whatButtons.checkedId() if exportType in (ExportDlg.htmlType, ExportDlg.xsltType, ExportDlg.textType, ExportDlg.odfType): self.rootButton.setEnabled(True) elif exportType == ExportDlg.tableType: self.rootButton.setChecked(False) self.rootButton.setEnabled(False) else: self.rootButton.setChecked(True) self.rootButton.setEnabled(False) if exportType in (ExportDlg.htmlType, ExportDlg.textType, ExportDlg.odfType): self.openOnlyButton.setEnabled(True) else: self.openOnlyButton.setChecked(False) self.openOnlyButton.setEnabled(False) if exportType in (ExportDlg.htmlType, ExportDlg.dirTableType): self.headerButton.setEnabled(True) else: self.headerButton.setChecked(False) self.headerButton.setEnabled(False) if exportType == ExportDlg.htmlType: self.colLabel.setEnabled(True) self.numColSpin.setEnabled(True) else: self.numColSpin.setValue(1) self.numColSpin.setEnabled(False) self.colLabel.setEnabled(False) if exportType == ExportDlg.tableType: self.whatButtons.button(ExportDlg.entireTree).setEnabled(False) if self.whatButtons.checkedId() == ExportDlg.entireTree: self.whatButtons.button(ExportDlg.selectBranch).\ setChecked(True) else: self.whatButtons.button(ExportDlg.entireTree).setEnabled(True) if exportType == ExportDlg.xsltType: self.whatButtons.button(ExportDlg.selectBranch).setEnabled(False) self.whatButtons.button(ExportDlg.entireTree).setChecked(True) else: self.whatButtons.button(ExportDlg.selectBranch).setEnabled(True) if exportType in (ExportDlg.dirTableType, ExportDlg.dirPageType, ExportDlg.xsltType): self.whatButtons.button(ExportDlg.selectNode).setEnabled(False) if self.whatButtons.checkedId() == ExportDlg.selectNode: self.whatButtons.button(ExportDlg.selectBranch).\ setChecked(True) else: self.whatButtons.button(ExportDlg.selectNode).setEnabled(True) if exportWhat == ExportDlg.selectNode: self.rootButton.setChecked(False) self.rootButton.setEnabled(False) self.openOnlyButton.setChecked(False) self.openOnlyButton.setEnabled(False) def accept(self): """Store results and store last-used before closing""" ExportDlg.exportType = self.typeButtons.checkedId() ExportDlg.exportWhat = self.whatButtons.checkedId() ExportDlg.includeRoot = self.rootButton.isChecked() ExportDlg.openOnly = self.openOnlyButton.isChecked() ExportDlg.addHeader = self.headerButton.isChecked() ExportDlg.numColumns = self.numColSpin.value() QtGui.QDialog.accept(self) class NumberingDlg(QtGui.QDialog): """Dialog for adding numbering to nodes""" outlineType, sectionType, singleType = range(3) outlineFormat = ['I.', 'A.', '1.', 'a)', '(1)', '(a)', '(i)'] sectionFormat = ['1', '.1', '.1'] singleFormat = ['1.'] def __init__(self, fieldList, maxLevels, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Data Numbering')) self.maxLevels = maxLevels self.currentStyle = NumberingDlg.outlineType self.currentFormat = [] topLayout = QtGui.QVBoxLayout(self) groupBox = QtGui.QGroupBox(_('&Number Field')) topLayout.addWidget(groupBox) boxLayout = QtGui.QVBoxLayout(groupBox) self.fieldBox = QtGui.QComboBox() boxLayout.addWidget(self.fieldBox) self.fieldBox.setEditable(True) self.fieldBox.addItems(fieldList) self.fieldBox.clearEditText() self.fieldBox.setFocus() self.existOnlyButton = QtGui.QCheckBox(_('Number only where field '\ 'already &exists')) boxLayout.addWidget(self.existOnlyButton) self.connect(self.fieldBox, QtCore.SIGNAL('editTextChanged(const QString&)'), self.updateField) groupBox = QtGui.QGroupBox(_('Root Node')) topLayout.addWidget(groupBox) boxLayout = QtGui.QVBoxLayout(groupBox) self.inclRootButton = QtGui.QCheckBox(_('&Include root node')) boxLayout.addWidget(self.inclRootButton) self.inclRootButton.setChecked(True) self.connect(self.inclRootButton, QtCore.SIGNAL('toggled(bool)'), self.updateRoot) groupBox = QtGui.QGroupBox(_('Number Style')) topLayout.addWidget(groupBox) boxLayout = QtGui.QVBoxLayout(groupBox) self.styleGroup = QtGui.QButtonGroup(self) button = QtGui.QRadioButton(_('Outline (&discrete numbers)')) boxLayout.addWidget(button) self.styleGroup.addButton(button, NumberingDlg.outlineType) button = QtGui.QRadioButton(_('&Section (append to parent number)')) boxLayout.addWidget(button) self.styleGroup.addButton(button, NumberingDlg.sectionType) button = QtGui.QRadioButton(_('Single &level (children only)')) boxLayout.addWidget(button) self.styleGroup.addButton(button, NumberingDlg.singleType) self.styleGroup.button(self.currentStyle).setChecked(True) self.connect(self.styleGroup, QtCore.SIGNAL('buttonClicked(int)'), self.updateStyle) groupBox = QtGui.QGroupBox(_('Number &Format')) topLayout.addWidget(groupBox) boxLayout = QtGui.QHBoxLayout(groupBox) self.formatEdit = QtGui.QLineEdit() boxLayout.addWidget(self.formatEdit) self.connect(self.formatEdit, QtCore.SIGNAL('textChanged(const QString&)'), self.updateFormat) boxLayout.addWidget(QtGui.QLabel(_('for Level'))) self.levelBox = QtGui.QSpinBox() boxLayout.addWidget(self.levelBox) self.connect(self.levelBox, QtCore.SIGNAL('valueChanged(int)'), self.updateLevel) groupBox = QtGui.QGroupBox(_('Initial N&umber')) topLayout.addWidget(groupBox) boxLayout = QtGui.QHBoxLayout(groupBox) boxLayout.addWidget(QtGui.QLabel(_('Start first level at number'))) self.startBox = QtGui.QSpinBox() boxLayout.addWidget(self.startBox) self.startBox.setRange(1, 1000000) self.loadFormat() ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) self.okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.okButton.setEnabled(False) self.connect(self.okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) def updateField(self, text): """Update OK button based on combo changes""" self.okButton.setEnabled(len(unicode(text).strip())) def updateRoot(self, on): """Update styles based on include root change""" if self.maxLevels <= 1: if not on: self.currentStyle = NumberingDlg.singleType self.styleGroup.button(self.currentStyle).setChecked(True) self.styleGroup.button(NumberingDlg.outlineType).\ setEnabled(False) self.styleGroup.button(NumberingDlg.sectionType).\ setEnabled(False) else: self.styleGroup.button(NumberingDlg.outlineType).\ setEnabled(True) self.styleGroup.button(NumberingDlg.sectionType).\ setEnabled(True) if self.currentStyle == NumberingDlg.singleType and on: self.currentStyle = NumberingDlg.outlineType self.styleGroup.button(NumberingDlg.outlineType).setChecked(True) self.loadFormat() def updateStyle(self, style): """Update dialog based on style selection change""" self.currentStyle = style if style == NumberingDlg.singleType: self.inclRootButton.setChecked(False) self.loadFormat() def loadFormat(self): """Load a default format and level numbers into dialog""" numLevels = self.maxLevels startLevel = 1 if self.inclRootButton.isChecked(): numLevels += 1 startLevel = 0 if self.currentStyle == NumberingDlg.singleType: self.currentFormat = NumberingDlg.singleFormat[:] numLevels = 1 elif self.currentStyle == NumberingDlg.outlineType: self.currentFormat = NumberingDlg.outlineFormat[:] else: self.currentFormat = NumberingDlg.sectionFormat[:] while len(self.currentFormat) < numLevels: self.currentFormat.extend(self.currentFormat[-2:]) self.currentFormat = self.currentFormat[:numLevels] self.levelBox.setMinimum(startLevel) self.levelBox.setMaximum(startLevel + numLevels - 1) self.levelBox.setValue(startLevel) self.updateLevel(startLevel) def updateLevel(self, levelNum): """Update dialog based on a level change""" self.formatEdit.blockSignals(True) self.formatEdit.setText(self.currentFormat[levelNum - self.levelBox.minimum()]) self.formatEdit.blockSignals(False) def updateFormat(self, text): """Update dialog based on a format string change""" self.currentFormat[self.levelBox.value() - self.levelBox.minimum()] = unicode(text).strip() def getField(self): """Return adjusted field name""" return unicode(self.fieldBox.currentText()).strip() def accept(self): """Check for acceptable field string before closing""" try: text = unicode(self.fieldBox.currentText()).strip() except UnicodeError: text = '' if not text.replace('_', '').isalnum(): QtGui.QMessageBox.warning(self, 'TreeLine', _('Illegal characters in field (only '\ 'alpa-numerics & underscores allowed)')) return return QtGui.QDialog.accept(self) def existOnly(self): """Return True if check box for only existing fields is checked""" return self.existOnlyButton.isChecked() def includeRoot(self): """Return True if root include box is checked""" return self.inclRootButton.isChecked() def startNumber(self): """Return value from start number box""" return self.startBox.value() class FindTextEntry(QtGui.QDialog): """Dialog for find string text entry""" def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) self.setAttribute(QtCore.Qt.WA_QuitOnClose, False) self.setWindowFlags(QtCore.Qt.Window) self.setWindowTitle(_('Find')) topLayout = QtGui.QVBoxLayout(self) label = QtGui.QLabel(_('Enter key words')) topLayout.addWidget(label) self.entry = QtGui.QLineEdit() topLayout.addWidget(self.entry) self.entry.setFocus() self.statusLabel = QtGui.QLabel() topLayout.addWidget(self.statusLabel) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) prevButton = QtGui.QPushButton(_('Find &Previous')) ctrlLayout.addWidget(prevButton) self.connect(prevButton, QtCore.SIGNAL('clicked()'), self.findPrev) nextButton = QtGui.QPushButton(_('Find &Next')) ctrlLayout.addWidget(nextButton) nextButton.setDefault(True) self.connect(nextButton, QtCore.SIGNAL('clicked()'), self.findNext) closeButton = QtGui.QPushButton(_('&Close')) ctrlLayout.addWidget(closeButton) self.connect(closeButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('close()')) def find(self, forward=True): """Find match in direction""" searchStr = unicode(self.entry.text()).strip() if searchStr: wordList = [text.lower() for text in searchStr.split()] leftView = globalref.mainWin.leftTabs.currentWidget() item = leftView.findText(wordList, forward) if item: rightSplitter = globalref.mainWin.rightTabs.currentWidget() rightView = rightSplitter.widget(0) if rightSplitter == globalref.mainWin.dataOutSplit and \ rightView.height(): rightView.highlightWords(wordList) self.statusLabel.setText('') else: self.statusLabel.setText(_('Text string not found')) def findNext(self): """Find next match""" self.find(True) def findPrev(self): """Find previous match""" self.find(False) def keyPressEvent(self, event): """Close on escape key""" if event.key() == QtCore.Qt.Key_Escape: self.close() QtGui.QDialog.keyPressEvent(self, event) def closeEvent(self, event): """Signal that view is closing""" self.hide() self.emit(QtCore.SIGNAL('viewClosed'), False) event.accept() class SpellCheckDlg(QtGui.QDialog): """Dialog for the spell check interface""" def __init__(self, spCheck, origSelect, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Spell Check')) self.spCheck = spCheck self.origSelect = origSelect self.item = None self.field = '' self.lineNum = 0 self.textLine = '' self.replaceAllDict = {} self.tmpIgnoreList = [] self.word = '' self.postion = 0 topLayout = QtGui.QHBoxLayout(self) leftLayout = QtGui.QVBoxLayout() topLayout.addLayout(leftLayout) wordBox = QtGui.QGroupBox(_('Not in Dictionary')) leftLayout.addWidget(wordBox) wordLayout = QtGui.QVBoxLayout(wordBox) label = QtGui.QLabel(_('Word:')) wordLayout.addWidget(label) self.wordEdit = QtGui.QLineEdit() wordLayout.addWidget(self.wordEdit) self.connect(self.wordEdit, QtCore.SIGNAL('textChanged(const QString&)'), self.updateFromWord) wordLayout.addSpacing(5) label = QtGui.QLabel(_('Context:')) wordLayout.addWidget(label) self.contextEdit = SpellContextEdit() wordLayout.addWidget(self.contextEdit) self.connect(self.contextEdit, QtCore.SIGNAL('textChanged()'), self.updateFromContext) suggestBox = QtGui.QGroupBox(_('Suggestions')) leftLayout.addWidget(suggestBox) suggestLayout = QtGui.QVBoxLayout(suggestBox) self.suggestList = QtGui.QListWidget() suggestLayout.addWidget(self.suggestList) self.connect(self.suggestList, QtCore.SIGNAL('itemDoubleClicked(QListWidgetItem*)'), self.replace) rightLayout = QtGui.QVBoxLayout() topLayout.addLayout(rightLayout) ignoreButton = QtGui.QPushButton(_('Ignor&e')) rightLayout.addWidget(ignoreButton) self.connect(ignoreButton, QtCore.SIGNAL('clicked()'), self.ignore) ignoreAllButton = QtGui.QPushButton(_('&Ignore All')) rightLayout.addWidget(ignoreAllButton) self.connect(ignoreAllButton, QtCore.SIGNAL('clicked()'), self.ignoreAll) rightLayout.addStretch() addButton = QtGui.QPushButton(_('&Add')) rightLayout.addWidget(addButton) self.connect(addButton, QtCore.SIGNAL('clicked()'), self.add) addLowerButton = QtGui.QPushButton(_('Add &Lowercase')) rightLayout.addWidget(addLowerButton) self.connect(addLowerButton, QtCore.SIGNAL('clicked()'), self.addLower) rightLayout.addStretch() replaceButton = QtGui.QPushButton(_('&Replace')) rightLayout.addWidget(replaceButton) self.connect(replaceButton, QtCore.SIGNAL('clicked()'), self.replace) self.replaceAllButton = QtGui.QPushButton(_('Re&place All')) rightLayout.addWidget(self.replaceAllButton) self.connect(self.replaceAllButton, QtCore.SIGNAL('clicked()'), self.replaceAll) rightLayout.addStretch() cancelButton = QtGui.QPushButton(_('&Cancel')) rightLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) self.widgetDisableList = [ignoreButton, ignoreAllButton, addButton, addLowerButton, self.suggestList] self.fullDisableList = self.widgetDisableList + \ [self.replaceAllButton, self.wordEdit] def startSpellCheck(self): """Initialize item generator, then check items, if results found, set words and return True""" self.lineGen = self.lineGenerator() try: self.lineGen.next() except StopIteration: return False return self.spellCheck() def continueSpellCheck(self): """Check lines, starting with current line, exit dialog if end of branes are reached""" if not self.spellCheck(): self.accept() def spellCheck(self): """Check lines, starting with current line, if results found, set words and return True""" while True: results = self.spCheck.checkLine(self.textLine, self.tmpIgnoreList) if results: self.word, self.position, suggestions = results[0] newWord = self.replaceAllDict.get(self.word, '') if newWord: self.textLine = self.replaceWord(newWord) self.changeItem(self.textLine) else: globalref.docRef.selection.changeSearchOpen([self.item]) self.setWord(suggestions) return True else: try: self.lineGen.next() except StopIteration: return False def lineGenerator(self): """Yield next line to be checked, also sets item, field and lineNum""" for parent in self.origSelect: for self.item in parent.descendantGen(): for self.field in self.item.nodeFormat().fieldNames(): text = self.item.data.get(self.field, '') if text: for self.lineNum, self.textLine in \ enumerate(text.split('\n')): self.tmpIgnoreList = [] yield self.textLine def setWord(self, suggestions): """Set dialog contents from the checked line and the spell check results""" self.wordEdit.blockSignals(True) self.wordEdit.setText(self.word) self.wordEdit.blockSignals(False) self.contextEdit.blockSignals(True) self.contextEdit.setPlainText(self.textLine) self.contextEdit.setSelection(self.position, self.position + len(self.word)) self.contextEdit.blockSignals(False) self.suggestList.clear() self.suggestList.addItems(suggestions) self.suggestList.setCurrentItem(self.suggestList.item(0)) for widget in self.fullDisableList: widget.setEnabled(True) def replaceWord(self, newWord): """Return textLine with word replaced to newWord""" return self.textLine[:self.position] + newWord + \ self.textLine[self.position+len(self.word):] def changeItem(self, newLine): """Replace current line in the current item""" globalref.docRef.undoStore.addDataUndo(self.item, True) textLines = self.item.data.get(self.field, '').split('\n') textLines[self.lineNum] = newLine self.item.data[self.field] = '\n'.join(textLines) globalref.docRef.modified = True globalref.updateViewAll() def ignore(self): """Set word to ignored and continue spell check""" self.tmpIgnoreList.append(self.word) self.continueSpellCheck() def ignoreAll(self): """Add to dictionary's ignore list and continue spell check""" self.spCheck.acceptWord(self.word) self.continueSpellCheck() def add(self): """Add to dictionary and continue spell check""" self.spCheck.addToDict(self.word, False) self.continueSpellCheck() def addLower(self): """Add to dictionary as lowercase and continue spell check""" self.spCheck.addToDict(self.word, True) self.continueSpellCheck() def replace(self): """Replace with current suggestion or contents from word or context edit boxes and continue spell check""" if self.widgetDisableList[0].isEnabled(): newWord = unicode(self.suggestList.currentItem().text()) self.textLine = self.replaceWord(newWord) else: self.textLine = unicode(self.contextEdit.toPlainText()) self.changeItem(self.textLine) self.continueSpellCheck() def replaceAll(self): """Replace with current suggestion (in future too) and continue spell check""" if self.widgetDisableList[0].isEnabled(): newWord = unicode(self.suggestList.currentItem().text()) else: newWord = unicode(self.wordEdit.text()) self.textLine = self.replaceWord(newWord) self.replaceAllDict[self.word] = newWord self.changeItem(self.textLine) self.continueSpellCheck() def updateFromWord(self): """Update dialog after word editor change""" for widget in self.widgetDisableList: widget.setEnabled(False) newWord = unicode(self.wordEdit.text()) self.suggestList.clearSelection() self.contextEdit.blockSignals(True) self.contextEdit.setPlainText(self.replaceWord(newWord)) self.contextEdit.setSelection(self.position, self.position + len(newWord)) self.contextEdit.blockSignals(False) def updateFromContext(self): """Update dialog after context editor change""" for widget in self.fullDisableList: widget.setEnabled(False) self.suggestList.clearSelection() class SpellContextEdit(QtGui.QTextEdit): """Editor for spell check word context""" def __init__(self, parent=None): QtGui.QTextEdit.__init__(self, parent) self.setTabChangesFocus(True) def sizeHint(self): """Set prefered size""" fontHeight = QtGui.QFontMetrics(self.currentFont()).lineSpacing() return QtCore.QSize(QtGui.QTextEdit.sizeHint(self).width(), fontHeight * 3) def setSelection(self, fromPos, toPos): """Select given range in first paragraph""" cursor = self.textCursor() cursor.setPosition(fromPos) cursor.setPosition(toPos, QtGui.QTextCursor.KeepAnchor) self.setTextCursor(cursor) self.ensureCursorVisible() class RadioChoiceDlg(QtGui.QDialog): """Dialog for choosing between a list of text items (radio buttons) choiceList contains tuples of item text and return values""" def __init__(self, caption, heading, choiceList, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowTitle(caption) topLayout = QtGui.QVBoxLayout(self) groupBox = QtGui.QGroupBox(heading) topLayout.addWidget(groupBox) groupLayout = QtGui.QVBoxLayout(groupBox) self.buttonGroup = QtGui.QButtonGroup(self) for text, value in choiceList: button = QtGui.QRadioButton(text) button.returnValue = value groupLayout.addWidget(button) self.buttonGroup.addButton(button) self.buttonGroup.buttons()[0].setChecked(True) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) groupBox.setFocus() def getResult(self): """Return value of selected button""" return self.buttonGroup.checkedButton().returnValue class PasswordEntry(QtGui.QDialog): """Dialog for password entry and optional verification""" def __init__(self, retype=True, parent=None): QtGui.QDialog.__init__(self, parent) self.password = '' self.saveIt = True self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Encrypted File Password')) topLayout = QtGui.QVBoxLayout(self) label = QtGui.QLabel(_('Type Password:')) topLayout.addWidget(label) self.editors = [QtGui.QLineEdit()] self.editors[0].setEchoMode(QtGui.QLineEdit.Password) topLayout.addWidget(self.editors[0]) if retype: label = QtGui.QLabel(_('Re-Type Password:')) topLayout.addWidget(label) self.editors.append(QtGui.QLineEdit()) self.editors[1].setEchoMode(QtGui.QLineEdit.Password) topLayout.addWidget(self.editors[1]) self.connect(self.editors[0], QtCore.SIGNAL('returnPressed()'), self.editors[1], QtCore.SLOT('setFocus()')) self.editors[0].setFocus() self.connect(self.editors[-1], QtCore.SIGNAL('returnPressed()'), self, QtCore.SLOT('accept()')) self.saveCheck = QtGui.QCheckBox(_('Remember password '\ 'during this session')) self.saveCheck.setChecked(True) topLayout.addWidget(self.saveCheck) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) okButton.setAutoDefault(False) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) cancelButton.setAutoDefault(False) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) def accept(self): """Store result and check for matching re-type before closing""" self.password = unicode(self.editors[0].text()) self.saveIt = self.saveCheck.isChecked() if not self.password: QtGui.QMessageBox.warning(self, 'TreeLine', _('Zero-length passwords are not permitted')) self.editors[0].setFocus() return if len(self.editors) > 1 and \ unicode(self.editors[1].text()) != self.password: QtGui.QMessageBox.warning(self, 'TreeLine', _('Re-typed password did not match')) self.editors[0].clear() self.editors[1].clear() self.editors[0].setFocus() return QtGui.QDialog.accept(self) class ShortcutDlg(QtGui.QDialog): """Dialog for keyboard shortcut editing""" capsRe = re.compile('[%s]' % string.uppercase) def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Keyboard Shortcuts')) topLayout = QtGui.QVBoxLayout(self) tabs = QtGui.QTabWidget() topLayout.addWidget(tabs) self.editList = [] menuScrollArea = self.setupScrollArea(optiondefaults.menuKeyBindList) tabs.addTab(menuScrollArea, _('&Menu Items')) otherScrollArea = self.setupScrollArea(optiondefaults.otherKeyBindList) tabs.addTab(otherScrollArea, _('&Non-menu Items')) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) defaultButton = QtGui.QPushButton(_('Restore Defaults')) ctrlLayout.addWidget(defaultButton) self.connect(defaultButton, QtCore.SIGNAL('clicked()'), self.restoreDefaults) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) def setupScrollArea(self, keyList): """Add label and edit widgets to a scrollArea and return it""" scrollArea = QtGui.QScrollArea() viewport = QtGui.QWidget() viewLayout = QtGui.QGridLayout(viewport) scrollArea.setWidget(viewport) scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) scrollArea.setWidgetResizable(True) for i, (cmd, default) in enumerate(keyList): text = optiondefaults.cmdTranslationDict[cmd] text = ShortcutDlg.capsRe.sub(' \g<0>', text).strip() label = QtGui.QLabel(text) viewLayout.addWidget(label, i, 0) edit = KeyLineEdit(cmd, self) viewLayout.addWidget(edit, i, 1) self.editList.append(edit) viewport.adjustSize() return scrollArea def restoreDefaults(self): """Set toolbars back to default settings""" for edit in self.editList: edit.loadKey(True) def accept(self): """Save changes to options and update actions before closing""" for edit in self.editList: edit.saveChange() globalref.options.writeChanges() QtGui.QDialog.accept(self) class KeyLineEdit(QtGui.QLineEdit): """Line editor for user key sequence entry""" blankText = ' ' * 8 def __init__(self, command, dialogRef, parent=None): QtGui.QLineEdit.__init__(self, parent) self.command = command self.dialogRef = dialogRef self.setReadOnly(True) self.modified = False self.key = None self.loadKey() def loadKey(self, defaultOnly=False): """Load key shortcut""" keyName = globalref.options.strData(self.command, True, defaultOnly) if keyName: keyName = '+'.join(keyName.split()) # for legacy config files self.key = QtGui.QKeySequence(keyName) self.setText(self.key.toString(QtGui.QKeySequence.NativeText)) else: self.key = None self.setText(KeyLineEdit.blankText) if defaultOnly: self.modified = True def clearKey(self): """Remove existing key""" if self.key: self.key = None self.modified = True self.setText(KeyLineEdit.blankText) self.selectAll() def saveChange(self): """Make changes to the option for this key""" if self.modified: keyText = '' if self.key: keyText = unicode(self.key.toString()) globalref.options.changeData(self.command, keyText, True) def keyPressEvent(self, event): """Capture keypesses""" if event.key() in (QtCore.Qt.Key_Shift, QtCore.Qt.Key_Control, QtCore.Qt.Key_Meta, QtCore.Qt.Key_Alt, QtCore.Qt.Key_AltGr, QtCore.Qt.Key_CapsLock, QtCore.Qt.Key_NumLock, QtCore.Qt.Key_ScrollLock): event.ignore() elif event.key() in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Escape): self.clearKey() event.accept() else: modifier = event.modifiers() if modifier & QtCore.Qt.KeypadModifier: modifier = modifier ^ QtCore.Qt.KeypadModifier key = QtGui.QKeySequence(event.key() + int(modifier)) if key != self.key: for editor in self.dialogRef.editList: if editor.key == key: cmd = ShortcutDlg.capsRe.sub(' \g<0>', editor.command).strip() text = _('Key %(key)s already used for "%(cmd)s"') % \ {'key': key.toString(QtGui.QKeySequence.NativeText), 'cmd': cmd} QtGui.QMessageBox.warning(self, 'TreeLine', text) event.accept() return self.key = key self.setText(key.toString(QtGui.QKeySequence.NativeText)) self.selectAll() self.modified = True event.accept() def contextMenuEvent(self, event): """Change to a context menu with a clear command""" menu = QtGui.QMenu(self) menu.addAction(_('Clear Key'), self.clearKey) menu.exec_(event.globalPos()) def mousePressEvent(self, event): """Capture mouse clicks to avoid selection loss""" event.accept() def mouseReleaseEvent(self, event): """Capture mouse clicks to avoid selection loss""" event.accept() def mouseMoveEvent(self, event): """Capture mouse movement to avoid selection loss""" event.accept() def focusInEvent(self, event): """Select contents when focussed""" self.selectAll() QtGui.QLineEdit.focusInEvent(self, event) class ToolbarDlg(QtGui.QDialog): """Dialog for editing toolbar contents""" separatorString = _('--Separator--') def __init__(self, updateFunction, toolIcons, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Customize Toolbars')) self.updateFunction = updateFunction self.toolIcons = toolIcons self.modified = False self.numToolbars = 0 self.availableCommands = [] self.toolbarLists = [] topLayout = QtGui.QVBoxLayout(self) gridLayout = QtGui.QGridLayout() topLayout.addLayout(gridLayout) sizeBox = QtGui.QGroupBox(_('Toolbar &Size')) gridLayout.addWidget(sizeBox, 0, 0, 1, 2) sizeLayout = QtGui.QVBoxLayout(sizeBox) self.sizeCombo = QtGui.QComboBox() sizeLayout.addWidget(self.sizeCombo) self.sizeCombo.addItems([_('Small Icons'), _('Large Icons')]) if globalref.options.intData('ToolbarSize', 1, 128) < 24: self.sizeCombo.setCurrentIndex(0) else: self.sizeCombo.setCurrentIndex(1) self.connect(self.sizeCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.setModified) numberBox = QtGui.QGroupBox(_('Toolbar Quantity')) gridLayout.addWidget(numberBox, 0, 2) numberLayout = QtGui.QHBoxLayout(numberBox) self.quantitySpin = QtGui.QSpinBox() numberLayout.addWidget(self.quantitySpin) self.quantitySpin.setRange(0, optiondefaults.maxNumToolbars) numberlabel = QtGui.QLabel(_('&Toolbars')) numberLayout.addWidget(numberlabel) numberlabel.setBuddy(self.quantitySpin) self.connect(self.quantitySpin, QtCore.SIGNAL('valueChanged(int)'), self.changeQuantity) availableBox = QtGui.QGroupBox(_('A&vailable Commands')) gridLayout.addWidget(availableBox, 1, 0) availableLayout = QtGui.QVBoxLayout(availableBox) menuCombo = QtGui.QComboBox() availableLayout.addWidget(menuCombo) menuList = [self.translatedCommand(cmd).split()[0] for cmd, dflt in optiondefaults.menuKeyBindList] uniqueList = [] for menu in menuList: if menu not in uniqueList: uniqueList.append(menu) uniqueList = ['%s %s' % (menu, _('Menu')) for menu in uniqueList] menuCombo.addItems(uniqueList) self.connect(menuCombo, QtCore.SIGNAL('currentIndexChanged(const QString&)'), self.updateAvailableCommands) self.availableListWidget = QtGui.QListWidget() availableLayout.addWidget(self.availableListWidget) buttonLayout = QtGui.QVBoxLayout() gridLayout.addLayout(buttonLayout, 1, 1) self.addButton = QtGui.QPushButton('>>') buttonLayout.addWidget(self.addButton) self.addButton.setMaximumWidth(self.addButton.sizeHint().height()) self.connect(self.addButton, QtCore.SIGNAL('clicked()'), self.addTool) self.removeButton = QtGui.QPushButton('<<') buttonLayout.addWidget(self.removeButton) self.removeButton.setMaximumWidth(self.removeButton.sizeHint(). height()) self.connect(self.removeButton, QtCore.SIGNAL('clicked()'), self.removeTool) toolbarBox = QtGui.QGroupBox(_('Tool&bar Commands')) gridLayout.addWidget(toolbarBox, 1, 2) toolbarLayout = QtGui.QVBoxLayout(toolbarBox) self.toolbarCombo = QtGui.QComboBox() toolbarLayout.addWidget(self.toolbarCombo) self.connect(self.toolbarCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.updateToolbarCommands) self.toolbarListWidget = QtGui.QListWidget() toolbarLayout.addWidget(self.toolbarListWidget) self.connect(self.toolbarListWidget, QtCore.SIGNAL('currentRowChanged(int)'), self.setButtonsAvailable) moveLayout = QtGui.QHBoxLayout() toolbarLayout.addLayout(moveLayout) self.moveUpButton = QtGui.QPushButton(_('Move &Up')) moveLayout.addWidget(self.moveUpButton) self.connect(self.moveUpButton, QtCore.SIGNAL('clicked()'), self.moveUp) self.moveDownButton = QtGui.QPushButton(_('Move &Down')) moveLayout.addWidget(self.moveDownButton) self.connect(self.moveDownButton, QtCore.SIGNAL('clicked()'), self.moveDown) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) defaultButton = QtGui.QPushButton(_('Restore Defaults')) ctrlLayout.addWidget(defaultButton) self.connect(defaultButton, QtCore.SIGNAL('clicked()'), self.restoreDefaults) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) self.applyButton = QtGui.QPushButton(_('&Apply')) ctrlLayout.addWidget(self.applyButton) self.connect(self.applyButton, QtCore.SIGNAL('clicked()'), self.applyChanges) self.applyButton.setEnabled(False) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) self.loadToolbars() self.updateAvailableCommands(menuCombo.currentText()) self.updateToolbarCombo() self.updateToolbarCommands(0) def translatedCommand(self, command): """Return command translated and with spaces added""" text = optiondefaults.cmdTranslationDict[command] return ShortcutDlg.capsRe.sub(' \g<0>', text).strip() def setModified(self): """Set modified flag and make apply available""" self.modified = True self.applyButton.setEnabled(True) def setButtonsAvailable(self): """Enable or disable buttons based on toolbar list state""" toolNum = 0 numCmds = 0 cmdNum = 0 if self.numToolbars: toolNum = self.toolbarCombo.currentIndex() numCmds = len(self.toolbarLists[toolNum]) if self.toolbarLists[toolNum]: cmdNum = self.toolbarListWidget.currentRow() self.addButton.setEnabled(self.numToolbars > 0) self.removeButton.setEnabled(self.numToolbars and numCmds) self.moveUpButton.setEnabled(self.numToolbars and numCmds > 1 and cmdNum > 0) self.moveDownButton.setEnabled(self.numToolbars and numCmds > 1 and cmdNum < numCmds - 1) def changeQuantity(self, num): """Change the toolbar quantity based on a spin box signal""" for i in range(self.numToolbars, num): globalref.options.addDefaultKey('Toolbar%d' % i) self.numToolbars = num while num > len(self.toolbarLists): self.toolbarLists.append([]) self.updateToolbarCombo() self.setModified() def updateAvailableCommands(self, menuName): """Fill in available command list for given menu""" menuName = unicode(menuName).split()[0] self.availableCommands = [] self.availableListWidget.clear() for command, dflt in optiondefaults.menuKeyBindList: translation = self.translatedCommand(command) if translation.startswith(menuName): icon = self.toolIcons.getIcon(command.lower()) if icon: self.availableCommands.append(command) QtGui.QListWidgetItem(icon, translation, self.availableListWidget) self.availableCommands.append('') # separator QtGui.QListWidgetItem(ToolbarDlg.separatorString, self.availableListWidget) self.availableListWidget.setCurrentRow(0) def loadToolbars(self, defaultOnly=False): """Load toolbar data from options""" self.numToolbars = globalref.options.intData('ToolbarQuantity', 0, optiondefaults.maxNumToolbars, defaultOnly) self.quantitySpin.blockSignals(True) self.quantitySpin.setValue(self.numToolbars) self.quantitySpin.blockSignals(False) self.toolbarLists = [globalref.options. strData('Toolbar%d' % num, True, defaultOnly). split(',') for num in range(self.numToolbars)] def updateToolbarCombo(self): """Fill in toolbar numbers for current toolbar quantity""" self.toolbarCombo.clear() if self.numToolbars: self.toolbarCombo.addItems(['Toolbar %d' % (num + 1) for num in range(self.numToolbars)]) else: self.toolbarListWidget.clear() self.setButtonsAvailable() def updateToolbarCommands(self, num): """Fill in toolbar commands for toolbar num""" self.toolbarListWidget.clear() if self.numToolbars == 0: return for command in self.toolbarLists[num]: if command: icon = self.toolIcons.getIcon(command.lower()) QtGui.QListWidgetItem(icon, self.translatedCommand(command), self.toolbarListWidget) else: # separator QtGui.QListWidgetItem(ToolbarDlg.separatorString, self.toolbarListWidget) if len(self.toolbarLists[num]): self.toolbarListWidget.setCurrentRow(0) self.setButtonsAvailable() def addTool(self): """Add selected command to toolbar""" toolNum = self.toolbarCombo.currentIndex() command = self.availableCommands[self.availableListWidget.currentRow()] if command: icon = self.toolIcons.getIcon(command.lower()) item = QtGui.QListWidgetItem(icon, self.translatedCommand(command)) else: item = QtGui.QListWidgetItem(ToolbarDlg.separatorString) if self.toolbarLists[toolNum]: pos = self.toolbarListWidget.currentRow() else: pos = -1 self.toolbarListWidget.insertItem(pos + 1, item) self.toolbarListWidget.setCurrentRow(pos + 1) self.toolbarListWidget.scrollToItem(item) self.toolbarLists[toolNum].insert(pos + 1, command) self.setModified() def removeTool(self): """Remove selected command from toolbar""" toolNum = self.toolbarCombo.currentIndex() pos = self.toolbarListWidget.currentRow() self.toolbarListWidget.takeItem(pos) del self.toolbarLists[toolNum][pos] if self.toolbarLists[toolNum]: if pos == len(self.toolbarLists[toolNum]): pos -= 1 self.toolbarListWidget.setCurrentRow(pos) self.setModified() def moveUp(self): """Raise selected command""" toolNum = self.toolbarCombo.currentIndex() pos = self.toolbarListWidget.currentRow() item = self.toolbarListWidget.takeItem(pos) self.toolbarListWidget.insertItem(pos - 1, item) self.toolbarListWidget.setCurrentRow(pos - 1) self.toolbarListWidget.scrollToItem(item) command = self.toolbarLists[toolNum].pop(pos) self.toolbarLists[toolNum].insert(pos - 1, command) self.setModified() def moveDown(self): """Lower selected command""" toolNum = self.toolbarCombo.currentIndex() pos = self.toolbarListWidget.currentRow() item = self.toolbarListWidget.takeItem(pos) self.toolbarListWidget.insertItem(pos + 1, item) self.toolbarListWidget.setCurrentRow(pos + 1) self.toolbarListWidget.scrollToItem(item) command = self.toolbarLists[toolNum].pop(pos) self.toolbarLists[toolNum].insert(pos + 1, command) self.setModified() def restoreDefaults(self): """Set toolbars back to default settings""" self.loadToolbars(True) self.updateToolbarCombo() self.updateToolbarCommands(0) self.setModified() def applyChanges(self): """Apply any changes from the dialog""" if self.sizeCombo.currentIndex() == 0: globalref.options.changeData('ToolbarSize', '16', True) else: globalref.options.changeData('ToolbarSize', '32', True) globalref.options.changeData('ToolbarQuantity', str(self.numToolbars), True) for num, toolbarList in enumerate(self.toolbarLists): globalref.options.changeData('Toolbar%d' % num, ','.join(toolbarList), True) globalref.options.writeChanges() self.updateFunction() self.modified = False self.applyButton.setEnabled(False) def accept(self): """Apply changes and close the dialog""" if self.modified: self.applyChanges() QtGui.QDialog.accept(self) class PluginListDlg(QtGui.QDialog): """Dialog for listing loaded plugins""" def __init__(self, plugins, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('TreeLine Plugins')) topLayout = QtGui.QVBoxLayout(self) label = QtGui.QLabel(_('Plugin Modules Loaded')) topLayout.addWidget(label) listBox = QtGui.QListWidget() listBox.setSelectionMode(QtGui.QAbstractItemView.NoSelection) listBox.setMinimumSize(250, 65) listBox.addItems(plugins) topLayout.addWidget(listBox) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) okButton.setAutoDefault(False) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) TreeLine/source/conditional.py0000644000175000017500000001452611651514477015443 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # conditional.py, provides a class to store comparison functions # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re class Conditional(object): """Stores & evaluates conditional comparisons for dictionary data""" parseRe = re.compile(r'((?:and)|(?:or)) (\S+) (.+?) ' r'(?:(? "value"'""" self.conditionList = [] self.formatName = '' # used only for evaluateType() for boolOper, fieldName, oper, value in \ Conditional.parseRe.findall('and ' + conditionStr): value = value.replace('\\"', '"').replace('\\\\', '\\') self.conditionList.append(ConditionLine(boolOper, fieldName, oper, value)) def evaluate(self, data): """Return the boolean result without checking format type""" result = True for condition in self.conditionList: result = condition.evalFunc(data, result) return result def evaluateType(self, item): """Return the boolean result with a type check""" if item.formatName == self.formatName: return self.evaluate(item.data) return False def conditionText(self): """Return the condition string for this conditional set""" return ' '.join([cond.conditionText() for cond in self.conditionList])[4:] def setupFields(self, nodeFormat): """Set fieldNames used in lines to field references""" for condition in self.conditionList[:]: condition.setupFields(nodeFormat) if not condition.field: self.conditionList.remove(condition) def __len__(self): """Return number of conditions for truth testing""" return len(self.conditionList) def __deepcopy__(self, memo): """Avoid problems with deepcopy of a ref to instance's own method""" return Conditional(self.conditionText()) class ConditionLine(object): """Stores & evaluates a portion of a conditional comparison""" def __init__(self, boolOper, fieldName, oper, value): self.boolOper = boolOper self.tmpFieldName = fieldName self.field = None self.oper = oper self.valueStr = value self.value = None functions = {'==': self.equalFunc, '<': self.lessFunc, '<=': self.lessEqualFunc, '>': self.greaterFunc, '>=': self.greaterEqualFunc, '!=': self.notEqualFunc, 'starts with': self.startsWithFunc, 'ends with': self.endsWithFunc, 'contains': self.containsFunc, 'True': self.trueFunc, 'False': self.falseFunc} self.compareFunc = functions[self.oper] if self.boolOper == 'and': self.evalFunc = self.andFunc else: self.evalFunc = self.orFunc def conditionText(self): """Return the text line for this condition""" value = self.valueStr.replace('\\', '\\\\').replace('"', '\\"') return '%s %s %s "%s"' % (self.boolOper, self.field.name, self.oper, value) def setupFields(self, nodeFormat): """Set fieldNames used to field references""" if not self.tmpFieldName: self.tmpFieldName = self.field.name self.field = nodeFormat.findField(self.tmpFieldName) if self.field: self.value = self.field.sortValue({self.field.name: self.valueStr}) self.tmpFieldName = '' def andFunc(self, data, prevResult=True): """Evaluates boolean combination""" return prevResult and self.compareFunc(data) def orFunc(self, data, prevResult=True): """Evaluates boolean combination""" return prevResult or self.compareFunc(data) def equalFunc(self, data): """Evaluates main condition (==)""" return self.field.sortValue(data) == \ self.field.adjustedCompareValue(self.value) def lessFunc(self, data): """Evaluates main condition (<)""" return self.field.sortValue(data) < \ self.field.adjustedCompareValue(self.value) def lessEqualFunc(self, data): """Evaluates main condition (<=)""" return self.field.sortValue(data) <= \ self.field.adjustedCompareValue(self.value) def greaterFunc(self, data): """Evaluates main condition (>)""" return self.field.sortValue(data) > \ self.field.adjustedCompareValue(self.value) def greaterEqualFunc(self, data): """Evaluates main condition (>=)""" return self.field.sortValue(data) >= \ self.field.adjustedCompareValue(self.value) def notEqualFunc(self, data): """Evaluates main condition (!=)""" return self.field.sortValue(data) != \ self.field.adjustedCompareValue(self.value) def startsWithFunc(self, data): """Evaluates main condition (starts with)""" return unicode(self.field.sortValue(data)).\ startswith(unicode(self.value)) def endsWithFunc(self, data): """Evaluates main condition (ends with)""" return unicode(self.field.sortValue(data)).\ endswith(unicode(self.value)) def containsFunc(self, data): """Evaluates main condition (contains)""" return unicode(self.field.sortValue(data)).\ find(unicode(self.value)) >= 0 def trueFunc(self, data): """Evaluates main condition (always true)""" return True def falseFunc(self, data): """Evaluates main condition (always false)""" return False TreeLine/source/printdata.py0000644000175000017500000003540311651514477015123 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # printdata.py, provides a class for printing # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** from PyQt4 import QtCore, QtGui import nodeformat import globalref import optiondefaults import output import printdialogs class PrintData(object): """Stores print data and main printing fuctions""" def __init__(self): # self.printer = QtGui.QPrinter(QtGui.QPrinter.HighResolution) self.printer = QtGui.QPrinter() try: pageSize = getattr(QtGui.QPrinter, globalref.options.strData('PrintPageSize', False)) except AttributeError: pageSize = QtGui.QPrinter.Letter self.printer.setPageSize(pageSize) if globalref.options.boolData('PrintLandscape'): self.printer.setOrientation(QtGui.QPrinter.Landscape) else: self.printer.setOrientation(QtGui.QPrinter.Portrait) self.printer.setFullPage(True) self.printFont = None # set by setPrintContent self.printList = [] self.pageActiveLevels = [] def getOutputFont(self): """Return the output view font, ignores use output font option""" return globalref.mainWin.dataOutSplit.widget(0).font() def getOptionPrintFont(self): """Return font set in option storage or None, ignores use output font option""" return globalref.mainWin.getFontFromOptions('Print') def setOptionPrintFont(self, font): """Set the print font in option storage""" globalref.mainWin.saveFontToOptions(font, 'Print') def filePrintOpt(self): """Set margins, page size and other options for printing""" dlg = printdialogs.PrintOptionsDialog(self, True, globalref.mainWin) if dlg.exec_() != QtGui.QDialog.Accepted: return def pageSizes(self): """Return a tuple of dpi, pageRect (page without margins) and availRect (page without margins and header/footer)""" dpi = min(self.printer.logicalDpiX(), self.printer.logicalDpiY()) minXMargin = (self.printer.paperRect().width() - self.printer.pageRect().width()) // 2 minYMargin = (self.printer.paperRect().height() - self.printer.pageRect().height()) // 2 xMargin = int(globalref.options.numData('HorizMargin', optiondefaults.minPrintMargin, optiondefaults.maxPrintMargin) * dpi) yMargin = int(globalref.options.numData('VertMargin', optiondefaults.minPrintMargin, optiondefaults.maxPrintMargin) * dpi) xMargin = max(xMargin, minXMargin) yMargin = max(yMargin, minYMargin) pageRect = QtCore.QRect(xMargin, yMargin, self.printer.width() - 2 * xMargin, self.printer.height() - 2 * yMargin) numCols = globalref.options.intData('PrintNumCols', 1, optiondefaults.maxNumCol) colSpacing = globalref.options.numData('PrintColSpace', 0.0, optiondefaults.maxPrintMargin) colSpacing = int(dpi * colSpacing) colWidth = (pageRect.width() - colSpacing * (numCols - 1)) // numCols lineHeight = QtGui.QFontMetrics(self.printFont, self.printer).\ lineSpacing() headerHt = globalref.docRef.fileInfoFormat.getHeaderFooter(True) and \ 2 * lineHeight or 0 footerHt = globalref.docRef.fileInfoFormat.getHeaderFooter(False) and \ 2 * lineHeight or 0 availRects = [] for colNum in range(numCols): colStart = xMargin + colNum * (colSpacing + colWidth) rect = QtCore.QRect(colStart, yMargin + headerHt, colWidth, pageRect.height() - headerHt - footerHt) availRects.append(rect) return (dpi, pageRect, availRects) def textHeight(self, text, width): """Returns height of rich text in printer setup""" # subtract one pixel for roundoff error doc = self.document(text, width - 1) layout = doc.documentLayout() layout.setPaintDevice(self.printer) return layout.documentSize().height() def document(self, text, width): """Return a QTextDocument for height calculation & painting""" doc = QtGui.QTextDocument() doc.setHtml(text) doc.setDefaultFont(self.printFont) frameFormat = doc.rootFrame().frameFormat() frameFormat.setBorder(0) frameFormat.setMargin(0) frameFormat.setPadding(0) doc.rootFrame().setFrameFormat(frameFormat) # TODO change to width setting only after Qt 4.2 doc.setPageSize(QtCore.QSizeF(width, width * 10000)) return doc def setPrintContent(self): """Add tree content to self.printList""" self.printFont = self.getOptionPrintFont() if not self.printFont or \ globalref.options.boolData('PrintUseOutputFont'): self.printFont = self.getOutputFont() dpi, pageRect, availRects = self.pageSizes() addBranches = True mode = globalref.options.strData('PrintWhat') if mode == 'tree': nodeList = [globalref.docRef.root] elif mode == 'node': nodeList = globalref.docRef.selection addBranches = False else: # branches nodeList = globalref.docRef.selection.uniqueBranches() includeRoot = globalref.options.boolData('PrintRoot') openOnly = globalref.options.boolData('PrintOpenOnly') indent = globalref.options.numData('PrintIndentOffset', 0.0, optiondefaults.maxPrintIndent) indent = int(dpi * indent) outGroup = output.OutputGroup() if addBranches: for node in nodeList: branch = node.outputItemList(includeRoot, openOnly) outGroup.extend(branch) else: outGroup.extend([node.outputItem() for node in nodeList]) outGroup.setHeights(self.textHeight, availRects[0].width(), indent) if globalref.options.boolData('PrintLines'): self.pageActiveLevels = [] else: self.pageActiveLevels = None firstChildAdjust = globalref.options.boolData('PrintKeepFirstChild') \ and 0.2 or 0.0 self.printList = outGroup.splitPages(availRects[0].height(), self.pageActiveLevels, firstChildAdjust) for group in self.printList: group.joinPrefixItems() group.addPrefix() if globalref.docRef.lineBreaks: group.addBreaks() numCols = len(availRects) maxPage = len(self.printList) // numCols if len(self.printList) % numCols: maxPage += 1 globalref.docRef.fileInfoItem.data[nodeformat.FileInfoFormat. numPagesFieldName] = repr(maxPage) def filePrintPreview(self): """Show a preview of the printing results""" QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) self.setPrintContent() numPages = int(globalref.docRef.fileInfoItem.data[nodeformat. FileInfoFormat. numPagesFieldName]) dlg = printdialogs.PrintPrevDlg(self, numPages, self.printer.paperRect(), self.printPage, globalref.mainWin) dlg.resize(globalref.options.intData('PrintPrevXSize', 10, 10000), globalref.options.intData('PrintPrevYSize', 10, 10000)) if globalref.options.boolData('SaveWindowGeom'): dlg.move(globalref.options.intData('PrintPrevXPos', 0, 10000), globalref.options.intData('PrintPrevYPos', 0, 10000)) QtGui.QApplication.restoreOverrideCursor() result = dlg.exec_() if globalref.options.boolData('SaveWindowGeom'): globalref.options.changeData('PrintPrevXSize', dlg.width(), True) globalref.options.changeData('PrintPrevYSize', dlg.height(), True) globalref.options.changeData('PrintPrevXPos', dlg.x(), True) globalref.options.changeData('PrintPrevYPos', dlg.y(), True) globalref.options.writeChanges() if result == QtGui.QDialog.Accepted: self.filePrint() def printPage(self, pageNum, painter): """Send pageNum to painter""" dpi, pageRect, availRects = self.pageSizes() numCols = len(availRects) painter.setFont(self.printFont) globalref.docRef.fileInfoItem.data[nodeformat.FileInfoFormat. pageNumFieldName] = repr(pageNum) addLines = globalref.options.boolData('PrintLines') indent = globalref.options.numData('PrintIndentOffset', 0.0, optiondefaults.maxPrintIndent) indent = int(dpi * indent) xLineDelta = indent // 2 yLineDelta = 0 if globalref.docRef.spaceBetween: yLineDelta = QtGui.QFontMetrics(self.printFont).ascent() linePos = QtGui.QFontMetrics(self.printFont).ascent() // 2 + 1 header = globalref.docRef.fileInfoFormat.getHeaderFooter(True) if header: doc = self.document(header, pageRect.width()) layout = doc.documentLayout() painter.save() painter.translate(pageRect.left(), pageRect.top()) layout.draw(painter, QtGui.QAbstractTextDocumentLayout.PaintContext()) painter.restore() for colNum, colRect in enumerate(availRects): listIndex = (pageNum - 1) * numCols + colNum if listIndex >= len(self.printList) or \ not self.printList[listIndex]: break yPos = colRect.top() lineStarts = [yPos] * (self.printList[listIndex][0].level + 1) for item in self.printList[listIndex]: xPos = colRect.left() + indent * item.level doc = self.document(''.join(item.textLines), colRect.right() - xPos) layout = doc.documentLayout() painter.save() painter.translate(xPos, yPos) layout.draw(painter, QtGui.QAbstractTextDocumentLayout.PaintContext()) painter.restore() if addLines: if item.level: painter.drawLine(xPos - xLineDelta, yPos + linePos, xPos - 2, yPos + linePos) if len(lineStarts) > item.level: yPrev = lineStarts[item.level] lineStarts = lineStarts[:item.level] else: yPrev = yPos - yLineDelta painter.drawLine(xPos - xLineDelta, yPrev, xPos - xLineDelta, yPos + linePos) lineStarts.append(yPos + linePos) else: lineStarts = [0] yPos += item.height if addLines and listIndex < len(self.pageActiveLevels): for level in self.pageActiveLevels[listIndex]: xPos = colRect.left() + indent * level - xLineDelta if len(lineStarts) > level: yStart = lineStarts[level] else: yStart = colRect.top() painter.drawLine(xPos, yStart, xPos, colRect.bottom()) footer = globalref.docRef.fileInfoFormat.getHeaderFooter(False) if footer: doc = self.document(footer, pageRect.width()) layout = doc.documentLayout() painter.save() lineHeight = QtGui.QFontMetrics(self.printFont, self.printer).\ lineSpacing() painter.translate(pageRect.left(), pageRect.bottom() - lineHeight) layout.draw(painter, QtGui.QAbstractTextDocumentLayout.PaintContext()) painter.restore() def filePrint(self): """Print file starting from selected item""" QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) self.setPrintContent() QtGui.QApplication.restoreOverrideCursor() printDlg = QtGui.QPrintDialog(self.printer, globalref.mainWin) maxPage = int(globalref.docRef.fileInfoItem.data[nodeformat. FileInfoFormat. numPagesFieldName]) printDlg.setMinMax(1, maxPage) if printDlg.exec_() != QtGui.QDialog.Accepted: return QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) self.setPrintContent() minPage = max(self.printer.fromPage(), 1) maxPage = int(globalref.docRef.fileInfoItem.data[nodeformat. FileInfoFormat. numPagesFieldName]) if 1 <= self.printer.toPage() < maxPage: maxPage = self.printer.toPage() if self.printer.pageOrder() == QtGui.QPrinter.FirstPageFirst: seq = range(minPage, maxPage + 1) else: seq = range(maxPage, minPage - 1, -1) painter = QtGui.QPainter() if not painter.begin(self.printer): QtGui.QApplication.restoreOverrideCursor() QtGui.QMessageBox.warning(globalref.mainWin, 'TreeLine', _('Error initializing printer')) return self.printPage(seq.pop(0), painter) for num in seq: self.printer.newPage() self.printPage(num, painter) painter.end() QtGui.QApplication.restoreOverrideCursor() TreeLine/source/configdialog.py0000644000175000017500000023314111651514477015561 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # configdialog.py, provides classes for the config dialogs # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re import copy from PyQt4 import QtCore, QtGui import treeformats import nodeformat import conditional import globalref import optiondefaults stdWinFlags = QtCore.Qt.Dialog | QtCore.Qt.WindowTitleHint | \ QtCore.Qt.WindowSystemMenuHint class ConfigDialog(QtGui.QDialog): """Base dialog for tree configuration changes""" treeFormats = None fileInfoFormat = None currentType = '' currentField = '' typeRenameDict = {} fieldRenameDict = {} def __init__(self, parent=None): QtGui.QDialog.__init__(self, parent) self.setAttribute(QtCore.Qt.WA_QuitOnClose, False) self.setWindowFlags(QtCore.Qt.Window) self.setWindowTitle(_('Configure Data Types')) self.formatModified = False self.prevPage = None topLayout = QtGui.QVBoxLayout(self) self.setLayout(topLayout) self.tabs = QtGui.QTabWidget() topLayout.addWidget(self.tabs) typeListPage = TypeListPage(self.setModified) self.tabs.addTab(typeListPage, _('T&ype List')) typeConfigPage = TypeConfigPage(self.setModified) self.tabs.addTab(typeConfigPage, _('&Type Config')) fieldListPage = FieldListPage(self.setModified) self.tabs.addTab(fieldListPage, _('Field &List')) fieldConfigPage = FieldConfigPage(self.setModified) self.tabs.addTab(fieldConfigPage, _('&Field Config')) outputPage = OutputPage(self.setModified) self.tabs.addTab(outputPage, _('&Output')) self.connect(self.tabs, QtCore.SIGNAL('currentChanged(int)'), self.updatePage) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) self.advancedButton = QtGui.QPushButton(_('&Show Advanced')) ctrlLayout.addWidget(self.advancedButton) self.advancedButton.setCheckable(True) self.connect(self.advancedButton, QtCore.SIGNAL('clicked(bool)'), self.toggleAdvanced) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self.applyAndClose) self.applyButton = QtGui.QPushButton(_('&Apply')) ctrlLayout.addWidget(self.applyButton) self.connect(self.applyButton, QtCore.SIGNAL('clicked()'), self.applyChanges) self.resetButton = QtGui.QPushButton(_('&Reset')) ctrlLayout.addWidget(self.resetButton) self.connect(self.resetButton, QtCore.SIGNAL('clicked()'), self.resetParam) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self.resetAndClose) self.resetParam(True) def resetParam(self, changeCurrent=False): """Reset type formats and update page, called initially, by the reset button, and after a file change""" self.prevPage = None ConfigDialog.treeFormats = copy.deepcopy(globalref.docRef.treeFormats) ConfigDialog.fileInfoFormat = copy.deepcopy(globalref.docRef. fileInfoFormat) ConfigDialog.treeFormats.updateDerivedTypes() if changeCurrent or \ ConfigDialog.currentType not in ConfigDialog.treeFormats: ConfigDialog.currentType = globalref.docRef.selection.\ currentItem.formatName if changeCurrent or ConfigDialog.currentField not in \ ConfigDialog.treeFormats[ConfigDialog.currentType].fieldNames(): ConfigDialog.currentField = globalref.docRef.\ treeFormats[ConfigDialog.currentType].\ fieldList[0].name ConfigDialog.typeRenameDict = {} ConfigDialog.fieldRenameDict = {} self.updatePage() self.formatModified = False self.setControlAvailability() def resetCurrent(self): """Reset current type and field based on tree selection""" ConfigDialog.currentType = globalref.docRef.selection.\ currentItem.formatName ConfigDialog.currentField = globalref.docRef.\ treeFormats[ConfigDialog.currentType].\ fieldList[0].name page = self.tabs.currentWidget() page.updateContent() def updatePage(self): """Update new page and advanced button state when changing tabs""" if self.prevPage: self.prevPage.readChanges() page = self.tabs.currentWidget() self.advancedButton.setEnabled(len(page.advancedWidgets)) page.toggleAdvanced(self.advancedButton.isChecked()) page.updateContent() self.prevPage = page def toggleAdvanced(self, show): """Toggle state of advanced widgets in sub-dialogs""" if show: self.advancedButton.setText(_('Hide Advanced')) else: self.advancedButton.setText(_('Show Advanced')) page = self.tabs.currentWidget() page.toggleAdvanced(show) def setControlAvailability(self): """Set buttons to enabled or disabled based on modified status""" self.applyButton.setEnabled(self.formatModified) self.resetButton.setEnabled(self.formatModified) def setModified(self): """Change availability of controls for a modified format""" self.formatModified = True self.setControlAvailability() def applyChanges(self): """Apply copied format changes to main doc format""" self.tabs.currentWidget().readChanges() globalref.docRef.undoStore.addFormatUndo(globalref.docRef.treeFormats, globalref.docRef.fileInfoFormat, ConfigDialog.fieldRenameDict, ConfigDialog.typeRenameDict) globalref.docRef.treeFormats = ConfigDialog.treeFormats if ConfigDialog.typeRenameDict: globalref.docRef.treeFormats.\ renameFormats(ConfigDialog.typeRenameDict) if ConfigDialog.fieldRenameDict: globalref.docRef.treeFormats.\ renameFields(ConfigDialog.fieldRenameDict) if ConfigDialog.fileInfoFormat.name in ConfigDialog.treeFormats: globalref.docRef.fileInfoFormat = ConfigDialog.fileInfoFormat globalref.docRef.treeFormats.updateAllLineFields() globalref.docRef.treeFormats.updateAutoChoices() globalref.docRef.treeFormats.updateUniqueID(True) globalref.docRef.treeFormats.updateDerivedTypes() ConfigDialog.treeFormats = copy.deepcopy(globalref.docRef.treeFormats) ConfigDialog.fileInfoFormat = copy.deepcopy(globalref.docRef. fileInfoFormat) ConfigDialog.treeFormats.updateDerivedTypes() ConfigDialog.typeRenameDict = {} ConfigDialog.fieldRenameDict = {} globalref.docRef.modified = True globalref.updateViewAll() self.formatModified = False self.setControlAvailability() def applyAndClose(self): """Apply copied format changes to main doc format & close the dialog""" if self.formatModified: self.applyChanges() self.close() def resetAndClose(self): """Reset type formats and update page and close""" self.resetParam() self.close() def closeEvent(self, event): """Signal dialog closing""" if self.formatModified: event.ignore() else: self.hide() self.emit(QtCore.SIGNAL('dialogClosed'), False) event.accept() class ConfigPage(QtGui.QWidget): """Abstract base class for config dialog tabbed pages""" def __init__(self, setModifiedFunction, parent=None): QtGui.QWidget.__init__(self, parent) self.setModifiedFunction = setModifiedFunction self.advancedWidgets = [] def toggleAdvanced(self, show=True): """Toggle state of advanced widgets""" for widget in self.advancedWidgets: widget.setVisible(show) def readChanges(self): """Make changes to the format for each widget""" pass def changeCurrentType(self, index): """Change the current format type based on a signal""" self.readChanges() ConfigDialog.currentType = ConfigDialog.treeFormats.\ nameList(True)[index] currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] ConfigDialog.currentField = currentFormat.fieldNames()[0] self.updateContent() def changeCurrentField(self, index): """Change the current format field based on a signal""" self.readChanges() currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] ConfigDialog.currentField = currentFormat.fieldNames()[index] self.updateContent() def changeField(self, currentItem, prevItem): """Change the current format field based on a tree widget signal""" self.changeCurrentField(self.fieldListBox. indexOfTopLevelItem(currentItem)) class TypeListPage(ConfigPage): """Config page for a list of node types""" def __init__(self, setModifiedFunction, parent=None): ConfigPage.__init__(self, setModifiedFunction, parent) topLayout = QtGui.QVBoxLayout(self) box = QtGui.QGroupBox(_('Add or Remove Data Types')) topLayout.addWidget(box) horizLayout = QtGui.QHBoxLayout(box) self.listBox = QtGui.QListWidget() horizLayout.addWidget(self.listBox) self.connect(self.listBox, QtCore.SIGNAL('currentRowChanged(int)'), self.changeCurrentType) buttonLayout = QtGui.QVBoxLayout() horizLayout.addLayout(buttonLayout) newButton = QtGui.QPushButton(_('&New Type...')) buttonLayout.addWidget(newButton) self.connect(newButton, QtCore.SIGNAL('clicked()'), self.newType) copyButton = QtGui.QPushButton(_('Co&py Type...')) buttonLayout.addWidget(copyButton) self.connect(copyButton, QtCore.SIGNAL('clicked()'), self.copyType) renameButton = QtGui.QPushButton(_('R&ename Type...')) buttonLayout.addWidget(renameButton) self.connect(renameButton, QtCore.SIGNAL('clicked()'), self.renameType) deleteButton = QtGui.QPushButton(_('&Delete Type')) buttonLayout.addWidget(deleteButton) self.connect(deleteButton, QtCore.SIGNAL('clicked()'), self.deleteType) def updateContent(self): """Update page contents from current format settings""" names = ConfigDialog.treeFormats.nameList(True) self.listBox.blockSignals(True) self.listBox.clear() self.listBox.addItems(names) selectNum = names.index(ConfigDialog.currentType) self.listBox.setCurrentRow(selectNum) self.listBox.blockSignals(False) def newType(self): """Create a new type based on button signal""" dlg = FieldEntry(_('Add Type'), _('Enter new type name:'), '', ConfigDialog.treeFormats.nameList(True), self) if dlg.exec_() == QtGui.QDialog.Accepted: newFormat = nodeformat.NodeFormat(dlg.text, {}, treeformats.TreeFormats. fieldDefault) ConfigDialog.treeFormats[dlg.text] = newFormat ConfigDialog.currentType = dlg.text ConfigDialog.currentField = treeformats.TreeFormats.fieldDefault self.updateContent() self.setModifiedFunction() def copyType(self): """Copy selected type based on button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] allowDerive = not currentFormat.genericType dlg = FieldCopyEntry(ConfigDialog.currentType, ConfigDialog.treeFormats.nameList(True), allowDerive, self) if dlg.exec_() == QtGui.QDialog.Accepted: newFormat = copy.deepcopy(currentFormat) newFormat.name = dlg.text ConfigDialog.treeFormats[dlg.text] = newFormat if dlg.derived: newFormat.genericType = ConfigDialog.currentType ConfigDialog.treeFormats.updateDerivedTypes() ConfigDialog.currentType = dlg.text self.updateContent() self.setModifiedFunction() def renameType(self): """Rename the selected type based on button signal""" oldName = ConfigDialog.currentType currentFormat = ConfigDialog.treeFormats[oldName] dlg = FieldEntry(_('Rename Type'), _('Rename from "%s" to:') % oldName, oldName, ConfigDialog.treeFormats.nameList(True), self) if dlg.exec_() == QtGui.QDialog.Accepted: currentFormat.name = dlg.text del ConfigDialog.treeFormats[oldName] ConfigDialog.treeFormats[dlg.text] = currentFormat reverseDict = {} for old, new in ConfigDialog.typeRenameDict.items(): reverseDict[new] = old origName = reverseDict.get(oldName, oldName) ConfigDialog.typeRenameDict[origName] = dlg.text ConfigDialog.currentType = dlg.text for format in ConfigDialog.treeFormats.values(): if format.genericType == oldName: format.genericType = dlg.text self.updateContent() self.setModifiedFunction() def deleteType(self): """Delete the selected type based on button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] if globalref.docRef.root.usesType(ConfigDialog.currentType): QtGui.QMessageBox.warning(self, 'TreeLine', _('Cannot delete data type being used by nodes')) return del ConfigDialog.treeFormats[ConfigDialog.currentType] ConfigDialog.currentType = ConfigDialog.treeFormats.nameList(True)[0] currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] ConfigDialog.currentField = currentFormat.fieldList[0].name self.updateContent() self.setModifiedFunction() class TypeConfigPage(ConfigPage): """Config page for a node type""" noChildTypeName = _('[None]', 'no default child type') def __init__(self, setModifiedFunction, parent=None): ConfigPage.__init__(self, setModifiedFunction, parent) topLayout = QtGui.QGridLayout(self) typeBox = QtGui.QGroupBox(_('&Data Type')) topLayout.addWidget(typeBox, 0, 0) typeLayout = QtGui.QVBoxLayout(typeBox) self.typeCombo = QtGui.QComboBox() typeLayout.addWidget(self.typeCombo) self.connect(self.typeCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.changeCurrentType) iconBox = QtGui.QGroupBox(_('Icon')) topLayout.addWidget(iconBox, 0, 1) iconLayout = QtGui.QHBoxLayout(iconBox) self.iconLabel = QtGui.QLabel() iconLayout.addWidget(self.iconLabel) self.iconLabel.setAlignment(QtCore.Qt.AlignCenter) iconButton = QtGui.QPushButton(_('Change &Icon')) iconLayout.addWidget(iconButton) self.connect(iconButton, QtCore.SIGNAL('clicked()'), self.changeIcon) childBox = QtGui.QGroupBox(_('Default C&hild Type')) topLayout.addWidget(childBox, 1, 0) childLayout = QtGui.QVBoxLayout(childBox) self.childCombo = QtGui.QComboBox() childLayout.addWidget(self.childCombo) self.connect(self.childCombo, QtCore.SIGNAL('currentIndexChanged(int)'), setModifiedFunction) # Advanced refFieldBox = QtGui.QGroupBox(_('Link R&eference Field')) topLayout.addWidget(refFieldBox, 1, 1) self.advancedWidgets.append(refFieldBox) refFieldLayout = QtGui.QVBoxLayout(refFieldBox) self.refFieldCombo = QtGui.QComboBox() refFieldLayout.addWidget(self.refFieldCombo) self.connect(self.refFieldCombo, QtCore.SIGNAL('currentIndexChanged(int)'), setModifiedFunction) siblingBox = QtGui.QGroupBox(_('Sibling Text')) topLayout.addWidget(siblingBox, 2, 0, 2, 1) self.advancedWidgets.append(siblingBox) siblingLayout = QtGui.QVBoxLayout(siblingBox) siblingLayout.setSpacing(0) prefixLabel = QtGui.QLabel(_('&Prefix Tags')) siblingLayout.addWidget(prefixLabel) self.prefixEdit = QtGui.QLineEdit() siblingLayout.addWidget(self.prefixEdit) self.connect(self.prefixEdit, QtCore.SIGNAL('textEdited(const QString &)'), setModifiedFunction) prefixLabel.setBuddy(self.prefixEdit) siblingLayout.addSpacing(8) suffixLabel = QtGui.QLabel(_('Suffi&x Tags')) siblingLayout.addWidget(suffixLabel) self.suffixEdit = QtGui.QLineEdit() siblingLayout.addWidget(self.suffixEdit) self.connect(self.suffixEdit, QtCore.SIGNAL('textEdited(const QString &)'), setModifiedFunction) suffixLabel.setBuddy(self.suffixEdit) self.genericBox = QtGui.QGroupBox(_('Derived from &Generic Type')) topLayout.addWidget(self.genericBox, 2, 1) self.advancedWidgets.append(self.genericBox) genericLayout = QtGui.QVBoxLayout(self.genericBox) self.genericCombo = QtGui.QComboBox() genericLayout.addWidget(self.genericCombo) self.connect(self.genericCombo, QtCore.SIGNAL('currentIndexChanged(int)'), setModifiedFunction) self.connect(self.genericCombo, QtCore.SIGNAL('currentIndexChanged(const QString &)'), self.readGenericChange) self.conditionBox = QtGui.QGroupBox(_('Automatic Types')) topLayout.addWidget(self.conditionBox, 3, 1) self.advancedWidgets.append(self.conditionBox) conditionLayout = QtGui.QVBoxLayout(self.conditionBox) self.conditionButton = QtGui.QPushButton() conditionLayout.addWidget(self.conditionButton) self.conditionButton.setSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) self.connect(self.conditionButton, QtCore.SIGNAL('clicked()'), self.createCondition) topLayout.setRowStretch(4, 1) def updateContent(self): """Update page contents from current format settings""" typeNames = ConfigDialog.treeFormats.nameList(True) self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItems(typeNames) selectNum = typeNames.index(ConfigDialog.currentType) self.typeCombo.setCurrentIndex(selectNum) self.typeCombo.blockSignals(False) currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] icon = globalref.treeIcons.getIcon(currentFormat.iconName, True) if icon: self.iconLabel.setPixmap(icon.pixmap(16, 16)) else: self.iconLabel.setText(_('None', 'no icon set')) self.childCombo.blockSignals(True) self.childCombo.clear() self.childCombo.addItem(TypeConfigPage.noChildTypeName) self.childCombo.addItems(typeNames) try: childItem = typeNames.index(currentFormat.childType) + 1 except ValueError: childItem = 0 self.childCombo.setCurrentIndex(childItem) self.childCombo.blockSignals(False) self.refFieldCombo.blockSignals(True) self.refFieldCombo.clear() fieldNames = currentFormat.fieldNames() self.refFieldCombo.addItems(fieldNames) self.refFieldCombo.setCurrentIndex(fieldNames. index(currentFormat.refField.name)) self.refFieldCombo.blockSignals(False) self.prefixEdit.setText(currentFormat.sibPrefix) self.suffixEdit.setText(currentFormat.sibSuffix) typeNames.remove(ConfigDialog.currentType) availGenerics = [name for name in typeNames if not ConfigDialog.treeFormats[name].genericType] self.genericCombo.blockSignals(True) self.genericCombo.clear() self.genericCombo.addItem(TypeConfigPage.noChildTypeName) self.genericCombo.addItems(availGenerics) if currentFormat.genericType: genericNum = availGenerics.index(currentFormat.genericType) + 1 else: genericNum = 0 self.genericCombo.setCurrentIndex(genericNum) self.genericCombo.blockSignals(False) self.genericBox.setEnabled(ConfigDialog.currentType not in ConfigDialog.treeFormats.derivedDict) self.setConditionAvail() def changeIcon(self): """Change the icon setting based on button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] dlg = IconSelectDlg(currentFormat, self) if dlg.exec_() == QtGui.QDialog.Accepted: currentFormat.iconName = dlg.currentName self.setModifiedFunction() self.updateContent() def setConditionAvail(self): """Set conditional type available if geenric or derived type""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] if self.genericCombo.currentIndex() > 0 or \ ConfigDialog.currentType in \ ConfigDialog.treeFormats.derivedDict: self.conditionBox.setEnabled(True) if currentFormat.conditional: self.conditionButton.setText(_('Modify Co&nditional Types')) return else: self.conditionBox.setEnabled(False) self.conditionButton.setText(_('Create Co&nditional Types')) def createCondition(self): """Create or modify a conditional type based on a button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] dlg = ConditionDlg(_('Set Types Conditionally'), currentFormat, self) dlg.setConditions(currentFormat.conditional) if dlg.exec_() == QtGui.QDialog.Accepted: currentFormat.conditional = dlg.conditional() ConfigDialog.treeFormats.updateDerivedTypes() self.setConditionAvail() self.setModifiedFunction() def readChanges(self): """Make changes to the format for each widget""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] currentFormat.childType = unicode(self.childCombo.currentText()) if currentFormat.childType == TypeConfigPage.noChildTypeName: currentFormat.childType = '' currentFormat.refField = currentFormat.fieldList[self.refFieldCombo. currentIndex()] currentFormat.sibPrefix = unicode(self.prefixEdit.text()) currentFormat.sibSuffix = unicode(self.suffixEdit.text()) def readGenericChange(self, genericName): """MAke changes based on signal from generic type setting, allows field names and conditionals to update immediately""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] previousGeneric = currentFormat.genericType generic = unicode(genericName) if generic == TypeConfigPage.noChildTypeName: currentFormat.genericType = '' else: currentFormat.genericType = generic if previousGeneric != currentFormat.genericType: self.readChanges() ConfigDialog.treeFormats.updateDerivedTypes() if ConfigDialog.currentField not in currentFormat.fieldNames(): ConfigDialog.currentField = currentFormat.fieldList[0].name self.updateContent() class FieldListPage(ConfigPage): """Config page for a list of fields""" def __init__(self, setModifiedFunction, parent=None): ConfigPage.__init__(self, setModifiedFunction, parent) topLayout = QtGui.QVBoxLayout(self) typeBox = QtGui.QGroupBox(_('&Data Type')) topLayout.addWidget(typeBox) typeLayout = QtGui.QVBoxLayout(typeBox) self.typeCombo = QtGui.QComboBox() typeLayout.addWidget(self.typeCombo) self.connect(self.typeCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.changeCurrentType) fieldBox = QtGui.QGroupBox(_('Modify Field List')) topLayout.addWidget(fieldBox) horizLayout = QtGui.QHBoxLayout(fieldBox) self.fieldListBox = QtGui.QTreeWidget() horizLayout.addWidget(self.fieldListBox) self.fieldListBox.setRootIsDecorated(False) self.fieldListBox.setColumnCount(2) self.fieldListBox.setHeaderLabels([_('Name'), _('Type')]) self.connect(self.fieldListBox, QtCore.SIGNAL('currentItemChanged(QTreeWidgetItem*, '\ 'QTreeWidgetItem*)'), self.changeField) buttonLayout = QtGui.QVBoxLayout() horizLayout.addLayout(buttonLayout) self.upButton = QtGui.QPushButton(_('Move &Up')) buttonLayout.addWidget(self.upButton) self.connect(self.upButton, QtCore.SIGNAL('clicked()'), self.moveUp) self.downButton = QtGui.QPushButton(_('Move Do&wn')) buttonLayout.addWidget(self.downButton) self.connect(self.downButton, QtCore.SIGNAL('clicked()'), self.moveDown) self.newButton = QtGui.QPushButton(_('&New Field...')) buttonLayout.addWidget(self.newButton) self.connect(self.newButton, QtCore.SIGNAL('clicked()'), self.newField) self.renameButton = QtGui.QPushButton(_('R&ename Field...')) buttonLayout.addWidget(self.renameButton) self.connect(self.renameButton, QtCore.SIGNAL('clicked()'), self.renameField) self.deleteButton = QtGui.QPushButton(_('Delete F&ield')) buttonLayout.addWidget(self.deleteButton) self.connect(self.deleteButton, QtCore.SIGNAL('clicked()'), self.deleteField) def updateContent(self): """Update page contents from current format settings""" typeNames = ConfigDialog.treeFormats.nameList(True) self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItems(typeNames) selectNum = typeNames.index(ConfigDialog.currentType) self.typeCombo.setCurrentIndex(selectNum) self.typeCombo.blockSignals(False) currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] self.fieldListBox.blockSignals(True) self.fieldListBox.clear() for field in currentFormat.fieldList: QtGui.QTreeWidgetItem(self.fieldListBox, [field.name, _(field.typeName)]) selectNum = currentFormat.fieldNames().index(ConfigDialog.currentField) selectItem = self.fieldListBox.topLevelItem(selectNum) self.fieldListBox.setCurrentItem(selectItem) self.fieldListBox.setItemSelected(selectItem, True) self.fieldListBox.blockSignals(False) self.setButtonsAvail() def setButtonsAvail(self): """Update button availability""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] num = currentFormat.fieldNames().index(ConfigDialog.currentField) notDerived = not currentFormat.genericType self.upButton.setEnabled(num > 0 and notDerived) self.downButton.setEnabled(num < len(currentFormat.fieldList) - 1 and notDerived) self.newButton.setEnabled(notDerived) self.renameButton.setEnabled(notDerived) self.deleteButton.setEnabled(len(currentFormat.fieldList) > 1 and notDerived) def moveUp(self): """Move field up based on button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] fieldList = currentFormat.fieldList num = currentFormat.fieldNames().index(ConfigDialog.currentField) if num > 0: fieldList[num-1], fieldList[num] = fieldList[num], fieldList[num-1] if ConfigDialog.currentType in \ ConfigDialog.treeFormats.derivedDict: ConfigDialog.treeFormats.updateDerivedTypes() self.updateContent() self.setModifiedFunction() def moveDown(self): """Move field down based on button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] fieldList = currentFormat.fieldList num = currentFormat.fieldNames().index(ConfigDialog.currentField) if num < len(fieldList) - 1: fieldList[num], fieldList[num+1] = fieldList[num+1], fieldList[num] if ConfigDialog.currentType in \ ConfigDialog.treeFormats.derivedDict: ConfigDialog.treeFormats.updateDerivedTypes() self.updateContent() self.setModifiedFunction() def newField(self): """Create new field based on button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] dlg = FieldEntry(_('Add Field'), _('Enter new field name:'), '', currentFormat.fieldNames(), self) if dlg.exec_() == QtGui.QDialog.Accepted: htmlAttrs = globalref.options.boolData('HtmlNewFields') and \ {'html': 'y'} or {} currentFormat.addNewField(dlg.text, htmlAttrs) ConfigDialog.currentField = dlg.text if ConfigDialog.currentType in \ ConfigDialog.treeFormats.derivedDict: ConfigDialog.treeFormats.updateDerivedTypes() self.updateContent() self.setModifiedFunction() def renameField(self): """Rename field down based on button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] currentField = currentFormat.findField(ConfigDialog.currentField) dlg = FieldEntry(_('Rename Field'), _('Rename from "%s" to:') % currentField.name, currentField.name, currentFormat.fieldNames(), self) if dlg.exec_() == QtGui.QDialog.Accepted: currentField.name = dlg.text derivedTypes = ConfigDialog.treeFormats.derivedDict.\ get(ConfigDialog.currentType, []) for format in derivedTypes: field = format.findField(ConfigDialog.currentField) if field: field.name = dlg.text ConfigDialog.fieldRenameDict[ConfigDialog.currentType] = \ ConfigDialog.fieldRenameDict.\ get(ConfigDialog.currentType, []) + \ [(ConfigDialog.currentField, dlg.text)] ConfigDialog.currentField = dlg.text self.updateContent() self.setModifiedFunction() def deleteField(self): """Delete field down based on button signal""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] currentField = currentFormat.findField(ConfigDialog.currentField) num = currentFormat.fieldNames().index(ConfigDialog.currentField) currentFormat.removeField(currentField) derivedTypes = ConfigDialog.treeFormats.derivedDict.\ get(ConfigDialog.currentType, []) for format in derivedTypes: field = format.findField(ConfigDialog.currentField) if field: format.removeField(field) currentFormat.fieldList.remove(currentField) if currentFormat.refField == currentField: currentFormat.refField = currentFormat.fieldList[0] if num: num -= 1 ConfigDialog.currentField = currentFormat.fieldList[num].name self.updateContent() self.setModifiedFunction() class FieldConfigPage(ConfigPage): """Config page for a field""" types = [N_('Text', 'field type'), N_('Number', 'field type'), N_('Choice', 'field type'), N_('Combination', 'field type'), N_('AutoChoice', 'field type'), N_('Date', 'field type'), N_('Time', 'field type'), N_('Boolean', 'field type'), N_('URL', 'field type'), N_('Path', 'field type'), N_('InternalLink', 'field type'), N_('ExecuteLink', 'field type'), N_('UniqueID', 'field type'), N_('Email', 'field type'), N_('Picture', 'field type')] typeTransDict = dict([(_(name), name) for name in types]) noAltLinkText = _('No Alternate', 'no alt link field text') def __init__(self, setModifiedFunction, parent=None): ConfigPage.__init__(self, setModifiedFunction, parent) self.currentFileInfoField = '' self.fileInfoFieldModified = False topLayout = QtGui.QGridLayout(self) typeBox = QtGui.QGroupBox(_('&Data Type')) topLayout.addWidget(typeBox, 0, 0) typeLayout = QtGui.QVBoxLayout(typeBox) self.typeCombo = QtGui.QComboBox() typeLayout.addWidget(self.typeCombo) self.connect(self.typeCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.changeCurrentType) fieldBox = QtGui.QGroupBox(_('F&ield')) topLayout.addWidget(fieldBox, 0, 1) fieldLayout = QtGui.QVBoxLayout(fieldBox) self.fieldCombo = QtGui.QComboBox() fieldLayout.addWidget(self.fieldCombo) self.connect(self.fieldCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.changeCurrentField) self.fieldTypeBox = QtGui.QGroupBox(_('Fi&eld Type')) topLayout.addWidget(self.fieldTypeBox, 1, 0) fieldTypeLayout = QtGui.QVBoxLayout(self.fieldTypeBox) self.fieldTypeCombo = QtGui.QComboBox() fieldTypeLayout.addWidget(self.fieldTypeCombo) self.fieldTypeCombo.addItems([_(name) for name in FieldConfigPage.types]) self.connect(self.fieldTypeCombo, QtCore.SIGNAL('currentIndexChanged(const QString &)'), self.changeType) self.formatBox = QtGui.QGroupBox(_('O&utput Format')) topLayout.addWidget(self.formatBox, 1, 1) formatLayout = QtGui.QHBoxLayout(self.formatBox) self.formatEdit = QtGui.QLineEdit() formatLayout.addWidget(self.formatEdit) self.connect(self.formatEdit, QtCore.SIGNAL('textEdited(const QString &)'), setModifiedFunction) self.connect(self.formatEdit, QtCore.SIGNAL('textEdited(const QString &)'), self.checkFileInfoMod) self.helpButton = QtGui.QPushButton(_('Format &Help')) formatLayout.addWidget(self.helpButton) self.connect(self.helpButton, QtCore.SIGNAL('clicked()'), self.formatHelp) extraBox = QtGui.QGroupBox(_('Extra Text')) topLayout.addWidget(extraBox, 2, 0) extraLayout = QtGui.QVBoxLayout(extraBox) extraLayout.setSpacing(0) prefixLabel = QtGui.QLabel(_('&Prefix')) extraLayout.addWidget(prefixLabel) self.prefixEdit = QtGui.QLineEdit() extraLayout.addWidget(self.prefixEdit) self.connect(self.prefixEdit, QtCore.SIGNAL('textEdited(const QString &)'), setModifiedFunction) self.connect(self.prefixEdit, QtCore.SIGNAL('textEdited(const QString &)'), self.checkFileInfoMod) prefixLabel.setBuddy(self.prefixEdit) extraLayout.addSpacing(8) suffixLabel = QtGui.QLabel(_('Suffi&x')) extraLayout.addWidget(suffixLabel) self.suffixEdit = QtGui.QLineEdit() extraLayout.addWidget(self.suffixEdit) self.connect(self.suffixEdit, QtCore.SIGNAL('textEdited(const QString &)'), setModifiedFunction) self.connect(self.suffixEdit, QtCore.SIGNAL('textEdited(const QString &)'), self.checkFileInfoMod) suffixLabel.setBuddy(self.suffixEdit) self.handleBox = QtGui.QGroupBox(_('Content Text Handling')) topLayout.addWidget(self.handleBox, 2, 1) handleLayout = QtGui.QVBoxLayout(self.handleBox) self.htmlButton = QtGui.QRadioButton(_('Allow HT&ML rich text')) handleLayout.addWidget(self.htmlButton) self.connect(self.htmlButton, QtCore.SIGNAL('toggled(bool)'), setModifiedFunction) self.connect(self.htmlButton, QtCore.SIGNAL('toggled(bool)'), self.checkFileInfoMod) self.plainButton = QtGui.QRadioButton(_('Plai&n text with '\ 'line breaks')) handleLayout.addWidget(self.plainButton) self.defaultBox = QtGui.QGroupBox(_('Default V&alue for New Nodes')) topLayout.addWidget(self.defaultBox, 3, 0) defaultLayout = QtGui.QVBoxLayout(self.defaultBox) self.defaultCombo = QtGui.QComboBox() defaultLayout.addWidget(self.defaultCombo) self.defaultCombo.setEditable(True) self.connect(self.defaultCombo, QtCore.SIGNAL('editTextChanged(const QString &)'), setModifiedFunction) self.heightBox = QtGui.QGroupBox(_('Editor Height')) topLayout.addWidget(self.heightBox, 3, 1) heightLayout = QtGui.QHBoxLayout(self.heightBox) heightLabel = QtGui.QLabel(_('Num&ber of text lines')) heightLayout.addWidget(heightLabel) self.heightCtrl = QtGui.QSpinBox() heightLayout.addWidget(self.heightCtrl) self.heightCtrl.setMinimum(1) self.heightCtrl.setMaximum(optiondefaults.maxNumLines) self.connect(self.heightCtrl, QtCore.SIGNAL('valueChanged(int)'), setModifiedFunction) heightLabel.setBuddy(self.heightCtrl) # Advanced self.linkBox = QtGui.QGroupBox(_('Field &with alternate '\ 'text for links')) topLayout.addWidget(self.linkBox, 4, 0) self.advancedWidgets.append(self.linkBox) linkLayout = QtGui.QVBoxLayout(self.linkBox) self.linkCombo = QtGui.QComboBox() linkLayout.addWidget(self.linkCombo) self.connect(self.linkCombo, QtCore.SIGNAL('currentIndexChanged(int)'), setModifiedFunction) self.paramBox = QtGui.QGroupBox(_('Optional Parameters')) topLayout.addWidget(self.paramBox, 4, 1) self.advancedWidgets.append(self.paramBox) paramLayout = QtGui.QVBoxLayout(self.paramBox) self.reqdButton = QtGui.QCheckBox(_('Re&quired to be filled')) paramLayout.addWidget(self.reqdButton) self.connect(self.reqdButton, QtCore.SIGNAL('toggled(bool)'), setModifiedFunction) self.hiddenButton = QtGui.QCheckBox(_('Hidden on editor &view')) paramLayout.addWidget(self.hiddenButton) self.connect(self.hiddenButton, QtCore.SIGNAL('toggled(bool)'), setModifiedFunction) topLayout.setRowStretch(5, 1) def updateContent(self): """Update page contents from current format settings""" typeNames = ConfigDialog.treeFormats.nameList(True) typeNames.append(_('File Info Reference')) self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItems(typeNames) if self.currentFileInfoField: selectNum = len(typeNames) - 1 else: selectNum = typeNames.index(ConfigDialog.currentType) self.typeCombo.setCurrentIndex(selectNum) self.typeCombo.blockSignals(False) currentFormat, currentField = self.currentFormatField() self.fieldCombo.blockSignals(True) self.fieldCombo.clear() self.fieldCombo.addItems(currentFormat.fieldNames()) selectNum = currentFormat.fieldNames().index(currentField.name) self.fieldCombo.setCurrentIndex(selectNum) self.fieldCombo.blockSignals(False) self.fieldTypeCombo.blockSignals(True) selectNum = FieldConfigPage.types.index(currentField.typeName) self.fieldTypeCombo.setCurrentIndex(selectNum) self.fieldTypeCombo.blockSignals(False) self.formatEdit.setText(currentField.format) self.prefixEdit.setText(currentField.prefix) self.suffixEdit.setText(currentField.suffix) self.htmlButton.setChecked(currentField.html) self.plainButton.setChecked(not currentField.html) self.defaultCombo.blockSignals(True) self.defaultCombo.clear() self.defaultCombo.addItem(currentField.getEditInitDefault()) self.defaultCombo.addItems(currentField.initDefaultChoices()) self.defaultCombo.setCurrentIndex(0) self.defaultCombo.blockSignals(False) self.heightCtrl.blockSignals(True) self.heightCtrl.setValue(currentField.numLines) self.heightCtrl.blockSignals(False) self.linkCombo.blockSignals(True) self.linkCombo.clear() self.linkCombo.addItem(FieldConfigPage.noAltLinkText) if currentField.allowAltLinkText: linkFields = [name for name in currentFormat.fieldNames() if name != currentField.name] self.linkCombo.addItems(linkFields) if currentField.linkAltField: self.linkCombo.setCurrentIndex(linkFields.index(currentField.\ linkAltField)) self.linkCombo.blockSignals(False) self.reqdButton.blockSignals(True) self.reqdButton.setChecked(currentField.isRequired) self.reqdButton.blockSignals(False) self.hiddenButton.blockSignals(True) self.hiddenButton.setChecked(currentField.hidden) self.hiddenButton.blockSignals(False) self.fileInfoFieldModified = False self.setControlAvailability() def currentFormatField(self): """Return a tuple of the current format & field""" if self.currentFileInfoField: currentFormat = ConfigDialog.fileInfoFormat currentField = currentFormat.findField(self.currentFileInfoField) else: currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] currentField = currentFormat.findField(ConfigDialog.currentField) return (currentFormat, currentField) def changeCurrentType(self, index): """Change the current format type based on a signal""" if index == len(ConfigDialog.treeFormats.nameList(True)): self.readChanges() self.currentFileInfoField = ConfigDialog.fileInfoFormat.\ fieldList[0].name self.updateContent() else: self.currentFileInfoField = '' ConfigPage.changeCurrentType(self, index) def changeCurrentField(self, index): """Change the current format field based on a signal""" if self.currentFileInfoField: self.readChanges() self.currentFileInfoField = ConfigDialog.fileInfoFormat.\ fieldNames()[index] self.updateContent() else: ConfigPage.changeCurrentField(self, index) def setControlAvailability(self): """Set controls available based on field type""" currentFormat, currentField = self.currentFormatField() self.fieldTypeBox.setEnabled(not self.currentFileInfoField and not currentFormat.genericType) self.formatBox.setEnabled(currentField.defaultFormat != '') self.handleBox.setEnabled(currentField.htmlOption) self.defaultBox.setEnabled(not self.currentFileInfoField) self.heightBox.setEnabled(not currentField.hasEditChoices and not self.currentFileInfoField) self.linkBox.setEnabled(currentField.allowAltLinkText and not self.currentFileInfoField) self.paramBox.setEnabled(not self.currentFileInfoField) def checkFileInfoMod(self): """Check for modified file info field""" if self.currentFileInfoField: self.fileInfoFieldModified = True def changeType(self, text): """Change field type based on combo box signal""" self.readChanges() # preserve previous changes currentFormat, currentField = self.currentFormatField() currentField.changeType(FieldConfigPage.typeTransDict[unicode(text)]) if ConfigDialog.currentType in ConfigDialog.treeFormats.derivedDict: ConfigDialog.treeFormats.updateDerivedTypes() self.updateContent() self.setModifiedFunction() def formatHelp(self): """Provide format help menu based on button signal""" currentFormat, currentField = self.currentFormatField() menu = QtGui.QMenu(self) self.formatDict = {} for item in currentField.formatMenuList: if item: descr, key = item self.formatDict[descr] = key menu.addAction(descr) else: menu.addSeparator() menu.popup(self.helpButton.\ mapToGlobal(QtCore.QPoint(0, self.helpButton.height()))) self.connect(menu, QtCore.SIGNAL('triggered(QAction*)'), self.insertFormat) def insertFormat(self, action): """Insert format text from id into edit box""" self.formatEdit.insert(self.formatDict[unicode(action.text())]) def readChanges(self): """Make changes to the format for each widget""" currentFormat, currentField = self.currentFormatField() currentField.format = unicode(self.formatEdit.text()) currentField.prefix = unicode(self.prefixEdit.text()) currentField.suffix = unicode(self.suffixEdit.text()) if currentField.htmlOption: currentField.html = self.htmlButton.isChecked() currentField.setInitDefault(unicode(self.defaultCombo.currentText())) if not currentField.hasEditChoices: currentField.numLines = self.heightCtrl.value() if currentField.allowAltLinkText: text = unicode(self.linkCombo.currentText()) if text == FieldConfigPage.noAltLinkText: text = '' currentField.linkAltField = text currentField.isRequired = self.reqdButton.isChecked() currentField.hidden = self.hiddenButton.isChecked() currentField.initFormat() if self.currentFileInfoField and self.fileInfoFieldModified: ConfigDialog.treeFormats[currentFormat.name] = currentFormat self.fileInfoFieldModified = False class OutputPage(ConfigPage): """Config page for type output""" refLevelList = [_('No Other Reference'), _('File Info Reference'), _('Any Ancestor Reference'), _('Parent Reference'), _('Grandparent Reference'), _('Great Grandparent Reference'), _('Child Reference'), _('Child Count')] # refLevelFlags correspond to refLevelList refLevelFlags = ['', '!', '?', '*', '**', '***', '&', '#'] fieldPattern = re.compile('{\*.*?\*}') def __init__(self, setModifiedFunction, parent=None): ConfigPage.__init__(self, setModifiedFunction, parent) self.refLevelFlag = '' self.refLevelType = '' topLayout = QtGui.QGridLayout(self) typeBox = QtGui.QGroupBox(_('&Data Type')) topLayout.addWidget(typeBox, 0, 0) typeLayout = QtGui.QVBoxLayout(typeBox) self.typeCombo = QtGui.QComboBox() typeLayout.addWidget(self.typeCombo) self.connect(self.typeCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.changeCurrentType) fieldBox = QtGui.QGroupBox(_('F&ield List')) topLayout.addWidget(fieldBox, 1, 0, 2, 1) horizLayout = QtGui.QVBoxLayout(fieldBox) self.fieldListBox = QtGui.QTreeWidget() horizLayout.addWidget(self.fieldListBox) self.fieldListBox.setRootIsDecorated(False) self.fieldListBox.setColumnCount(2) self.fieldListBox.setHeaderLabels([_('Name'), _('Type')]) self.connect(self.fieldListBox, QtCore.SIGNAL('currentItemChanged(QTreeWidgetItem*, '\ 'QTreeWidgetItem*)'), self.changeVirtualField) titleButtonLayout = QtGui.QVBoxLayout() topLayout.addLayout(titleButtonLayout, 1, 1) self.toTitleButton = QtGui.QPushButton('>>') titleButtonLayout.addWidget(self.toTitleButton) self.toTitleButton.setMaximumWidth(self.toTitleButton. sizeHint().height()) self.connect(self.toTitleButton, QtCore.SIGNAL('clicked()'), self.fieldToTitle) self.delTitleButton = QtGui.QPushButton('<<') titleButtonLayout.addWidget(self.delTitleButton) self.delTitleButton.setMaximumWidth(self.delTitleButton. sizeHint().height()) self.connect(self.delTitleButton, QtCore.SIGNAL('clicked()'), self.delTitleField) titleBox = QtGui.QGroupBox(_('Titl&e Format')) topLayout.addWidget(titleBox, 1, 2) titleLayout = QtGui.QVBoxLayout(titleBox) self.titleEdit = TitleEdit() titleLayout.addWidget(self.titleEdit) self.connect(self.titleEdit, QtCore.SIGNAL('cursorPositionChanged(int, int)'), self.setControlAvailability) self.connect(self.titleEdit, QtCore.SIGNAL('textEdited(const QString &)'), setModifiedFunction) outputButtonLayout = QtGui.QVBoxLayout() topLayout.addLayout(outputButtonLayout, 2, 1) self.toOutputButton = QtGui.QPushButton('>>') outputButtonLayout.addWidget(self.toOutputButton) self.toOutputButton.setMaximumWidth(self.toOutputButton. sizeHint().height()) self.connect(self.toOutputButton, QtCore.SIGNAL('clicked()'), self.fieldToOutput) self.delOutputButton = QtGui.QPushButton('<<') outputButtonLayout.addWidget(self.delOutputButton) self.delOutputButton.setMaximumWidth(self.delOutputButton.\ sizeHint().height()) self.connect(self.delOutputButton, QtCore.SIGNAL('clicked()'), self.delOutputField) outputBox = QtGui.QGroupBox(_('O&utput Format')) topLayout.addWidget(outputBox, 2, 2) outputLayout = QtGui.QVBoxLayout(outputBox) self.outputEdit = QtGui.QTextEdit() self.outputEdit.setLineWrapMode(QtGui.QTextEdit.NoWrap) outputLayout.addWidget(self.outputEdit) self.outputEdit.setTabChangesFocus(True) self.connect(self.outputEdit, QtCore.SIGNAL('cursorPositionChanged()'), self.setControlAvailability) self.connect(self.outputEdit, QtCore.SIGNAL('textChanged()'), setModifiedFunction) topLayout.setRowStretch(1, 1) topLayout.setRowStretch(2, 1) # Advanced advancedStack = QtGui.QStackedWidget() topLayout.addWidget(advancedStack, 0, 2) otherBox = QtGui.QGroupBox(_('Other Field References')) spaceWidget = QtGui.QWidget() advancedStack.addWidget(spaceWidget) advancedStack.addWidget(otherBox) self.advancedWidgets.append(advancedStack) otherLayout = QtGui.QHBoxLayout(otherBox) levelLayout = QtGui.QVBoxLayout() otherLayout.addLayout(levelLayout) levelLayout.setSpacing(0) levelLabel = QtGui.QLabel(_('Re&ference Level')) levelLayout.addWidget(levelLabel) levelCombo = QtGui.QComboBox() levelLayout.addWidget(levelCombo) levelLabel.setBuddy(levelCombo) levelCombo.addItems(OutputPage.refLevelList) self.connect(levelCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.changeRefLevel) refTypeLayout = QtGui.QVBoxLayout() otherLayout.addLayout(refTypeLayout) refTypeLayout.setSpacing(0) refTypeLabel = QtGui.QLabel(_('Reference Ty&pe')) refTypeLayout.addWidget(refTypeLabel) self.refTypeCombo = QtGui.QComboBox() refTypeLayout.addWidget(self.refTypeCombo) refTypeLabel.setBuddy(self.refTypeCombo) self.connect(self.refTypeCombo, QtCore.SIGNAL('currentIndexChanged(int)'), self.changeRefType) def toggleAdvanced(self, show=True): """Toggle state of advanced widgets, done with a stack to preserve spacing""" self.advancedWidgets[0].setCurrentIndex(show) def updateContent(self): """Update page contents from current format settings""" typeNames = ConfigDialog.treeFormats.nameList(True) self.typeCombo.blockSignals(True) self.typeCombo.clear() self.typeCombo.addItems(typeNames) selectNum = typeNames.index(ConfigDialog.currentType) self.typeCombo.setCurrentIndex(selectNum) self.typeCombo.blockSignals(False) currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] if not self.refLevelFlag: format = currentFormat elif self.refLevelFlag == '!': format = ConfigDialog.fileInfoFormat elif self.refLevelFlag == '#': format = nodeformat.ChildCountFormat() else: format = ConfigDialog.treeFormats[self.refLevelType] self.fieldListBox.blockSignals(True) self.fieldListBox.clear() for field in format.fieldList: if field.showInDialog: QtGui.QTreeWidgetItem(self.fieldListBox, [field.name, _(field.typeName)]) try: selectNum = format.fieldNames().index(ConfigDialog.currentField) except ValueError: selectNum = 0 selectItem = self.fieldListBox.topLevelItem(selectNum) self.fieldListBox.setCurrentItem(selectItem) self.fieldListBox.setItemSelected(selectItem, True) self.fieldListBox.blockSignals(False) lines = currentFormat.getLines() self.titleEdit.blockSignals(True) self.titleEdit.setText(lines[0]) self.titleEdit.end(False) self.titleEdit.blockSignals(False) self.outputEdit.blockSignals(True) self.outputEdit.setPlainText(u'\n'.join(lines[1:])) cursor = self.outputEdit.textCursor() cursor.movePosition(QtGui.QTextCursor.End) self.outputEdit.setTextCursor(cursor) self.outputEdit.blockSignals(False) self.refTypeCombo.blockSignals(True) self.refTypeCombo.clear() self.refTypeCombo.addItems(typeNames) try: self.refTypeCombo.setCurrentIndex(typeNames. index(self.refLevelType)) except ValueError: # type no longer exists self.refLevelType = unicode(self.refTypeCombo.currentText()) self.refTypeCombo.blockSignals(False) self.refTypeCombo.setEnabled(self.refLevelFlag not in ['', '!', '#']) self.setControlAvailability() def changeVirtualField(self, currentItem, prevItem): """Change the current format field based on a tree widget signal, update global if not set to alternate ref level""" if not currentItem: currentItem = self.fieldListBox.\ topLevelItem(self.fieldListBox.topLevelItemCount() - 1) self.fieldListBox.setCurrentItem(currentItem) self.fieldListBox.setItemSelected(currentItem, True) if not self.refLevelFlag: ConfigDialog.currentField = unicode(currentItem.text(0)) def changeRefLevel(self, num): """Change the reference level based on a widget signal""" self.refLevelFlag = OutputPage.refLevelFlags[num] if self.refLevelFlag not in ['', '!', '#'] and not self.refLevelType: self.refLevelType = ConfigDialog.treeFormats.nameList(True)[0] self.updateContent() def changeRefType(self, num): """Change the reference level type based on a widget signal""" self.refLevelType = ConfigDialog.treeFormats.nameList(True)[num] self.updateContent() def setControlAvailability(self): """Set controls available based on text cursor movements""" titleCursorField = self.titleFieldPos() self.toTitleButton.setEnabled(titleCursorField == ()) self.delTitleButton.setEnabled(len(titleCursorField) > 1) outputCursorField = self.outputFieldPos() self.toOutputButton.setEnabled(outputCursorField == ()) self.delOutputButton.setEnabled(len(outputCursorField) > 1) def currentFieldSepName(self): """Return current field name with proper separators""" return u'{*%s%s*}' % (self.refLevelFlag, unicode(self.fieldListBox.currentItem().text(0))) def fieldToTitle(self): """Add selected field to cursor pos in editor""" self.titleEdit.insert(self.currentFieldSepName()) self.titleEdit.setFocus() def delTitleField(self): """Remove field from cursor pos in editor""" start, end = self.titleFieldPos() self.titleEdit.setSelection(start, end - start) self.titleEdit.insert('') def fieldToOutput(self): """Add selected field to cursor pos in editor""" self.outputEdit.insertPlainText(self.currentFieldSepName()) self.outputEdit.setFocus() def delOutputField(self): """Remove field from cursor pos in editor""" start, end = self.outputFieldPos() outputCursor = self.outputEdit.textCursor() outputCursor.setPosition(start) outputCursor.setPosition(end, QtGui.QTextCursor.KeepAnchor) self.outputEdit.setTextCursor(outputCursor) self.outputEdit.insertPlainText('') def titleFieldPos(self): """Return tuple of start, end for field pattern at title cursor, or (None,) if selection overlaps a field end, or empty tuple if not found""" position = self.titleEdit.cursorPosition() start = self.titleEdit.selectionStart() if start < 0: start = position elif start == position: # backward selection position += len(unicode(self.titleEdit.selectedText())) return self.fieldPosAtCursor(start, position, unicode(self.titleEdit.text())) def outputFieldPos(self): """Return tuple of start, end for field pattern at output cursor or (None,) if selection overlaps a field end, or empty tuple if not found""" outputCursor = self.outputEdit.textCursor() anchor = outputCursor.anchor() position = outputCursor.position() block = outputCursor.block() blockStart = block.position() if anchor < blockStart or anchor > blockStart + block.length(): return (None,) # multi-line selection result = self.fieldPosAtCursor(anchor - blockStart, position - blockStart, unicode(block.text())) if len(result) > 1: return (result[0] + blockStart, result[1] + blockStart) return result def fieldPosAtCursor(self, anchorPos, cursorPos, textLine): """Find field pattern enclosing the cursor or selection, return tuple of start, end if found, return (None,) if selection overlaps a field end, return empty tuple if not found""" for match in OutputPage.fieldPattern.finditer(textLine): cursorIn = match.start() < cursorPos < match.end() anchorIn = match.start() < anchorPos < match.end() if cursorIn and anchorIn: return (match.start(), match.end()) if cursorIn or anchorIn: return (None,) return () def readChanges(self): """Make changes to the format for each widget""" currentFormat = ConfigDialog.treeFormats[ConfigDialog.currentType] currentFormat.changeTitleLine(unicode(self.titleEdit.text())) currentFormat.changeOutputLines(unicode(self.outputEdit. toPlainText()).split('\n')) class TitleEdit(QtGui.QLineEdit): """LineEdit that avoids changing the selection on focus changes""" def __init__(self, parent=None): QtGui.QLineEdit.__init__(self, parent) def focusInEvent(self, event): """Override to keep selection & cursor position""" cursorPos = self.cursorPosition() selectStart = self.selectionStart() if selectStart == cursorPos: selectStart = cursorPos + len(unicode(self.selectedText())) QtGui.QLineEdit.focusInEvent(self, event) self.setCursorPosition(cursorPos) if selectStart >= 0: self.setSelection(selectStart, cursorPos - selectStart) self.emit(QtCore.SIGNAL('focusIn'), self) def focusOutEvent(self, event): """Override to keep selection & cursor position""" cursorPos = self.cursorPosition() selectStart = self.selectionStart() if selectStart == cursorPos: selectStart = cursorPos + len(unicode(self.selectedText())) QtGui.QLineEdit.focusOutEvent(self, event) self.setCursorPosition(cursorPos) if selectStart >= 0: self.setSelection(selectStart, cursorPos - selectStart) class FieldEntry(QtGui.QDialog): """Dialog for alpha-numeric and underscore text entry""" illegalRe = re.compile(r'[^\w_\-.]', re.U) def __init__(self, caption, labelText, dfltText='', badStr=[], parent=None): QtGui.QDialog.__init__(self, parent) self.text = '' self.badStr = badStr self.setWindowFlags(stdWinFlags) self.setWindowTitle(caption) self.topLayout = QtGui.QVBoxLayout(self) label = QtGui.QLabel(labelText) self.topLayout.addWidget(label) self.entry = QtGui.QLineEdit(dfltText) self.topLayout.addWidget(self.entry) self.entry.setFocus() self.connect(self.entry, QtCore.SIGNAL('returnPressed()'), self, QtCore.SLOT('accept()')) ctrlLayout = QtGui.QHBoxLayout() self.topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) def accept(self): """Check for acceptable string before closing""" self.text = unicode(self.entry.text()).strip() error = '' if not self.text: error = _('Empty name is not acceptable') elif not self.text[0].isalpha(): error = _('Name must start with a letter') elif self.text[:3].lower() == 'xml': error = _('Name cannot start with "xml"') elif FieldEntry.illegalRe.search(self.text): badChars = set(FieldEntry.illegalRe.findall(self.text)) error = '%s: "%s"' % \ (_('The following characters are not allowed'), unicode(''.join(badChars))) elif self.text in self.badStr: error = _('Entered name was already used') if error: QtGui.QMessageBox.warning(self, 'TreeLine', error) return return QtGui.QDialog.accept(self) class FieldCopyEntry(FieldEntry): """Dialog for alpha-numeric and underscore text entry with derive option""" def __init__(self, dfltText='', badStr=[], allowDerive=True, parent=None): FieldEntry.__init__(self, _('Copy Type'), _('Enter new type name:'), dfltText, badStr, parent) self.derived = False self.deriveCheck = QtGui.QCheckBox(_('Derive from original')) self.topLayout.insertWidget(2, self.deriveCheck) self.deriveCheck.setEnabled(allowDerive) def accept(self): """Check for derived and acceptable string before closing""" self.derived = self.deriveCheck.isChecked() return FieldEntry.accept(self) class ConditionDlg(QtGui.QDialog): """Dialog for selecting conditional filter rules""" boolOp = [N_('and', 'filter bool'), N_('or', 'filter bool')] boolOpTransDict = dict([(_(name), name) for name in boolOp]) def __init__(self, caption, nodeFormat, parent=None): QtGui.QDialog.__init__(self, parent) self.nodeFormat = nodeFormat self.fieldList = nodeFormat.fieldNames() self.setWindowFlags(stdWinFlags) self.setWindowTitle(caption) self.topLayout = QtGui.QVBoxLayout(self) self.setLayout(self.topLayout) self.ruleList = [ConditionRule(1, self.fieldList)] self.topLayout.addWidget(self.ruleList[-1]) self.boolList = [] ctrlLayout = QtGui.QHBoxLayout() self.topLayout.addLayout(ctrlLayout) ctrlLayout.insertStretch(0) addButton = QtGui.QPushButton(_('&Add New Rule')) ctrlLayout.addWidget(addButton) self.connect(addButton, QtCore.SIGNAL('clicked()'), self.addRule) self.remButton = QtGui.QPushButton(_('&Remove Rule')) ctrlLayout.addWidget(self.remButton) self.connect(self.remButton, QtCore.SIGNAL('clicked()'), self.removeRule) self.okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(self.okButton) self.connect(self.okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) if len(self.ruleList) < 1: self.remButton.setEnabled(False) def addRule(self): """Add new rule to dialog""" if self.ruleList: boolBox = QtGui.QComboBox() boolBox.setEditable(False) self.boolList.append(boolBox) boolBox.addItems([_(op) for op in ConditionDlg.boolOp]) self.topLayout.insertWidget(len(self.ruleList) * 2 - 1, boolBox, 0, QtCore.Qt.AlignHCenter) rule = ConditionRule(len(self.ruleList) + 1, self.fieldList) self.ruleList.append(rule) self.topLayout.insertWidget(len(self.ruleList) * 2 - 2, rule) self.remButton.setEnabled(True) def removeRule(self): """Remove the last rule""" if len(self.ruleList) > 0: if self.boolList: self.boolList[-1].hide() del self.boolList[-1] self.ruleList[-1].hide() del self.ruleList[-1] self.topLayout.invalidate() if len(self.ruleList) < 1: self.remButton.setEnabled(False) def setConditions(self, condition): """Set dialog to match Condition instance""" while len(self.ruleList) > 1: self.removeRule() if condition: self.ruleList[0].setCondition(condition.conditionList[0], self.nodeFormat, self.fieldList) for condLine in condition.conditionList[1:]: self.addRule() self.boolList[-1].setCurrentIndex(ConditionDlg.boolOp. index(condLine.boolOper)) self.ruleList[-1].setCondition(condLine, self.nodeFormat, self.fieldList) def conditional(self): """Return a Conditional instance for this rule set""" return conditional.Conditional(self.ruleText()) def ruleText(self): """Return full text of this rule set""" textList = [rule.ruleText(self.nodeFormat) for rule in self.ruleList] boolList = [ConditionDlg.boolOpTransDict[unicode(box.currentText())] for box in self.boolList] result = '' if textList: result = textList.pop(0) for text in textList: result = ' '.join((result, boolList.pop(0), text)) return result class ConditionRule(QtGui.QGroupBox): """Saves rules for filtering items""" oper = ['==', '<', '<=', '>', '>=', '!=', N_('starts with', 'filter rule'), N_('ends with', 'filter rule'), N_('contains', 'filter rule'), N_('True', 'filter rule'), N_('False', 'filter rule')] operTransDict = dict([(_(name), name) for name in oper]) def __init__(self, num, fieldList, parent=None): QtGui.QGroupBox.__init__(self, parent) self.setTitle(_('Rule %d') % num) layout = QtGui.QHBoxLayout(self) self.fieldBox = QtGui.QComboBox() self.fieldBox.setEditable(False) self.fieldBox.addItems(fieldList) layout.addWidget(self.fieldBox) self.opBox = QtGui.QComboBox() self.opBox.setEditable(False) self.opBox.addItems([_(op) for op in ConditionRule.oper]) layout.addWidget(self.opBox) self.connect(self.opBox, QtCore.SIGNAL('activated(const QString &)'), self.changeOp) self.edit = QtGui.QLineEdit() self.edit.setMinimumWidth(80) layout.addWidget(self.edit) self.fieldBox.setFocus() def changeOp(self, newOp): """Update the dialog based on type selection change""" newOp = ConditionRule.operTransDict[unicode(newOp)] hasFields = newOp not in ('True', 'False') self.fieldBox.setEnabled(hasFields) self.edit.setEnabled(hasFields) def setCondition(self, condLine, nodeFormat, fieldList): """Set values to match ConditionLine instance""" try: fieldNum = fieldList.index(condLine.field.name) except ValueError: fieldNum = 0 self.fieldBox.setCurrentIndex(fieldNum) self.opBox.setCurrentIndex(ConditionRule.oper.index(condLine.oper)) value = condLine.field.formatEditText(condLine.value)[0] self.edit.setText(value) def ruleText(self, nodeFormat): """Return full text of this rule""" op = ConditionRule.operTransDict[unicode(self.opBox.currentText())] field = unicode(self.fieldBox.currentText()) value = unicode(self.edit.text()) value = nodeFormat.findField(field).storedText(value)[0] value = value.replace('\\', '\\\\').replace('"', '\\"') return '%s %s "%s"' % (field, op, value) class IconSelectDlg(QtGui.QDialog): """Dialog for selecting icons for a format type""" iconList = [N_('default', 'icon name'), N_('treeline', 'icon name'), N_('anchor', 'icon name'), N_('arrow_1', 'icon name'), N_('arrow_2', 'icon name'), N_('arrow_3', 'icon name'), N_('arrow_4', 'icon name'), N_('arrow_5', 'icon name'), N_('bell', 'icon name'), N_('book_1', 'icon name'), N_('book_2', 'icon name'), N_('book_3', 'icon name'), N_('bookmark', 'icon name'), N_('bulb', 'icon name'), N_('bullet_1', 'icon name'), N_('bullet_2', 'icon name'), N_('bullet_3', 'icon name'), N_('check_1', 'icon name'), N_('check_2', 'icon name'), N_('check_3', 'icon name'), N_('clock', 'icon name'), N_('colors', 'icon name'), N_('date_1', 'icon name'), N_('date_2', 'icon name'), N_('disk', 'icon name'), N_('doc', 'icon name'), N_('euro', 'icon name'), N_('folder_1', 'icon name'), N_('folder_2', 'icon name'), N_('folder_3', 'icon name'), N_('gear', 'icon name'), N_('gnu', 'icon name'), N_('hand', 'icon name'), N_('heart', 'icon name'), N_('home', 'icon name'), N_('lock_1', 'icon name'), N_('lock_2', 'icon name'), N_('mag', 'icon name'), N_('mail', 'icon name'), N_('minus', 'icon name'), N_('misc', 'icon name'), N_('move', 'icon name'), N_('music', 'icon name'), N_('note', 'icon name'), N_('pencil', 'icon name'), N_('person', 'icon name'), N_('plus', 'icon name'), N_('printer', 'icon name'), N_('question', 'icon name'), N_('rocket', 'icon name'), N_('round_minus', 'icon name'), N_('round_plus', 'icon name'), N_('smiley_1', 'icon name'), N_('smiley_2', 'icon name'), N_('smiley_3', 'icon name'), N_('smiley_4', 'icon name'), N_('smiley_5', 'icon name'), N_('sphere', 'icon name'), N_('star', 'icon name'), N_('sum', 'icon name'), N_('table', 'icon name'), N_('task_1', 'icon name'), N_('task_2', 'icon name'), N_('term', 'icon name'), N_('text', 'icon name'), N_('trash', 'icon name'), N_('tux_1', 'icon name'), N_('tux_2', 'icon name'), N_('warning', 'icon name'), N_('wrench', 'icon name'), N_('write', 'icon name'), N_('x_1', 'icon name'), N_('x_2', 'icon name'), N_('x_3', 'icon name')] iconTransDict = dict([(_(name), name) for name in iconList]) dialogSize = () dialogPos = () def __init__(self, nodeFormat, parent=None): QtGui.QDialog.__init__(self, parent) self.currentName = nodeFormat.iconName if not self.currentName or \ self.currentName not in globalref.treeIcons.keys(): self.currentName = globalref.treeIcons.defaultName self.setWindowFlags(stdWinFlags) self.setWindowTitle(_('Set Data Type Icon')) topLayout = QtGui.QVBoxLayout(self) self.iconView = QtGui.QListWidget() self.iconView.setViewMode(QtGui.QListView.ListMode) self.iconView.setMovement(QtGui.QListView.Static) self.iconView.setResizeMode(QtGui.QListView.Adjust) self.iconView.setWrapping(True) self.iconView.setGridSize(QtCore.QSize(112, 32)) topLayout.addWidget(self.iconView) ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) clearButton = QtGui.QPushButton(_('Clear &Select')) ctrlLayout.addWidget(clearButton) self.connect(clearButton, QtCore.SIGNAL('clicked()'), self.iconView.clearSelection) okButton = QtGui.QPushButton(_('&OK')) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel')) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) self.connect(self.iconView, QtCore.SIGNAL('itemDoubleClicked(QListWidgetItem*)'), QtCore.SLOT('accept()')) if IconSelectDlg.dialogSize: self.resize(IconSelectDlg.dialogSize[0], IconSelectDlg.dialogSize[1]) self.move(IconSelectDlg.dialogPos[0], IconSelectDlg.dialogPos[1]) self.loadIcons() def loadIcons(self): """Load icons from the icon source""" if not globalref.treeIcons.allLoaded: globalref.treeIcons.loadAllIcons() for name, icon in globalref.treeIcons.items(): if icon: try: transName = _(name.encode()) except UnicodeError: transName = name item = QtGui.QListWidgetItem(icon, transName, self.iconView) if name == self.currentName: self.iconView.setCurrentItem(item) self.iconView.sortItems() self.show() # req'd to make scroll to item work selectedItem = self.iconView.currentItem() if selectedItem: self.iconView.scrollToItem(selectedItem, QtGui.QAbstractItemView.PositionAtCenter) def saveSize(self): """Record dialog size at close""" IconSelectDlg.dialogSize = (self.width(), self.height()) IconSelectDlg.dialogPos = (self.x(), self.y()) def accept(self): """Save changes before closing""" selectedItems = self.iconView.selectedItems() if selectedItems: name = unicode(selectedItems[0].text()) self.currentName = IconSelectDlg.iconTransDict.get(name, name) if self.currentName == globalref.treeIcons.defaultName: self.currentName = '' else: self.currentName = globalref.treeIcons.noneName QtGui.QDialog.accept(self) self.saveSize() def reject(self): """Save size before closing""" QtGui.QDialog.reject(self) self.saveSize() TreeLine/source/treeline.py0000755000175000017500000001256311656076723014753 0ustar dougdoug#!/usr/bin/env python """ **************************************************************************** treeline.py, the main program file TreeLine, an information storage program Copyright (C) 2011, Douglas W. Bell This is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, either Version 2 or any later version. This program is distributed in the hope that it will be useful, but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. ***************************************************************************** """ __progname__ = 'TreeLine' __version__ = '1.4.1' __author__ = 'Doug Bell' helpFilePath = None # modified by install script if required iconPath = None # modified by install script if required templatePath = None # modified by install script if required translationPath = 'translations' import sys import signal import getopt import os.path import locale import __builtin__ from PyQt4 import QtCore, QtGui import globalref def setModulePath(): """Set module path in globalref""" globalref.modPath = unicode(os.path.abspath(sys.path[0]), sys.getfilesystemencoding()) if globalref.modPath.endswith('.zip'): # for py2exe globalref.modPath = os.path.dirname(globalref.modPath) def loadTranslator(fileName, app): """Load and install qt translator, return True if sucessful""" translator = QtCore.QTranslator(app) path = os.path.join(globalref.modPath, translationPath) result = translator.load(fileName, path) if not result: path = os.path.join(globalref.modPath, '..', translationPath) result = translator.load(fileName, path) if not result: path = os.path.join(globalref.modPath, '..', 'i18n', translationPath) result = translator.load(fileName, path) if result: QtCore.QCoreApplication.installTranslator(translator) return True else: print 'Warning: translation file "%s" could not be loaded' % fileName return False def setupTranslator(app): """Set language, load translators and setup translator function""" try: locale.setlocale(locale.LC_ALL, '') except locale.Error: pass globalref.lang = os.environ.get('LC_MESSAGES', '') if not globalref.lang: globalref.lang = os.environ.get('LANG', '') if not globalref.lang: try: globalref.lang = locale.getdefaultlocale()[0] except ValueError: pass if not globalref.lang: globalref.lang = '' numTranslators = 0 if globalref.lang and globalref.lang[:2] not in ['C', 'en']: numTranslators += loadTranslator('qt_%s' % globalref.lang, app) numTranslators += loadTranslator('treeline_%s' % globalref.lang, app) def translate(text, comment=''): """Translation function that sets context to calling module's filename""" try: frame = sys._getframe(1) fileName = frame.f_code.co_filename finally: del frame context = os.path.basename(os.path.splitext(fileName)[0]) return unicode(QtCore.QCoreApplication.translate(context, text, comment)) def markNoTranslate(text, comment=''): return text if numTranslators: __builtin__._ = translate else: __builtin__._ = markNoTranslate __builtin__.N_ = markNoTranslate def setLocalEncoding(): """Store locale's default text encoding in globalref.localTextEncoding""" try: # not reliable? globalref.localTextEncoding = locale.getpreferredencoding() 'test'.encode(globalref.localTextEncoding) except (AttributeError, LookupError, TypeError, locale.Error): try: # not available on windows globalref.localTextEncoding = locale.nl_langinfo(locale.CODESET) 'test'.encode(globalref.localTextEncoding) except (AttributeError, LookupError, TypeError, locale.Error): try: globalref.localTextEncoding = locale.getdefaultlocale()[1] 'test'.encode(globalref.localTextEncoding) except (AttributeError, LookupError, TypeError, locale.Error): globalref.localTextEncoding = 'utf-8' def main(): userStyle = '-style' in ' '.join(sys.argv) app = QtGui.QApplication(sys.argv) setModulePath() setupTranslator(app) # must be before importing any treeline modules setLocalEncoding() import treedoc from cmdline import CmdLine import treecontrol import treemainwin if not treedoc.testXmlParser(): QtGui.QMessageBox.critical(None, _('Error'), _('Error loading XML Parser\n'\ 'See TreeLine ReadMe file'), 1, 0) sys.exit(3) try: opts, args = getopt.gnu_getopt(sys.argv, CmdLine.options, CmdLine.longOptions) except getopt.GetoptError: import cmdline cmdline.printUsage() sys.exit(2) args = args[1:] treeControl = treecontrol.TreeControl(userStyle) if opts: CmdLine(opts, args) else: treeControl.firstWindow(args) signal.signal(signal.SIGINT, signal.SIG_IGN) app.exec_() if __name__ == '__main__': main() TreeLine/source/undo.py0000644000175000017500000002471011651514477014101 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # undo.py, provides a classes to store and execute undo & redo operations # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import copy from treeitem import TreeItem import globalref class UndoRedoStore(object): """Stores list of info needed for undo or redo steps""" def __init__(self, levels=-1): self.levels = levels if self.levels < 0: self.levels = globalref.options.intData('UndoLevels', 0, 999) self.undoList = [] self.tmpRedoRef = None # ref for clearing redo list when undos added def addUndoObj(self, obj, clearRedo=True): """Add undo/redo object to list, truncate if longer than levels""" self.undoList.append(obj) if self.levels: self.undoList = self.undoList[-self.levels:] else: self.undoList = [] if self.tmpRedoRef and clearRedo: self.tmpRedoRef.undoList = [] self.tmpRedoRef = None def addDataUndo(self, items, skipSame=False, clearRedo=True): """Syntactic sugar, adds tree item data changes, skips adding item if skipSame is true and a single item matches""" if skipSame and self.undoList and \ isinstance(self.undoList[-1], DataUndo) and \ len(self.undoList[-1].dataList) == 1 and \ items == self.undoList[-1].dataList[0][0]: return self.addUndoObj(DataUndo(items), clearRedo) def addChildListUndo(self, items, skipSame=False, clearRedo=True): """Syntactic sugar, adds tree item child list changes, skips adding item if skipSame is true and a single item matches""" if skipSame and self.undoList and \ isinstance(self.undoList[-1], ChildListUndo) and \ len(self.undoList[-1].dataList) == 1 and \ items == self.undoList[-1].dataList[0][0]: return self.addUndoObj(ChildListUndo(items), clearRedo) def addParentListUndo(self, items, skipSame=False, clearRedo=True): """Adds child list changes from the parents of items, skips adding item if skipSame is true and a single item matches""" if isinstance(items, TreeItem): items = [items] if skipSame and self.undoList and \ isinstance(self.undoList[-1], ChildListUndo) and \ len(self.undoList[-1].dataList) == 1 and len(items) == 1 and \ items[0].parent == self.undoList[-1].dataList[0][0]: return self.addUndoObj(ChildListUndo([item.parent for item in items]), clearRedo) def addTypeUndo(self, items, clearRedo=True): """Syntactic sugar, adds tree item type setting changes""" self.addUndoObj(TypeUndo(items), clearRedo) def addParamUndo(self, varList, clearRedo=True): """Syntactic sugar, adds general immutable param changes""" self.addUndoObj(ParamUndo(varList), clearRedo) def addFormatUndo(self, treeFormats, fileInfoFormat, fieldRenameDict, typeRenameDict, clearRedo=True): """Syntactic sugar, adds type formatting changes""" self.addUndoObj(FormatUndo(treeFormats, fileInfoFormat, fieldRenameDict, typeRenameDict), clearRedo) def addBranchUndo(self, items, clearRedo=True): """Syntactic sugar, adds tree branch changes""" self.addUndoObj(BranchUndo(items), clearRedo) def undo(self, redoRef=None): """Restore saved state for next undo item, add undo item to redoRef, update selection and views""" if self.undoList: self.tmpRedoRef = redoRef obj = self.undoList.pop() obj.undo(redoRef) globalref.docRef.modified = obj.docModified globalref.docRef.selection = obj.selection globalref.docRef.selection.selectEmptyCurrent() globalref.updateViewAll() def removeLastUndo(self): """Remove most recent undo from list""" self.undoList = self.undoList[:-1] class UndoBase(object): """Base class for undo/redo info""" def __init__(self): self.dataList = [] # list of changes self.selection = globalref.docRef.selection[:] # store selected nodes self.docModified = globalref.docRef.modified class DataUndo(UndoBase): """Info for undo/redo of tree item data changes""" def __init__(self, items): """Pass an item or list of items with data to save""" UndoBase.__init__(self) if isinstance(items, TreeItem): items = [items] for item in items: self.dataList.append((item, item.data.copy())) def undo(self, redoRef=None): """Restore saved state""" if redoRef: redoRef.addDataUndo([tuple[0] for tuple in self.dataList], False, False) for item, data in self.dataList: item.data = data class ChildListUndo(UndoBase): """Info for undo/redo of tree item child lists""" def __init__(self, items): """Pass an item or list of items with child lists to save""" UndoBase.__init__(self) if isinstance(items, TreeItem): items = [items] for item in items: if item: self.dataList.append((item, item.childList[:])) def undo(self, redoRef=None): """Restore saved state""" if redoRef: redoRef.addChildListUndo([tuple[0] for tuple in self.dataList], False, False) for item, childList in self.dataList: item.childList = childList for child in childList: child.parent = item class TypeUndo(UndoBase): """Info for undo/redo of tree item type settings and data (for title change)""" def __init__(self, items): """Pass an item or list of items with type settings to save""" UndoBase.__init__(self) if isinstance(items, TreeItem): items = [items] for item in items: self.dataList.append((item, item.formatName, item.data.copy())) def undo(self, redoRef=None): """Restore saved state""" if redoRef: redoRef.addTypeUndo([tuple[0] for tuple in self.dataList], False) for item, format, data in self.dataList: item.formatName = format item.data = data class ParamUndo(UndoBase): """Info for undo/redo of general immutable parameters""" def __init__(self, varList): """Pass a list containing a tuple for each variable, each tuple has the variable's owner and name string""" UndoBase.__init__(self) for varOwner, varName in varList: value = varOwner.__dict__[varName] self.dataList.append((varOwner, varName, value)) def undo(self, redoRef=None): """Restore saved state""" if redoRef: redoRef.addParamUndo([tuple[:2] for tuple in self.dataList], False) for varOwner, varName, value in self.dataList: varOwner.__dict__[varName] = value class FormatUndo(UndoBase): """Info for undo/redo of type formatting changes""" def __init__(self, treeFormats, fileInfoformat, fieldRenameDict, typeRenameDict): """Pass the full list of type formats to save""" UndoBase.__init__(self) self.treeFormats = copy.deepcopy(treeFormats) self.fileInfoFormat = copy.deepcopy(fileInfoformat) self.fieldRenameDict = {} for typeName, values in fieldRenameDict.items(): # swap dict around self.fieldRenameDict[typeName] = [] for origField, newField in values: self.fieldRenameDict[typeName].append((newField, origField)) self.typeRenameDict = {} for origType, newType in typeRenameDict.items(): #swap type dict around self.typeRenameDict[newType] = origType def undo(self, redoRef=None): """Restore saved state""" if redoRef: redoRef.addFormatUndo(globalref.docRef.treeFormats, globalref.docRef.fileInfoFormat, self.fieldRenameDict, self.typeRenameDict, False) globalref.docRef.treeFormats = self.treeFormats globalref.docRef.fileInfoFormat = self.fileInfoFormat if self.typeRenameDict: globalref.docRef.treeFormats.renameFormats(self.typeRenameDict) if self.fieldRenameDict: globalref.docRef.treeFormats.renameFields(self.fieldRenameDict) globalref.docRef.treeFormats.updateDerivedTypes() globalref.docRef.treeFormats.updateAllLineFields() globalref.docRef.treeFormats.updateAutoChoices() globalref.docRef.treeFormats.updateUniqueID(True) class BranchUndo(UndoBase): """Info for undo/redo of tree branches, all data, children, etc., and full format""" def __init__(self, items): """Pass an item or list of items that are the roots of the branches""" UndoBase.__init__(self) self.treeFormats = copy.deepcopy(globalref.docRef.treeFormats) if isinstance(items, TreeItem): items = [items] self.origItems = items for parent in items: for item in parent.descendantGen(): self.dataList.append((item, item.data.copy(), item.childList[:])) def undo(self, redoRef=None): """Restore saved state""" if redoRef and len(self.dataList) == 1: redoRef.addBranchUndo(self.origItems, False) globalref.docRef.treeFormats = self.treeFormats globalref.docRef.treeFormats.updateDerivedTypes() globalref.docRef.treeFormats.updateAllLineFields() for item, data, childList in self.dataList: item.data = data item.childList = childList for child in childList: child.parent = item globalref.docRef.treeFormats.updateAutoChoices() globalref.docRef.treeFormats.updateUniqueID(True) TreeLine/source/treeformats.py0000644000175000017500000001550511651514477015471 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treeformats.py, provides non-GUI base classes for storing node format info # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import sys import nodeformat import treedoc import globalref class TreeFormats(dict): """A dict to store node formats by name in the doc""" rootFormatDefault = _('ROOT', 'root format default name') formatDefault = _('DEFAULT', 'default format name') fieldDefault = _('Name', 'default field name') textFieldName = _('Text', 'text field name') linkFieldName = _('Link', 'link field name') def __init__(self, initDict=None, setDefaults=False): if not initDict: initDict = {} dict.__init__(self, initDict) self.derivedDict = {} self.hasConditionals = False if setDefaults: self[TreeFormats.rootFormatDefault] = \ nodeformat.NodeFormat(TreeFormats.rootFormatDefault, {}, TreeFormats.fieldDefault) self[TreeFormats.formatDefault] = \ nodeformat.NodeFormat(TreeFormats.formatDefault, {}, TreeFormats.fieldDefault) self[TreeFormats.rootFormatDefault].childType = \ TreeFormats.formatDefault def nameList(self, excludeFileInfo=False): """Return sorted list of names of format items""" names = self.keys() if excludeFileInfo and nodeformat.FileInfoFormat.name in self: names.remove(nodeformat.FileInfoFormat.name) names.sort() return names def addIfMissing(self, format): """Add format to list if not a duplicate""" self.setdefault(format.name, format) def removeQuiet(self, format): """Remove from list if there""" try: del self[format.name] except KeyError: pass def renameFields(self, nameDict): """Rename data fields for all items, doesn't change node format""" for format in self.values(): if format.genericType in nameDict: nameDict[format.name] = nameDict[format.genericType] for item in globalref.docRef.root.descendantGen(): for oldName, newName in nameDict.get(item.formatName, []): if oldName in item.data: item.data[newName] = item.data[oldName] del item.data[oldName] def renameFormats(self, nameDict): """Rename format types referenced by nodes""" for item in globalref.docRef.root.descendantGen(): item.formatName = nameDict.get(item.formatName, item.formatName) def updateAllLineFields(self): """Re-find fields to update format lines for any changes in the fieldLists""" for format in self.values(): format.updateLineFields() globalref.docRef.fileInfoFormat.updateLineFields() def updateAutoChoices(self): """Update auto menu choices for all AutoChoice fields""" autoFields = {} for format in self.values(): fields = format.findAutoChoiceFields() if fields: autoFields[format.name] = fields if autoFields: for item in globalref.docRef.root.descendantGen(): for field in autoFields.get(item.formatName, []): field.addChoice(item.data.get(field.name, '')) for fieldList in autoFields.values(): for field in fieldList: field.sortChoices() def updateUniqueID(self, updateAll=False): """Adds UniqueID fields to list to be updated, if updateAll, fills in any blank UniqueID fields""" fieldsFound = False for format in self.values(): if format.findUniqueIDFields(): fieldsFound = True if updateAll and fieldsFound: globalref.docRef.root.setDescendantUniqueID() def updateDerivedTypes(self): """Update fields for all derived types, update the dict containing lists of type families and update fields used in conditionals""" self.hasConditionals = False self.derivedDict = {} for format in self.values(): if format.genericType: format.updateFromGeneric() generic = self[format.genericType] if format.genericType in self.derivedDict: self.derivedDict[format.genericType].append(format) else: self.derivedDict[format.genericType] = [generic, format] if format.conditional: self.hasConditionals = True format.conditional.setupFields(format) def configCopy(self, fileRef, password=''): """Copy the configuration from another TreeLine file""" if hasattr(fileRef, 'read'): fileName = unicode(fileRef.name, sys.getfilesystemencoding()) else: fileName = fileRef origDocRef = globalref.docRef refDoc = treedoc.TreeDoc() if password: refDoc.setPassword(fileName, password) try: refDoc.readFile(fileRef) except: globalref.docRef = origDocRef raise globalref.docRef = origDocRef origDocRef.undoStore.addFormatUndo(self, origDocRef.fileInfoFormat, {}, {}) for newFormat in refDoc.treeFormats.values(): format = self.get(newFormat.name, None) if format: format.duplicateSettings(newFormat) else: self[newFormat.name] = newFormat self.updateAutoChoices() self.updateUniqueID() self.updateDerivedTypes() globalref.docRef.modified = True globalref.updateViewAll() def commonFields(self, itemList): """Return names of fields that are common to all nodes in list""" if not itemList: return [] typeList = [] for item in itemList: if item.formatName not in typeList: typeList.append(item.formatName) fieldNames = self[typeList.pop(0)].fieldNames() for type in typeList: typeFields = self[type].fieldNames() for field in fieldNames[:]: if field not in typeFields: fieldNames.remove(field) return fieldNames TreeLine/source/treecontrol.py0000644000175000017500000005237511656116331015474 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treecontrol.py, provides a class for control of the main windows # # TreeLine, an information storage program # Copyright (C) 2009, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import sys import os.path from PyQt4 import QtCore, QtGui, QtNetwork try: from __main__ import __version__, iconPath except ImportError: __version__ = '??' iconPath = None import globalref import treedoc import treemainwin import treedialogs import option import optiondefaults import icondict import recentfiles class TreeControl(object): """Program and main window control""" def __init__(self, userStyle): self.windowList = [] globalref.treeControl = self self.serverSocket = None mainVersion = '.'.join(__version__.split('.')[:2]) globalref.options = option.Option(u'treeline-%s' % mainVersion, 21) globalref.options.loadAll(optiondefaults.defaultOutput()) iconPathList = [iconPath, os.path.join(globalref.modPath, u'icons/'), os.path.join(globalref.modPath, u'../icons/')] if not iconPath: del iconPathList[0] globalref.treeIcons = icondict.IconDict() globalref.treeIcons.addIconPath([os.path.join(path, u'tree') for path in iconPathList]) globalref.treeIcons.addIconPath([globalref.options.iconPath]) treemainwin.TreeMainWin.toolIcons = icondict.IconDict() treemainwin.TreeMainWin.toolIcons.\ addIconPath([os.path.join(path, u'toolbar') for path in iconPathList], [u'', u'32x32', u'16x16']) treemainwin.TreeMainWin.toolIcons.loadAllIcons() windowIcon = globalref.treeIcons.getIcon(u'treeline') if windowIcon: QtGui.QApplication.setWindowIcon(windowIcon) if not userStyle: if sys.platform.startswith('dar'): QtGui.QApplication.setStyle('macintosh') elif not sys.platform.startswith('win'): QtGui.QApplication.setStyle('plastique') self.recentFiles = recentfiles.RecentFileList() qApp = QtGui.QApplication.instance() qApp.connect(qApp, QtCore.SIGNAL('focusChanged(QWidget*, QWidget*)'), self.updateFocus) def getSocket(self): """Open a socket from another TreeLine process, focus or open the applicable file""" socket = self.serverSocket.nextPendingConnection() if socket and socket.waitForReadyRead(1000): data = unicode(socket.readAll(), globalref.localTextEncoding) if data.startswith('[') and data.endswith(']'): fileNames = eval(data) if fileNames: self.openMultipleFiles(fileNames) else: globalref.mainWin.activateWindow() globalref.mainWin.raise_() def firstWindow(self, fileNames): """Open first main window""" try: # check for existing TreeLine session socket = QtNetwork.QLocalSocket() socket.connectToServer('treeline-session', QtCore.QIODevice.WriteOnly) # if found, send files to open and exit TreeLine if socket.waitForConnected(1000): socket.write(repr(fileNames)) if socket.waitForBytesWritten(1000): socket.close() sys.exit(0) qApp = QtGui.QApplication.instance() # start local server to listen for attempt to start new session self.serverSocket = QtNetwork.QLocalServer() self.serverSocket.listen('treeline-session') qApp.connect(self.serverSocket, QtCore.SIGNAL('newConnection()'), self.getSocket) except AttributeError: print 'Warning: Could not create local socket' if fileNames: fileNames = [unicode(fileName, globalref.localTextEncoding) for fileName in fileNames] self.openMultipleFiles(fileNames) else: win = treemainwin.TreeMainWin() self.windowList.append(win) self.updateWinMenu() self.autoOpen() win.show() globalref.setStatusBar(_('Ready'), 2000) def openMultipleFiles(self, fileNames): """Open files in multiple windows if unique""" for fileName in fileNames: fileName = os.path.abspath(fileName) if self.matchingWindows(fileName): win = self.matchingWindows(fileName)[0] win.activateWindow() win.raise_() else: win = treemainwin.TreeMainWin() self.windowList.append(win) self.openFile(fileName, False) win.show() QtGui.QApplication.alert(win) def autoOpen(self): """Open last used file""" if globalref.options.boolData('AutoFileOpen') and \ self.recentFiles: path = self.recentFiles[0].path if path and not self.openFile(path, False, False): self.recentFiles.removeEntry(path) elif not self.recentFiles and \ globalref.options.intData('RecentFiles', 0, 99): globalref.mainWin.show() # prompt for template if no recent files globalref.mainWin.fileNew(False) def recentOpen(self, filePath): """Open from recentFiles call""" if filePath and self.savePrompt(): if not self.openFile(filePath): self.recentFiles.removeEntry(filePath) def openFile(self, filePath, newWinOk=True, importOnFail=True, addToRecent=True): """Open given file, fail quietly if not importOnFail, return True on success or user cancel, return False on failure (to remove from recent files)""" if self.matchingWindows(filePath): win = self.matchingWindows(filePath)[0] win.activateWindow() win.raise_() return True oldWin = globalref.mainWin if newWinOk and globalref.options.boolData('OpenNewWindow') and \ (globalref.docRef.fileName or globalref.docRef.modified): win = treemainwin.TreeMainWin() else: win = globalref.mainWin if not self.checkAutoSave(filePath): return True QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) try: win.doc = treedoc.TreeDoc(filePath) win.fileImported = False except treedoc.PasswordError: QtGui.QApplication.restoreOverrideCursor() dlg = treedialogs.PasswordEntry(False, win) if dlg.exec_() != QtGui.QDialog.Accepted: globalref.updateRefs(oldWin) return True win.doc.setPassword(filePath, dlg.password) result = self.openFile(filePath, False, importOnFail) if not dlg.saveIt: win.doc.clearPassword(filePath) return result except (IOError, UnicodeError): QtGui.QApplication.restoreOverrideCursor() QtGui.QMessageBox.warning(win, 'TreeLine', _('Error - could not read file "%s"') % filePath) globalref.updateRefs(oldWin) return False except treedoc.ReadFileError, e: QtGui.QApplication.restoreOverrideCursor() if not importOnFail: globalref.updateRefs(oldWin) return True # assume file is not a TreeLine file importType = self.chooseImportType() if not importType: globalref.updateRefs(oldWin) return True try: QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) win.doc = treedoc.TreeDoc(filePath, False, importType) except treedoc.ReadFileError, e: QtGui.QApplication.restoreOverrideCursor() QtGui.QMessageBox.warning(win, 'TreeLine', _('Error - %s') % e) globalref.updateRefs(oldWin) return False win.fileImported = True win.doc.root.open = True QtGui.QApplication.restoreOverrideCursor() if win not in self.windowList: self.windowList.append(win) win.updateForFileChange(addToRecent) if win.pluginInterface: win.pluginInterface.execCallback(win.pluginInterface. fileOpenCallbacks) win.show() return True def chooseImportType(self): """Show dialog for selecting file import type""" choices = [(_('Tab &indented text, one node per line'), treedoc.tabbedImport), (_('Text &table with header row, one node per line'), treedoc.tableImport), (_('Plain text, one &node per line (CR delimitted)'), treedoc.textLineImport), (_('Plain text ¶graphs (blank line delimitted)'), treedoc.textParaImport), (_('Treepad &file (text nodes only)'), treedoc.treepadImport), (_('&XML bookmarks (XBEL format)'), treedoc.xbelImport), (_('&HTML bookmarks (Mozilla format)'), treedoc.mozillaImport), (_('&Generic XML (Non-TreeLine file)'), treedoc.xmlImport), (_('Open Document (ODF) text'), treedoc.odfImport)] dlg = treedialogs.RadioChoiceDlg(_('Import Text'), _('Choose Text Import Method'), choices, globalref.mainWin) if dlg.exec_() != QtGui.QDialog.Accepted: return None return dlg.getResult() def newFile(self, templatePath='', newWinOk=True): """Open a new file""" if newWinOk and globalref.options.boolData('OpenNewWindow') and \ (globalref.docRef.fileName or globalref.docRef.modified): win = treemainwin.TreeMainWin() self.windowList.append(win) else: win = globalref.mainWin if templatePath: try: win.doc = treedoc.TreeDoc(templatePath) win.doc.root.open = True win.doc.fileName = '' win.doc.fileInfoFormat.updateFileInfo() except (treedoc.PasswordError, IOError, UnicodeError, treedoc.ReadFileError): QtGui.QMessageBox.warning(win, 'TreeLine', _('Error - could not read template file "%s"') \ % templatePath) win.doc = treedoc.TreeDoc() else: win.doc = treedoc.TreeDoc() win.updateForFileChange(False) if win.pluginInterface: win.pluginInterface.execCallback(win.pluginInterface. fileNewCallbacks) win.show() def saveFile(self, fileName): """Save file to fileName, return True on success""" win = globalref.mainWin unsetPassword = False if win.doc.encryptFile and not win.doc.hasPassword(fileName): dlg = treedialogs.PasswordEntry(True, win) if dlg.exec_() != QtGui.QDialog.Accepted: return False win.doc.setPassword(fileName, dlg.password) unsetPassword = not dlg.saveIt try: win.doc.writeFile(fileName) except IOError: QtGui.QMessageBox.warning(win, 'TreeLine', _('Error - Could not write to %s') % fileName) return False if unsetPassword: win.doc.clearPassword(fileName) win.updateCmdAvail() self.delAutoSaveFile() self.resetAutoSave() if win.pluginInterface: win.pluginInterface.execCallback(win.pluginInterface. fileSaveCallbacks) return True def resetAutoSave(self): """Restart auto save timer if the option is enabled""" globalref.mainWin.autoSaveTimer.stop() minutes = globalref.options.intData('AutoSaveMinutes', 0, 999) if minutes: globalref.mainWin.autoSaveTimer.start(60000 * minutes) def autoSave(self): """Perform auto save if the option is enabled (called from timer)""" win = globalref.mainWin if win.doc.modified and win.doc.fileName and not win.fileImported: unsetPassword = False if win.doc.encryptFile and \ not win.doc.hasPassword(win.doc.fileName): dlg = PasswordEntry(True, win) if dlg.exec_() != QtGui.QDialog.Accepted: return win.doc.setPassword(win.doc.fileName, dlg.password) unsetPassword = not dlg.saveIt try: win.doc.writeFile(win.doc.fileName + '~', False) except IOError: pass if unsetPassword: win.doc.clearPassword(win.doc.fileName) def checkAutoSave(self, filePath): """Check for presence of auto save file & prompt user, return True if OK to continue""" if not globalref.options.intData('AutoSaveMinutes', 0, 999): return True autoSaveFile = self.autoSaveFilePath(filePath) if not autoSaveFile: return True ans = QtGui.QMessageBox.information(self, 'TreeLine', _('Backup file "%s" exists.\n'\ 'A previous session may '\ 'have crashed.') % autoSaveFile, _('&Restore Backup'), _('&Delete Backup'), _('&Cancel File Open'), 0, 2) if ans == 0: if not self.restoreAutoSaveFile(filePath): QtGui.QMessageBox.warning(self, 'TreeLine', _('Error - could not restore '\ 'backup')) return False elif ans == 1: self.delAutoSaveFile(filePath) return True else: return False def autoSaveFilePath(self, baseName=''): """Return the path to a backup file if it exists""" filePath = baseName and baseName or globalref.docRef.fileName filePath = filePath + '~' if len(filePath) > 1 and \ os.access(filePath.encode(sys.getfilesystemencoding()), os.R_OK): return filePath return '' def delAutoSaveFile(self, baseName=''): """Remove the backup auto save file if it exists""" filePath = self.autoSaveFilePath(baseName) if filePath: try: os.remove(filePath) except OSError: print 'Could not remove backup file %s' % \ filePath.encode(globalref.localTextEncoding) def restoreAutoSaveFile(self, baseName): """Open backup file, then move baseName~ to baseName by overwriting, return True on success""" fileName = baseName + '~' self.openFile(fileName, False, False, False) if globalref.docRef.fileName != fileName: return False try: os.remove(baseName) except OSError: print 'Could not remove file %s' % \ baseName.encode(globalref.localTextEncoding) return False try: os.rename(fileName, baseName) except OSError: print 'Could not rename file %s' % \ fileName.encode(globalref.localTextEncoding) return False globalref.docRef.fileName = baseName globalref.mainWin.setMainCaption() return True def savePrompt(self, closing=False): """Ask for save if doc modified, return false on cancel""" win = globalref.mainWin if not self.duplicateWindows(): if win.doc.modified and (closing or not globalref.options. boolData('OpenNewWindow')): text = win.fileImported and _('Save changes?') or \ _('Save changes to "%s"?') % win.doc.fileName ans = QtGui.QMessageBox.information(win, 'TreeLine', text, _('&Yes'), _('&No'), _('&Cancel'), 0, 2) if ans == 0: win.fileSave() elif ans == 1: self.delAutoSaveFile() return True else: return False if globalref.options.boolData('PersistTreeState'): self.recentFiles.saveTreeState(win.treeView) return True def newWindow(self): """Create a new window viewing the current file""" globalref.mainWin.saveMultiWinTree() doc = globalref.mainWin.doc win = treemainwin.TreeMainWin() self.windowList.append(win) win.doc = doc win.updateForFileChange(False) win.show() def closeWindow(self): """Close the current window without exiting""" if self.windowCount() > 1: globalref.mainWin.close() elif self.savePrompt(): self.newFile('', False) def duplicateWindows(self): """Return list of windows with the same file as the active window""" return [win for win in self.windowList if win != globalref.mainWin and win.doc.fileName == globalref.mainWin.doc.fileName] def matchingWindows(self, fileName): """Return list of windows with the given file name""" return [win for win in self.windowList if win.doc.fileName == fileName] def removeWin(self, win): """Remove given windoww from the window list""" self.windowList.remove(win) def windowCount(self): """Return the current number of windows""" return len(self.windowList) def updateWinMenu(self): """Update the window list menu in the active main window""" mainWin = globalref.mainWin for action in mainWin.winMenu.actions(): if hasattr(action, 'refWin'): mainWin.winMenu.removeAction(action) num = 1 for win in self.windowList: action = WindowAction(mainWin, win, num) mainWin.winMenu.addAction(action) if win is mainWin: action.setChecked(True) num += 1 def updateDialogs(self): """Update non-modal dialogs in response to their close signal""" globalref.mainWin.updateNonModalDialogs() def updateFocus(self): """Check for focus change to a different main window""" win = QtGui.QApplication.activeWindow() while win and win.parent(): win = win.parent() if win and win != globalref.mainWin and win in self.windowList: oldWin = globalref.mainWin if self.duplicateWindows(): oldWin.saveMultiWinTree() globalref.updateRefs(win) self.recentFiles.updateMenu() self.updateWinMenu() if self.duplicateWindows(): win.updateMultiWinTree() win.updateNonModalDialogs() def forceUpdateWindow(self): """Update an alternate window that shows the same file""" oldWin = globalref.mainWin oldWin.saveMultiWinTree() for win in self.duplicateWindows(): globalref.updateRefs(win) win.updateMultiWinTree() globalref.updateRefs(oldWin) oldWin.updateMultiWinTree() class WindowAction(QtGui.QAction): """Menu item for a window entry""" maxLength = 30 def __init__(self, parent, refWin, num): QtGui.QAction.__init__(self, parent) self.refWin = refWin self.setText('&%d %s' % (num, self.abbrevPath())) self.setStatusTip(refWin.doc.fileName) self.setCheckable(True) self.connect(self, QtCore.SIGNAL('triggered()'), self.raiseWin) def abbrevPath(self): """Return shortened version of path""" abbrevPath = self.refWin.doc.fileName if len(abbrevPath) > WindowAction.maxLength: truncLength = WindowAction.maxLength - 3 pos = abbrevPath.find(os.sep, len(abbrevPath) - truncLength) if pos < 0: pos = len(abbrevPath) - truncLength abbrevPath = '...' + abbrevPath[pos:] return abbrevPath def raiseWin(self): """Raise referenced window""" self.refWin.activateWindow() self.refWin.raise_() TreeLine/source/option.py0000644000175000017500000002205611651514477014445 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # option.py, provides classes to read and set user preferences # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import sys import os.path import codecs import globalref class Option(object): """Stores and retrieves string options""" def __init__(self, dirName, keySpaces=20): self.path = '' self.pluginPath = '' self.iconPath = '' self.templatePath = '' if dirName: fileName = dirName.split('-')[0] if sys.platform.startswith('win'): self.path = unicode(os.environ.get('APPDATA', ''), sys.getfilesystemencoding()) altPath = os.path.join(globalref.modPath, u'%s.ini' % fileName) if self.path and os.path.exists(self.path): self.path = os.path.join(self.path, u'bellz', dirName) if (os.path.exists(self.path) or not os.path.exists(altPath)) and self.createDirs(): self.path = os.path.join(self.path, u'%s.ini' % fileName) else: self.path = altPath else: self.path = altPath else: self.path = unicode(os.environ.get('HOME', ''), sys.getfilesystemencoding()) if self.path and os.path.exists(self.path): self.path = os.path.join(self.path, u'.%s' % dirName) if self.createDirs(): self.path = os.path.join(self.path, u'%src' % fileName) else: self.path = '' self.keySpaces = keySpaces self.dfltDict = {} self.userDict = {} self.dictList = (self.userDict, self.dfltDict) self.chgList = [] def createDirs(self): """Create option, plugin, icon & template directories if necessary and save path locations. Return True on success.""" try: if not os.path.isdir(self.path): if os.path.isfile(self.path): os.remove(self.path) os.makedirs(self.path) self.pluginPath = os.path.join(self.path, u'plugins') if not os.path.isdir(self.pluginPath): os.makedirs(self.pluginPath) self.iconPath = os.path.join(self.path, u'icons') if not os.path.isdir(self.iconPath): os.makedirs(self.iconPath) self.templatePath = os.path.join(self.path, u'templates') if not os.path.isdir(self.templatePath): os.makedirs(self.templatePath) except OSError: self.path = '' self.pluginPath = '' self.iconPath = '' self.templatePath = '' return False return True def loadAll(self, defaultList): """Reads defaultList & file, writes file if required return true if file read""" self.loadSet(defaultList, self.dfltDict) if self.path: try: f = codecs.open(self.path, 'r', 'utf-8') except IOError: try: f = codecs.open(self.path, 'w', 'utf-8') except IOError: print 'Error - could not write to config file', \ self.path.encode(globalref.localTextEncoding) self.path = '' else: f.writelines([line + '\n' for line in defaultList]) f.close() else: self.loadSet(f.readlines(), self.userDict) f.close() return True return False def loadSet(self, list, data): """Reads settings from list into dict""" for line in list: line = line.split('#', 1)[0].strip() if line: item = line.split(None, 1) + [''] # add value if blank data[item[0]] = item[1].strip() def addData(self, key, strData, storeChange=0): """Add new entry, add to write list if storeChange""" self.userDict[key] = strData if storeChange: self.chgList.append(key) def boolData(self, key, defaultOnly=False): """Returns true or false from yes or no in option data""" dictList = defaultOnly and (self.dfltDict,) or self.dictList for data in dictList: val = data.get(key) if val and val[0] in ('y', 'Y'): return True if val and val[0] in ('n', 'N'): return False print 'Option error - bool key', key, 'is not valid' return False def numData(self, key, min=None, max=None, defaultOnly=False): """Return float from option data""" dictList = defaultOnly and (self.dfltDict,) or self.dictList for data in dictList: val = data.get(key) if val: try: num = float(val) if (min == None or num >= min) and \ (max == None or num <= max): return num except ValueError: pass print 'Option error - float key', key, 'is not valid' return False def intData(self, key, min=None, max=None, defaultOnly=False): """Return int from option data""" dictList = defaultOnly and (self.dfltDict,) or self.dictList for data in dictList: val = data.get(key) if val: try: num = int(val) if (min == None or num >= min) and \ (max == None or num <= max): return num except ValueError: pass print 'Option error - int key', key, 'is not valid' return False def strData(self, key, emptyOk=0, defaultOnly=False): """Return string from option data""" dictList = defaultOnly and (self.dfltDict,) or self.dictList for data in dictList: val = data.get(key) if val != None: if val or emptyOk: return val print 'Option error - string key', key, 'is not valid' return '' def changeData(self, key, strData, storeChange): """Change entry, add to write list if storeChange Return true if changed""" for data in self.dictList: val = data.get(key) if val != None: if strData == val: # no change reqd return False self.userDict[key] = strData if storeChange: self.chgList.append(key) return True print 'Option error - key', key, 'is not valid' return False def addDefaultKey(self, key, initValue=' '): """Add a new value to the default dict if it isn't there, set the init value (set to space by default so it always gets overwritten wit changeData""" for data in self.dictList: if data.has_key(key): return self.dfltDict[key] = initValue def writeChanges(self): """Write any stored changes to the option file - rtn true on success""" if self.path and self.chgList: try: f = codecs.open(self.path, 'r', 'utf-8') fileList = f.readlines() f.close() for key in self.chgList[:]: hitList = [line for line in fileList if line.strip().split(None, 1)[:1] == [key]] if not hitList: hitList = [line for line in fileList if line.replace('#', ' ', 1).strip(). split(None, 1)[:1] == [key]] if hitList: fileList[fileList.index(hitList[-1])] = '%s%s\n' % \ (key.ljust(self.keySpaces), self.userDict[key]) self.chgList.remove(key) for key in self.chgList: fileList.append('%s%s\n' % (key.ljust(self.keySpaces), self.userDict[key])) f = codecs.open(self.path, 'w', 'utf-8') f.writelines([line for line in fileList]) f.close() return True except IOError: print 'Error - could not write to config file', \ self.path.encode(globalref.localTextEncoding) return False TreeLine/source/helpview.py0000644000175000017500000001305511651514477014757 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # helpview.py, provides a window for viewing an html help file # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import os.path import sys import webbrowser from PyQt4 import QtCore, QtGui class HelpView(QtGui.QMainWindow): """Main window for viewing an html help file""" def __init__(self, path, caption, icons, parent=None): """Helpview initialize with text""" QtGui.QMainWindow.__init__(self, parent) self.setAttribute(QtCore.Qt.WA_QuitOnClose, False) self.setWindowFlags(QtCore.Qt.Window) self.setStatusBar(QtGui.QStatusBar()) self.textView = HelpViewer(self) self.setCentralWidget(self.textView) path = os.path.abspath(path) if sys.platform.startswith('win'): path = path.replace('\\', '/') self.textView.setSearchPaths([os.path.dirname(path)]) self.textView.setSource(QtCore.QUrl('file:///%s' % path)) self.resize(520, 440) self.setWindowTitle(caption) tools = self.addToolBar('Tools') self.menu = QtGui.QMenu(self.textView) self.connect(self.textView, QtCore.SIGNAL('highlighted(const QString&)'), self.showLink) backAct = QtGui.QAction(_('&Back'), self) backAct.setIcon(icons['helpback']) tools.addAction(backAct) self.menu.addAction(backAct) self.connect(backAct, QtCore.SIGNAL('triggered()'), self.textView, QtCore.SLOT('backward()')) backAct.setEnabled(False) self.connect(self.textView, QtCore.SIGNAL('backwardAvailable(bool)'), backAct, QtCore.SLOT('setEnabled(bool)')) forwardAct = QtGui.QAction(_('&Forward'), self) forwardAct.setIcon(icons['helpforward']) tools.addAction(forwardAct) self.menu.addAction(forwardAct) self.connect(forwardAct, QtCore.SIGNAL('triggered()'), self.textView, QtCore.SLOT('forward()')) forwardAct.setEnabled(False) self.connect(self.textView, QtCore.SIGNAL('forwardAvailable(bool)'), forwardAct, QtCore.SLOT('setEnabled(bool)')) homeAct = QtGui.QAction(_('&Home'), self) homeAct.setIcon(icons['helphome']) tools.addAction(homeAct) self.menu.addAction(homeAct) self.connect(homeAct, QtCore.SIGNAL('triggered()'), self.textView, QtCore.SLOT('home()')) tools.addSeparator() tools.addSeparator() findLabel = QtGui.QLabel(_(' Find: '), self) tools.addWidget(findLabel) self.findEdit = QtGui.QLineEdit(self) tools.addWidget(self.findEdit) self.connect(self.findEdit, QtCore.SIGNAL('textEdited(const QString &)'), self.findTextChanged) self.connect(self.findEdit, QtCore.SIGNAL('returnPressed()'), self.findNext) self.findPreviousAct = QtGui.QAction(_('Find &Previous'), self) self.findPreviousAct.setIcon(icons['helpprevious']) tools.addAction(self.findPreviousAct) self.menu.addAction(self.findPreviousAct) self.connect(self.findPreviousAct, QtCore.SIGNAL('triggered()'), self.findPrevious) self.findPreviousAct.setEnabled(False) self.findNextAct = QtGui.QAction(_('Find &Next'), self) self.findNextAct.setIcon(icons['helpnext']) tools.addAction(self.findNextAct) self.menu.addAction(self.findNextAct) self.connect(self.findNextAct, QtCore.SIGNAL('triggered()'), self.findNext) self.findNextAct.setEnabled(False) def showLink(self, text): """Send link text to the statusbar""" self.statusBar().showMessage(unicode(text)) def findTextChanged(self, text): """Update find controls based on text in text edit""" text = unicode(text) self.findPreviousAct.setEnabled(len(text) > 0) self.findNextAct.setEnabled(len(text) > 0) def findPrevious(self): """Command to find the previous string""" if self.textView.find(unicode(self.findEdit.text()), QtGui.QTextDocument.FindBackward): self.statusBar().clearMessage() else: self.statusBar().showMessage(_('Text string not found')) def findNext(self): """Command to find the next string""" if self.textView.find(unicode(self.findEdit.text())): self.statusBar().clearMessage() else: self.statusBar().showMessage(_('Text string not found')) class HelpViewer(QtGui.QTextBrowser): """Shows an html help file""" def __init__(self, parent=None): QtGui.QTextBrowser.__init__(self, parent) def setSource(self, url): """Called when user clicks on a URL""" name = unicode(url.toString()) if name.startswith(u'http'): webbrowser.open(name, True) else: QtGui.QTextBrowser.setSource(self, QtCore.QUrl(name)) def contextMenuEvent(self, event): """Init popup menu on right click""""" self.parentWidget().menu.exec_(event.globalPos()) TreeLine/source/icondict.py0000644000175000017500000000744011651514477014731 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # icondict.py, provides a class to load and store tree icons # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import os.path from PyQt4 import QtGui import globalref class IconDict(dict): """Loads and stores icons by name""" iconExt = ['.png', '.bmp', '.ico', 'gif'] defaultName = 'default' noneName = 'NoIcon' def __init__(self): dict.__init__(self, {}) self.pathList = [] self.subPaths = [''] self.allLoaded = False self[IconDict.noneName] = None def addIconPath(self, potentialPaths, subPaths=None): """Add first good path from potentialPaths, set subPaths from a list if given""" if subPaths: self.subPaths = subPaths for path in potentialPaths: for subPath in self.subPaths: try: for name in os.listdir(os.path.join(path, subPath)): pixmap = QtGui.QPixmap(os.path.join(path, subPath, name)) if not pixmap.isNull(): self.pathList.append(path) return except OSError: pass def getIcon(self, name, substitute=False): """Return an icon matching name or the default icon""" try: return self[name] except KeyError: icon = self.loadIcon(name) if not icon and substitute and name != IconDict.defaultName: icon = self.getIcon(IconDict.defaultName) return icon def loadAllIcons(self): """Load all icons available in self.pathList""" self.clear() self[IconDict.noneName] = None for path in self.pathList: for subPath in self.subPaths: try: for name in os.listdir(os.path.join(path, subPath)): pixmap = QtGui.QPixmap(os.path.join(path, subPath, name)) if not pixmap.isNull(): name = os.path.splitext(name)[0] try: icon = self[name] except KeyError: icon = QtGui.QIcon() self[name] = icon icon.addPixmap(pixmap) except OSError: pass self.allLoaded = True def loadIcons(self, iconList): """Load icons from name list""" for iconName in iconList: self.loadIcon(iconName) def loadIcon(self, iconName): """Load icon from iconPath, add to dictionary and return the icon""" icon = QtGui.QIcon() for path in self.pathList: for ext in IconDict.iconExt: fileName = iconName + ext for subPath in self.subPaths: pixmap = QtGui.QPixmap(os.path.join(path, subPath, fileName)) if not pixmap.isNull(): icon.addPixmap(pixmap) if not icon.isNull(): self[iconName] = icon return icon return None TreeLine/source/treeflatview.py0000644000175000017500000003376711651514477015651 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treeflatview.py, provides classes for a flattened "tree" view # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import string from PyQt4 import QtCore, QtGui import globalref class FlatViewItem(QtGui.QListWidgetItem): """Qt flat tree item, contains ref to treecore TreeItem""" def __init__(self, parent, docItemRef): QtGui.QListWidgetItem.__init__(self, parent) self.docItemRef = docItemRef docItemRef.viewData = self self.tempSortKey = None self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) self.setText(docItemRef.title()) self.setTreeIcon() def setTreeIcon(self): """Set tree node icon""" if globalref.options.boolData('ShowTreeIcons'): icon = globalref.treeIcons.getIcon(self.docItemRef. nodeFormat().iconName, True) if icon: self.setIcon(icon) def loadTempSortKey(self): """Calculate a view index for sort key""" self.tempSortKey = self.listWidget().indexFromItem(self).row() class FlatView(QtGui.QListWidget): """Left pane view of flat node structure""" def __init__(self, parent=None): QtGui.QListWidget.__init__(self, parent) self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) self.setEditTriggers(QtGui.QAbstractItemView.SelectedClicked) self.updateGenOptions() self.rootItems = [] self.incremSearchMode = False self.incremSearchStr = '' self.editedItem = None self.noSelectClickCallback = None self.connect(self, QtCore.SIGNAL('itemSelectionChanged()'), self.changeSelected) self.connect(self, QtCore.SIGNAL('currentItemChanged(QListWidgetItem*, '\ 'QListWidgetItem*)'), self.changeCurrent) def updateTree(self, viewSwitched=False): """Replace contents of FlatView from the doc""" if viewSwitched: self.rootItems = globalref.docRef.selection.uniqueBranches() if globalref.docRef.treeFormats.hasConditionals: for root in self.rootItems: root.setDescendantCondTypes() origX, origY = (self.horizontalOffset(), self.verticalOffset()) self.blockSignals(True) self.clear() self.blockSignals(False) for root in self.rootItems: for node in root.descendantGen(): if (not globalref.mainWin.condFilter or globalref.mainWin.condFilter.evaluateType(node)) and \ (not globalref.mainWin.textFilter or node.matchWords(globalref.mainWin.textFilter)): FlatViewItem(self, node) self.blockSignals(True) self.scrollContentsBy(origX, origY) self.updateSelect() self.blockSignals(False) def updateSelect(self): """Update view selection""" self.blockSignals(True) self.clearSelection() selectList = [] for item in globalref.docRef.selection: if hasattr(item.viewData, 'listWidget'): try: item.viewData.text() selectList.append(item) except RuntimeError: pass if len(selectList) < len(globalref.docRef.selection): if not selectList and self.item(0): selectList = [self.item(0).docItemRef] globalref.docRef.selection.replace(selectList) globalref.updateRightView() if selectList: self.setCurrentItem(selectList[-1].viewData) globalref.docRef.selection.currentItem = selectList[-1] self.scrollToItem(selectList[-1].viewData) for node in selectList: self.setItemSelected(node.viewData, True) self.blockSignals(False) def updateTreeItem(self, item): """Update the title and open status of item""" if hasattr(item.viewData, 'listWidget'): try: item.viewData.text() except RuntimeError: return if globalref.docRef.treeFormats.hasConditionals: item.setConditionalType() item.viewData.setTreeIcon() item.viewData.setText(item.title()) def updateGenOptions(self): """Update flat tree option settings""" if globalref.options.boolData('ClickRename'): self.setEditTriggers(QtGui.QAbstractItemView.SelectedClicked) else: self.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) def changeCurrent(self, currentItem, prevItem): """Set current item in selection, called from tree signal""" if currentItem: globalref.docRef.selection.currentItem = currentItem.docItemRef def changeSelected(self): """Set selection based on signal""" selections = self.selectedItems()[:] if len(selections) > 1 and \ globalref.options.strData('SelectOrder') == 'tree': for item in selections: item.loadTempSortKey() selections.sort(lambda x,y: cmp(x.tempSortKey, y.tempSortKey)) globalref.docRef.selection.replace([item.docItemRef for item in selections]) globalref.updateRightView() def edit(self, index, trigger, event): """Override to block editing with multiple selection, also saves ref to edited item to avoid commiting change to wrong item due to next commands""" if len(globalref.docRef.selection) == 1: result = QtGui.QListWidget.edit(self, index, trigger, event) if result: self.editedItem = globalref.docRef.selection[0] return result else: return False def commitData(self, editor): """Change tree based on results of edit operation""" text = unicode(editor.text()) item = globalref.docRef.selection[0] if text and text != item.title() and item == self.editedItem and \ item.setTitle(text, True): QtGui.QListWidget.commitData(self, editor) globalref.updateRightView() self.editedItem = None def treeIncremSearch(self): """Begin iterative search""" self.incremSearchMode = True self.incremSearchStr = '' globalref.setStatusBar(_('Search for:'), 0, True) def doIncremSearch(self): """Search for searchStr in all titles""" globalref.setStatusBar(_('Search for: %s') % self.incremSearchStr, 0, True) if self.findTitleText(self.incremSearchStr): globalref.setStatusBar(_('Search for: %s') % self.incremSearchStr, 0, True) else: globalref.setStatusBar(_('Search for: %s (not found)') % self.incremSearchStr, 0, True) def findText(self, wordList, forward=True): """Select item containing words in searchStr in any field, starts with currentItem, return item if found""" fullList = [self.item(i).docItemRef for i in range(self.count())] currentPos = fullList.index(self.currentItem().docItemRef) fullList = fullList[currentPos+1:] + fullList[:currentPos] if not forward: fullList.reverse() for item in fullList: if item.matchWords(wordList): self.clearSelection() self.setCurrentItem(item.viewData) self.scrollToItem(item.viewData) self.setItemSelected(item.viewData, True) return item return None def findTitleText(self, searchStr, forward=True, includeCurrent=True): """Select item containing search string""" searchStr = searchStr.lower() itemList = [self.item(i) for i in range(self.count())] start = self.currentRow() if forward: if not includeCurrent: start += 1 seqences = [itemList[start:], itemList[:self.currentRow()]] else: if not includeCurrent: start -= 1 seqences = [itemList[start::-1], itemList[-1:self.currentRow():-1]] for partialList in seqences: for item in partialList: if unicode(item.text()).lower().find(searchStr) >= 0: self.clearSelection() self.setCurrentItem(item) self.scrollToItem(item) self.setItemSelected(item, True) return True return False def treeIncremNext(self): """Search for next occurance of increm string""" if self.incremSearchStr: if self.findTitleText(self.incremSearchStr, True, False): globalref.setStatusBar(_('Next: %s') % self.incremSearchStr, 0, True) else: globalref.setStatusBar(_('Next: %s (not found)') % self.incremSearchStr, 0, True) def treeIncremPrev(self): """Search for previous occurance of increm string""" if self.incremSearchStr: if self.findTitleText(self.incremSearchStr, False, False): globalref.setStatusBar(_('Previous: %s') % self.incremSearchStr, 0, True) else: globalref.setStatusBar(_('Previous: %s (not found)') % self.incremSearchStr, 0, True) def showTypeMenu(self): """Show popup menu for changing the item type""" self.scrollToItem(self.currentItem()) rect = self.visualItemRect(self.currentItem()) pt = self.mapToGlobal(QtCore.QPoint(rect.center().x(), rect.bottom())) globalref.mainWin.typeSubMenu.popup(pt) def mousePressEvent(self, event): """Mouse press down event saves selected item for rename""" if self.incremSearchMode: self.incremSearchMode = False globalref.setStatusBar('') clickedItem = self.itemAt(event.pos()) if not clickedItem: # skip unselecting click on blank space return if self.noSelectClickCallback: self.noSelectClickCallback(clickedItem.docItemRef) self.noSelectClickCallback = None return if event.button() == QtCore.Qt.RightButton: return # stop rename when context menu is used QtGui.QListWidget.mousePressEvent(self, event) def mouseReleaseEvent(self, event): """Mouse release event for popup menus""" clickedItem = self.itemAt(event.pos()) if not clickedItem: # skip unselecting click on blank space return # if event.button() == QtCore.Qt.LeftButton and self.editTrigger: # self.editItem(clickedItem) # Qt's edit triggers hit too often QtGui.QListWidget.mouseReleaseEvent(self, event) def contextMenuEvent(self, event): """Show popup menu""" if event.reason() == QtGui.QContextMenuEvent.Mouse: clickedItem = self.itemAt(event.pos()) if not clickedItem: event.ignore() return if not self.isItemSelected(clickedItem): self.blockSignals(True) self.clearSelection() self.blockSignals(False) self.setItemSelected(clickedItem, True) pos = event.globalPos() else: # shown for menu key or other reason if not globalref.docRef.selection: event.ignore() return selectList = globalref.docRef.selection[:] if globalref.docRef.selection.currentItem in selectList: selectList.insert(0, globalref.docRef.selection.currentItem) posList = [self.visualItemRect(item.viewData).bottomLeft() for item in selectList] posList = [pos for pos in posList if self.rect().contains(pos)] if not posList: posList = [QtCore.QPoint(0, 0)] pos = self.mapToGlobal(posList[0]) parentList = [item for item in globalref.docRef.selection if item.childList] if parentList: menu = globalref.mainWin.parentPopup else: menu = globalref.mainWin.childPopup menu.popup(pos) event.accept() def focusOutEvent(self, event): """Stop incremental search on focus loss""" if self.incremSearchMode: self.incremSearchMode = False globalref.setStatusBar('') QtGui.QListWidget.focusOutEvent(self, event) def keyPressEvent(self, event): """Bind keys to functions""" keyText = unicode(event.text()) if self.incremSearchMode: if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, QtCore.Qt.Key_Escape): self.incremSearchMode = False globalref.setStatusBar('') elif event.key() == QtCore.Qt.Key_Backspace and \ self.incremSearchStr: self.incremSearchStr = self.incremSearchStr[:-1] self.doIncremSearch() elif keyText and keyText in string.printable: self.incremSearchStr += keyText self.doIncremSearch() event.accept() else: QtGui.QListWidget.keyPressEvent(self, event) TreeLine/source/output.py0000644000175000017500000002427111651514477014476 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # output.py, provides non-GUI base classes for output of node info # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import re import sys import globalref breakRegExp = re.compile('(|)$') class OutputItem(object): """Tree item rich text output class - stores text, level & height""" def __init__(self, textLines, level): self.textLines = textLines if globalref.docRef.spaceBetween: self.textLines.append(u'') self.level = level self.height = len(self.textLines) # Removed for non-continuous selections # self.firstSibling = False # self.lastSibling = False # self.hasChildren = False self.prefix = '' self.suffix = '' def addBreaks(self): """Add
elements to format lines""" self.textLines = [line + u'
' for line in self.textLines] def addInnerBreaks(self): """Add
elements to all but last format line""" self.textLines[:-1] = [line + u'
' for line in self.textLines[:-1]] def addIndents(self, prevLevel, nextLevel): """Add

elements to format indented lines, return this level""" for num in range(self.level - prevLevel): self.textLines[0] = u'
%s' % self.textLines[0] for num in range(self.level - nextLevel): self.textLines[-1] = u'%s
' % self.textLines[-1] return self.level def addAbsoluteIndents(self, pixels=20): """Add tags for individual indent, since output view does not support nested
""" self.textLines[0] = u'
%s' % \ (pixels * self.level, self.textLines[0]) self.textLines[-1] = u'%s
' % self.textLines[-1] def equalPrefix(self, other): """Return True if prefix and suffix are equivalent""" return self.prefix.strip().upper() == other.prefix.strip().upper() and \ self.suffix.strip().upper() == other.suffix.strip().upper() def textList(self, withPrefix=False, withSuffix=False): """Return list of text lines, optionally with prefix/suffix""" textList = self.textLines[:] if not textList: textList = [''] if withPrefix: textList[0] = self.prefix + textList[0] if withSuffix: textList[-1] += self.suffix return textList class OutputGroup(list): """List of OutputItems - splits into segments & adds indentation""" def __init__(self, initList=[]): list.__init__(self, initList) def __getslice__(self, i, j): """Modified to copy object with slice""" return OutputGroup(list.__getslice__(self, i, j)) def addBreaks(self): """Add
elements to format lines""" for item in self: item.addBreaks() def addInnerBreaks(self): """Add
elements to all but last format line of each item""" for item in self: item.addInnerBreaks() def addIndents(self, prevLevel=0): """Add
elements to indent items""" for num in range(len(self)): nextLevel = 0 if num + 1 < len(self): nextLevel = self[num + 1].level prevLevel = self[num].addIndents(prevLevel, nextLevel) def addPrefix(self): """Add prefix and suffix text to group items""" closeRqd = None for item, next in map(None, self, self[1:]): lastSibling = not next or next.level != item.level if item.prefix and item.textLines and closeRqd == None: item.textLines[0] = item.prefix + item.textLines[0] closeRqd = item.suffix if closeRqd != None and (lastSibling or not item.equalPrefix(next)): if item.textLines: item.textLines[-1] = item.textLines[-1] + closeRqd else: item.textLines = [closeRqd] closeRqd = None def joinPrefixItems(self): """Merges adjacent items with same prefix and level for printing""" newList = [] mergeList = OutputGroup() for item in self: if mergeList and (item.level != mergeList[0].level or not item.prefix or not item.equalPrefix(mergeList[0])): mergeList.mergeGroup() newList.append(mergeList[0]) mergeList[:] = [] mergeList.append(item) if mergeList: mergeList.mergeGroup() newList.append(mergeList[0]) self[:] = newList def mergeGroup(self): """Merge self (group of adjacent items)""" if len(self) < 2: return mainItem = self[0] for item in self[1:]: mainItem.textLines.extend(item.textLines) mainItem.height = reduce(lambda x,y: x+y, [item.height for item in self]) def splitColumns(self, numColumns): """Split output into even columns, return list with a group for each""" if numColumns < 2: return [self] if len(self) <= numColumns: return [OutputGroup([item]) for item in self] groupList = [] numEach = len(self) // numColumns for colNum in range(numColumns - 1): groupList.append(self[colNum * numEach : (colNum+1) * numEach]) groupList.append(self[(numColumns-1) * numEach : ]) numChanges = 1 while numChanges: numChanges = 0 for colNum in range(numColumns - 1): if groupList[colNum].totalHeight() > groupList[colNum+1].\ totalHeight() + groupList[colNum][-1].height: groupList[colNum+1].insert(0, groupList[colNum][-1]) groupList[colNum] = groupList[colNum][:-1] numChanges += 1 if groupList[colNum].totalHeight() + groupList[colNum+1][0].\ height <= groupList[colNum+1].totalHeight(): groupList[colNum].append(groupList[colNum+1][0]) groupList[colNum+1] = groupList[colNum+1][1:] numChanges += 1 return groupList def setHeights(self, textHtFunc, totalWidth, indent): """Set item heights after adding prefixes and applying fixes, textHtFunc is a callback to get height from text""" for prevItem, item in zip([None] + self, self): width = totalWidth - indent * item.level if not prevItem or item.level != prevItem.level or not item.prefix: textList = item.textList(True, True) item.height = self.textHeight(textList, textHtFunc, width) else: prevList = prevItem.textList(True, True) prevHeight = self.textHeight(prevList, textHtFunc, width) fullList = prevItem.textList(True, False) + \ item.textList(False, True) fullHeight = self.textHeight(fullList, textHtFunc, width) item.height = fullHeight - prevHeight def textHeight(self, textList, textHtFunc, width): """Return text height after joining""" text = u'
\n'.join(textList) if breakRegExp.search(text): text += ' ' # add space if ends with break for proper height return textHtFunc(text, width) def totalHeight(self): """Return the combined height of items in the group""" return reduce(lambda x, y: x+y, [item.height for item in self]) def splitPages(self, pageHeight, pageActiveLevels=None, firstChildAdjust=0.2): """Split output by pageHeight, return list with a group for each, record levels for lines in pageActiveLevels, firstChildAdjust is fraction of page left blank to avoid break between parent and 1st child""" groupList = [OutputGroup()] height = 0 for item in self: if height + item.height > pageHeight: height = 0 nextGroup = OutputGroup() if firstChildAdjust: lastGroup = groupList[-1][:] tmpItem = item deltaHeight = 0 while lastGroup and \ tmpItem.level == lastGroup[-1].level + 1: tmpItem = lastGroup.pop() nextGroup.insert(0, tmpItem) deltaHeight += tmpItem.height if nextGroup and \ deltaHeight < pageHeight * firstChildAdjust: groupList[-1] = lastGroup height = deltaHeight else: nextGroup = OutputGroup() groupList.append(nextGroup) height += item.height groupList[-1].append(item) if pageActiveLevels == []: for pageNum in range(len(groupList)): levels = [] minLevel = sys.maxint scanPage = pageNum + 1 while scanPage < len(groupList) and minLevel > 1: for item in groupList[scanPage]: if item.level and item.level < minLevel: minLevel = item.level levels.append(item.level) scanPage += 1 pageActiveLevels.append(levels) return groupList def getLines(self): """Return full list of text lines from this group""" lines = [] for item in self: lines.extend(item.textLines) return lines TreeLine/source/setup.py0000644000175000017500000000107311651514477014271 0ustar dougdoug#!/usr/bin/env python from distutils.core import setup import py2exe guiProg = {'script': 'treeline.py', 'icon_resources': [(1, '../win/treeline.ico')], 'dest_base': 'treeline'} consoleProg = {'script': 'treeline.py', 'icon_resources': [(1, '../win/treeline.ico')], 'dest_base': 'treeline_dos'} options = {'py2exe': {'includes': ['sip', 'urllib2'], 'dist_dir': 'dist/lib'}} setup(windows=[guiProg], console=[consoleProg], options=options) # run with: python setup.py py2exe TreeLine/source/gentime.py0000644000175000017500000001733111651514477014565 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # gentime.py, provides a class for time formating # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re import time import types class GenTime(object): """Stores & formats time values""" amStr = _('am') pmStr = _('pm') ampmRegExp = re.compile('(.+)(%s|%s|a|p)' % (amStr[0], pmStr[0])) formatChooser = {'HH':'hour', 'H':'hour', 'hh':'hour12', 'h':'hour12', 'MM':'minute', 'M':'minute', 'SS':'intSecond', 'S':'intSecond', 'sss':'milliSecond', 'ss':'milliSecond', 's':'milliSecond', 'AA':'AMPM', 'A':'AMPM', 'aa':'ampm', 'a':'ampm'} timeType = type(time.localtime()) def __init__(self, time=None): """Accepts one of the following to initialize: 1. None - sets to current time 2. tuple in (h, m, [s]) - or localtime/gmtime 3. string in (h:m[:s]) format 4. number of seconds (int or float) 5. GenTime instance""" if time == None: self.setToCurrent() else: self.setTime(time) def setToCurrent(self, gmt=0): """Set date to current values, local or GMT""" if gmt: self.setTime(time.gmtime()) else: self.setTime(time.localtime()) def setTime(self, time): """Sets the time value from a tuple (h, m, [s]), a localtime/gmtime tuple, a string (h:m[:s]), number of seconds (int or float), or a GenTime instance""" ampm = '' inType = type(time) if inType in (types.TupleType, types.ListType, GenTime.timeType): if len(time) >= 6: self.time = list(time[3:6]) else: self.time = list(time[:3]) elif inType in (types.StringType, types.UnicodeType): self.time = re.split(':', time)[:3] ampmMatch = GenTime.ampmRegExp.match(self.time[-1].lower()) if ampmMatch: self.time[-1], ampm = ampmMatch.groups() elif inType in (types.IntType, types.FloatType): self.time = [0, 0, time] elif inType is type(self): self.time = time.time else: raise GenTimeError('Invalid time type') self._normalize() if (ampm == GenTime.amStr[0] or ampm == 'a') and self.time[0] == 12: self.time[0] = 0 elif (ampm == GenTime.pmStr[0] or ampm == 'p') and self.time[0] < 12: self.time[0] += 12 def timeStr(self, format='HH:MM:SS'): """Return string based on the format, which includes series of 'H' (24 hr), 'h' (12 hr), 'M', 'S' (int), 's' (fract part), 'A' (AM/PM), 'a' (am/pm) repeated to indicate length. Backslashes will escape these letters""" exp = r'((? 12: hour -= 12 elif hour == 0: hour = 12 return hour def minute(self): """Return int minute value""" return self.time[1] def second(self): """Return float seconds value""" return self.time[2] def intSecond(self): """Return truncated seconds value""" return int(self.time[2]) def milliSecond(self): """Return int for 1/1000 of a second""" return int((self.time[2] - self.intSecond()) * 1000) def AMPM(self): """Return string for AM or PM""" if self.time[0] < 12: return GenTime.amStr.upper() return GenTime.pmStr.upper() def ampm(self): """Return string for am or pm""" if self.time[0] < 12: return GenTime.amStr return GenTime.pmStr def toTuple(self): """Return (h, m, s) tuple""" return self.time def totalSeconds(self): """Return total number of seconds since midnight as a float""" return self.time[0] * 3600 + self.time[1] * 60 + self.time[2] def _normalize(self): """Adjust time values to normal ranges and verify value formats""" while len(self.time) < 3: self.time.append(0.0) try: self.time = [float(value) for value in self.time] except ValueError: raise GenTimeError('Invalid time format') totalSec = self.totalSeconds() self.time[2] = totalSec % 60 self.time[1] = int(totalSec) / 60 % 60 self.time[0] = int(totalSec) / 3600 % 24 def clone(self): """Return cloned instance""" return self.__class__(self.time) def __repr__(self): """Outputs in format HH:MM:SS""" return '%02d:%02d:%02d' % (self.hour(), self.minute(), self.intSecond()) def __cmp__(self, other): """Compare operator""" if other is None or not isinstance(other, GenTime): return 1 return cmp(self.totalSeconds(), other.totalSeconds()) def __hash__(self): """Allow use as dictionary key""" return hash(self.totalSeconds()) def __add__(self, seconds): """Addition operator, adds seconds to time Caution: no knowledge of date, wrapping will occur""" if not type(seconds) in (types.IntType, types.FloatType): raise GenTimeError('Add operator requires a number of seconds') copy = self.clone() copy.time[2] += seconds copy._normalize() return copy def __radd__(self, seconds): """Addition operator""" return self.__add__(seconds) def __sub__(self, other): """Subtraction operator for time (returns seconds) or num seconds (returns time) Caution: no knowledge of date, wrapping will occur""" if type(other) in (types.IntType, types.FloatType): return self.__add__(-other) return self.totalSeconds() - other.totalSeconds() def __rsub__(self, other): """Subtraction operator for time, returns num seconds""" if not isinstance(other, GenTime): raise GenTimeError('Cannot subtract a date from a number') return other.totalSeconds() - self.totalSeconds() class GenTimeError(Exception): """Exception class for invalid time data""" pass if __name__ == '__main__': gt = GenTime() print 'The time is', gt.timeStr('h:MM:SS aa') TreeLine/source/gennumber.py0000644000175000017500000001670011651514477015116 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # gennumber.py, provides a class for number formating # # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import re import types import fpformat import math class GenNumber(object): """Stores & formats number values""" def __init__(self, num=0): """Accepts one of the following to initialize: 1. int value 2. float value 3. string in common int or float format 4. GenNumber instance""" self.setNumber(num) def setNumber(self, num): """Sets the number value from number formats, strings, or a GenNumber class""" inType = type(num) if inType in (types.IntType, types.LongType, types.FloatType): self.num = num elif inType in (types.StringType, types.UnicodeType): try: self.num = int(num) except ValueError: try: self.num = float(num) except ValueError: self.setFromStr(num) elif inType is type(self): self.num = num.num else: raise GenNumberError('Invalid initialization type') def setFromStr(self, numStr, format='#\,###'): """Sets the value after removing extra characters found in format and using the approriate radix. Returns self""" radix = _getRadix(format) format = _unescapeFormat(radix, format).strip() extraChar = re.sub(r'[#0\seE\-\+%s]' % re.escape(radix), '', format) if extraChar: numStr = re.sub('[%s]' % re.escape(extraChar), '', numStr) if radix == ',': if '.' in numStr: raise GenNumberError('Invalid number string') numStr = numStr.replace(',', '.') try: self.num = int(numStr) except ValueError: try: self.num = float(numStr) except ValueError: raise GenNumberError('Invalid number string') return self def numStr(self, format='#.##'): """Returns string in given format, including exponents, e=exponent, #=optional digit, 0=required digit, -=optional sign, +=required sign space=digit or space (external) or thousands sep (internal), \, and \. are thousands separators""" formMain, formExp = _doubleSplit('eE', format) if not formExp: return self.basicNumStr(format) exp = math.floor(math.log10(abs(self.num))) num = float(self.num) / 10**exp totPlcs = len(re.findall(r'[#0]', formMain)) num = round(num, totPlcs > 0 and totPlcs - 1 or 0) wholePlcs = len(re.findall(r'[#0]', _doubleSplit('.', formMain)[0])) expChg = wholePlcs - int(math.floor(math.log10(abs(num)))) - 1 num = num * 10**expChg exp -= expChg c = 'e' in format and 'e' or 'E' return '%s%s%s' % (GenNumber(num).basicNumStr(formMain), c, GenNumber(exp).basicNumStr(formExp)) def basicNumStr(self, format='#.##'): """Returns string in given format, without exponent support, #=optional digit, 0=required digit, -=optional sign, +=required sign space=digit or space (external) or thousands sep (internal), \, and \. are thousands separators""" radix = _getRadix(format) format = _unescapeFormat(radix, format) formWhole, formFract = _doubleSplit(radix, format) decPlcs = len(re.findall(r'[#0]', formFract)) numWhole, numFract = _doubleSplit('.', fpformat.fix(self.num, decPlcs)) while numFract[-1:] == '0': numFract = numFract[:-1] numWhole, numFract = list(numWhole), list(numFract) formWhole, formFract = list(formWhole), list(formFract) sign = '+' if numWhole[0] == '-': sign = numWhole.pop(0) result = [] while numWhole or formWhole: c = formWhole and formWhole.pop() or '' if c and c not in '#0 +-': if numWhole or '0' in formWhole: result.insert(0, c) elif numWhole and c != ' ': result.insert(0, numWhole.pop()) if c and c in '+-': formWhole.append(c) elif c in '0 ': result.insert(0, c) elif c in '+-': if sign == '-' or c == '+': result.insert(0, sign) sign = '' if sign == '-': if result[0] == ' ': result = [re.sub(r'\s(?!\s)', '-', ''.join(result), 1)] else: result.insert(0, '-') if formFract or (format and format[-1] == radix): result.append(radix) while formFract: c = formFract.pop(0) if c not in '#0 ': if numFract or '0' in formFract: result.append(c) elif numFract: result.append(numFract.pop(0)) elif c in '0 ': result.append('0') return ''.join(result) def clone(self): """Return cloned instance""" return self.__class__(self.num) def __repr__(self): """Outputs in general string fomat""" return repr(self.num) def __coerce__(self, other): """Allow mixed mode arithmetic""" thisType = type(self.num) otherType = type(other) if thisType == otherType: return (self.num, other) if types.FloatType in (thisType, otherType): return (float(self.num), float(other)) if types.LongType in (thisType, otherType) and \ types.IntType in (thisType, otherType): return (long(self.num), long(other)) return None def __int__(self): """Return integer repr""" return int(self.num) def __long__(self): """Return long repr""" return long(self.num) def __float__(self): """Return float repr""" return float(self.num) def __hash__(self): """Allow use as dictionary key""" return hash(self.num) class GenNumberError(Exception): """Exception class for invalid number data""" pass ######### Utility Functions ########## def _doubleSplit(sepChars, string): """Return tuple of two parts of the string, sep by one of chars in sepChars, return empty string if no separator found""" for sep in sepChars: result = string.split(sep, 1) if len(result) == 2: return result return (string, '') def _getRadix(format): """Return the radix character (. or ,) used in format, assumes "." if ambiguous""" if not '\,' in format and (not '.' in format or '\.' in format) and \ (',' in format or '\.' in format): return ',' return '.' def _unescapeFormat(radix, format): """Return format with escapes removed form non-radix separators""" if radix == '.': return format.replace('\,', ',') return format.replace('\.', '.') TreeLine/source/fieldformat.py0000644000175000017500000012164211651514477015432 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # fieldformat.py, provides non-GUI base classes for field formating # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import re from xml.sax.saxutils import escape, unescape from gennumber import GenNumber, GenNumberError from gendate import GenDate, GenDateError from gentime import GenTime, GenTimeError from genboolean import GenBoolean, GenBooleanError import treedoc import globalref _errorStr = '#####' def xslEscape(text): """Encapsulate all literal text in elements and transform/escape some non-XML entities. For the moment, only   is supported""" nonTagRe = re.compile(r'(.*?)(<.*?>)|(.*)') escDict = {'&nbsp;': ' '} # escape function does '&' first def esc(matchObj): """Return escaped replacement text""" if matchObj.group(1) == None: # no tags found return u'%s' % \ escape(matchObj.group(3), escDict) if matchObj.group(1): # leading text and tag return u'%s%s' % \ (escape(matchObj.group(1), escDict), matchObj.group(2)) return matchObj.group(2) # tag only return nonTagRe.sub(esc, text) class TextFormat(object): """Holds format info for a normal text field""" typeName = 'Text' sortSequence = 20 stripTagRe = re.compile('<.*?>') defaultNumLines = 1 #field format edit options: defaultFormat = '' formatMenuList = [] htmlOption = True hasEditChoices = False autoAddChoices = False hasFileBrowse = False allowAltLinkText = False def __init__(self, name, attrs={}): """Any prefix, suffix, html info in attrs dict""" self.name = name self.enName = '' # used only by fileFormat field for i18n self.format = attrs.get(u'format', self.defaultFormat) self.prefix = attrs.get(u'prefix', '') self.suffix = attrs.get(u'suffix', '') # defaults to no html (line breaks preserved) self.html = attrs.get(u'html', '').startswith('y') and True or False self.isRequired = attrs.get(u'required', '').startswith('y') and \ True or False self.hidden = attrs.get(u'hidden', '').startswith('y') and \ True or False try: self.numLines = int(attrs.get(u'lines', repr(self.defaultNumLines))) except ValueError: self.numLines = 1 self.initDefault = attrs.get(u'init', '') self.linkAltField = attrs.get(u'linkalt', '') self.parentLevel = 0 self.useFileInfo = False self.showInDialog = True self.initFormat() def initFormat(self): """Called by base init, after class change or format text change""" pass def duplicateSettings(self, otherField): """Assign other field's parameters to this field""" self.name = otherField.name self.enName = otherField.enName self.format = otherField.format self.prefix = otherField.prefix self.suffix = otherField.suffix self.html = otherField.html self.isRequired = otherField.isRequired self.hidden = otherField.hidden self.numLines = otherField.numLines self.initDefault = otherField.initDefault self.linkAltField = otherField.linkAltField self.parentLevel = otherField.parentLevel self.useFileInfo = otherField.useFileInfo self.showInDialog = otherField.showInDialog def changeType(self, newType): """Change this field's type to newType with default format""" self.__class__ = globals()[newType + 'Format'] self.format = self.defaultFormat self.initFormat() def englishName(self): """Returns English name if assigned, o/w name""" if self.enName: return self.enName return self.name def sepName(self, englishOnly=False): """Return name enclosed with {* *} separators""" name = englishOnly and self.enName or self.name if not self.useFileInfo: return u'{*%s*}' % name return u'{*!%s*}' % name def labelName(self): """Return name used for labels - add * for required fields""" if self.isRequired: return '%s*' % self.name return self.name def writeXml(self): """Return text for xml attributes""" text = u' type="%s"' % self.typeName if self.format: text += u' format="%s"' % escape(self.format, treedoc.escDict) if self.prefix: text += u' prefix="%s"' % escape(self.prefix, treedoc.escDict) if self.suffix: text += u' suffix="%s"' % escape(self.suffix, treedoc.escDict) if self.html: text += u' html="y"' if self.isRequired: text += u' required="y"' if self.hidden: text += u' hidden="y"' if self.numLines > 1: text += u' lines="%d"' % self.numLines if self.initDefault: text += u' init="%s"' % escape(self.initDefault, treedoc.escDict) if self.linkAltField: text += u' linkalt="%s"' % escape(self.linkAltField, treedoc.escDict) return text def outputText(self, item, titleMode, internal=False): """Return formatted text for this field""" if self.useFileInfo: item = globalref.docRef.fileInfoItem storedText = item.data.get(self.name, '') if storedText: return self.formatOutput(storedText, titleMode, internal) return '' def removeMarkup(self, text): """Remove HTML Markup and unescape entities""" text = TextFormat.stripTagRe.sub('', text) return unescape(text) def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped if not in titleMode""" prefix = self.prefix suffix = self.suffix if titleMode: if self.html: storedText = self.removeMarkup(storedText) if globalref.docRef.formHtml: prefix = self.removeMarkup(prefix) suffix = self.removeMarkup(suffix) else: if not self.html: storedText = escape(storedText).replace('\n', '
') if not globalref.docRef.formHtml: prefix = escape(prefix) suffix = escape(suffix) return u'%s%s%s' % (prefix, storedText, suffix) def editText(self, item): """Return tuple of this field's text in edit format and bool validity, using edit format option""" storedText = item.data.get(self.name, '') result = self.formatEditText(storedText) if self.isRequired and not result[0]: return (result[0], False) return result def formatEditText(self, storedText): """Return tuple of text in edit format and bool validity, using edit format option""" return (storedText, True) def storedText(self, editText): """Return tuple of stored text from edited text and bool validity, using edit format option""" return (editText, editText or not self.isRequired) def getInitDefault(self): """Return initial stored value for new nodes""" return self.initDefault def setInitDefault(self, editText): """Set initial value from editor version using edit format option""" self.initDefault = self.storedText(editText)[0] def getEditInitDefault(self): """Return initial value in edit format, found in edit format option""" return self.formatEditText(self.initDefault)[0] def initDefaultChoices(self): """Return a list of choices for setting the init default""" return [] def sortValue(self, data): """Return value to be compared for sorting and conditionals""" storedText = data.get(self.name, '') return storedText.lower() def adjustedCompareValue(self, value): """Return conditional comparison value with real-time adjustments, used for date and time types' 'now' value""" return value def xslText(self): """Return what we need to write into an XSL file for this type""" return u'%s'\ '%s' % \ (self.name, xslEscape(self.prefix), self.name, xslEscape(self.suffix)) def xslTestText(self): """Return XSL file test for data existance""" return u'normalize-space(./%s)' % self.name class LongTextFormat(TextFormat): """Holds format info for a long text field - Obsolete - kept for compatability with old files""" # typeName = 'LongText' defaultNumLines = 7 def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) class NumberFormat(TextFormat): """Holds format info for a number field""" typeName = 'Number' sortSequence = 10 #field format edit options: defaultFormat = u'#.##' formatMenuList = [(u'%s\t%s' % (_('Optional Digit'), '#'), '#'), (u'%s\t%s' % (_('Required Digit'), '0'), '0'), (u'%s\t%s' % (_('Digit or Space (external)'), _('')), ' '), None, (u'%s\t%s' % (_('Decimal Point'), '.'), '.'), (u'%s\t%s' % (_('Decimal Comma'), ','), ','), None, (u'%s\t%s' % (_('Comma Separator'), '\,'), '\,'), (u'%s\t%s' % (_('Dot Separator'), '\.'), '\.'), (u'%s\t%s' % (_('Space Separator (internal)'), _('')), ' '), None, (u'%s\t%s' % (_('Optional Sign'), '-'), '-'), (u'%s\t%s' % (_('Required Sign'), '+'), '+'), None, (u'%s\t%s' % (_('Exponent (capital)'), 'E'), 'E'), (u'%s\t%s' % (_('Exponent (small)'), 'e'), 'e')] def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped if not in titleMode""" try: text = GenNumber(storedText).numStr(self.format) except GenNumberError: text = _errorStr return TextFormat.formatOutput(self, text, titleMode, internal) def formatEditText(self, storedText): """Return tuple of text in edit format and bool validity, using self.format""" try: return (GenNumber(storedText).numStr(self.format), True) except GenNumberError: return (storedText, not storedText) def storedText(self, editText): """Return tuple of stored text from edited text and bool validity, using self.format""" try: return (repr(GenNumber().setFromStr(editText, self.format)), True) except GenNumberError: return (editText, not editText and not self.isRequired) def sortValue(self, data): """Return value to be compared for sorting and conditionals""" storedText = data.get(self.name, '') try: return GenNumber(storedText).num except GenNumberError: return '' class ChoiceFormat(TextFormat): """Holds format info for a field with one of several text options""" typeName = 'Choice' sortSequence = 20 editSep = '/' #field format edit options: defaultFormat = '1/2/3/4' formatMenuList = [(u'%s\t%s' % (_('Separator'), '/'), '/'), None, (u'%s\t%s' % (_('"/" Character'), '//'), '//'), None, (u'%s\t%s' % (_('Example'), '1/2/3/4'), '1/2/3/4')] hasEditChoices = True def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) def initFormat(self): """Called by base init, after class change or format text change""" self.formatList = self.splitText(self.format) def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped if not in titleMode""" if storedText not in self.formatList: storedText = _errorStr return TextFormat.formatOutput(self, storedText, titleMode, internal) def formatEditText(self, storedText): """Return tuple of text in edit format and bool validity, using edit format option""" if storedText in self.formatList: return (storedText, True) return (storedText, not storedText) def storedText(self, editText): """Return tuple of stored text from edited text and bool validity, using edit format option""" if editText in self.formatList: return (editText, True) return (editText, not editText and not self.isRequired) def getEditChoices(self, currentText=''): """Return list of choices for combo box, each a tuple of edit text and any annotation text""" return [(text, '') for text in self.formatList] def initDefaultChoices(self): """Return a list of choices for setting the init default""" return [text for text in self.formatList] def splitText(self, textStr): """Split textStr using editSep, double sep's become char""" return [text.strip().replace('\0', self.editSep) for text in textStr.replace(self.editSep * 2, '\0'). split(self.editSep)] class CombinationFormat(ChoiceFormat): """Holds format info for a field of combinations of text options""" typeName = 'Combination' outputSepList = (',', ';', ':', '|', '/', '\\', '~') def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" ChoiceFormat.__init__(self, name, attrs) def initFormat(self): """Called by base init, after class change or format text change""" ChoiceFormat.initFormat(self) fullFormat = ''.join(self.formatList) try: self.sep = [sep for sep in CombinationFormat.outputSepList if sep not in fullFormat][0] + ' ' except IndexError: self.sep = CombinationFormat.outputSepList[0] + ' ' def sortedChoices(self, inText): """Return tuple of choices from inText sorted like format and True if all splits are valid and included""" choices = self.splitText(inText) sortedChoices = [text for text in self.formatList if text in choices] if len(choices) == len(sortedChoices): return (sortedChoices, True) else: return (sortedChoices, False) def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped if not in titleMode""" choices, valid = self.sortedChoices(storedText) if valid: result = self.sep.join(choices) else: result = _errorStr return TextFormat.formatOutput(self, result, titleMode, internal) def formatEditText(self, storedText): """Return tuple of text in edit format and bool validity, using edit format option""" for choice in self.splitText(storedText): if choice not in self.formatList: return (storedText, not storedText) return (storedText, True) def storedText(self, editText): """Return tuple of stored text from edited text and bool validity, using edit format option""" choices, valid = self.sortedChoices(editText) if valid: return (self.editSep.join(choices), True) else: return (editText, not editText and not self.isRequired) def getEditChoices(self, currentText=''): """Return list of choices for combo box, each a tuple of edit text and any annotation text""" currentChoices, valid = self.sortedChoices(currentText) nonChoices = [text for text in self.formatList if text not in currentChoices] results = [] for choice in nonChoices: # menu entries to add a choice allChoices = currentChoices + [choice] allChoices = [text for text in self.formatList if text in allChoices] results.append((self.editSep.join(allChoices), '(%s %s)' % (_('add'), choice))) if currentChoices: results.append((None, None)) # separator for choice in currentChoices: # menu entries to remove a choice allChoices = currentChoices[:] allChoices.remove(choice) allChoices = [text for text in self.formatList if text in allChoices] results.append((self.editSep.join(allChoices), '(%s %s)' % (_('remove'), choice))) return results def initDefaultChoices(self): """Return a list of choices for setting the init default""" return [entry[0] for entry in self.getEditChoices()] class AutoChoiceFormat(ChoiceFormat): """Holds format info for a field with one of several text options""" typeName = 'AutoChoice' #field format edit options: defaultFormat = '' formatMenuList = () hasEditChoices = True autoAddChoices = True def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) def initFormat(self): """Called by base init, after class change or format text change""" self.formatList = [] def addChoice(self, choice, sort=False): """Add choice to edit menu list if not already there""" if choice and choice not in self.formatList: self.formatList.append(choice) if sort: self.sortChoices() def sortChoices(self): """Sort menu list choices""" self.formatList.sort() def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped if not in titleMode""" return TextFormat.formatOutput(self, storedText, titleMode, internal) def formatEditText(self, storedText): """Return tuple of text in edit format and bool validity, using edit format option""" return (storedText, True) def storedText(self, editText): """Return tuple of stored text from edited text and bool validity, using edit format option""" if editText: return (editText, True) return (editText, not self.isRequired) class DateFormat(TextFormat): """Holds format info for a date field""" typeName = 'Date' sortSequence = 5 #field format edit options: defaultFormat = u'mmmm d, yyyy' dateStampStrings = ('Now', _('Now', 'date stamp setting')) formatMenuList = [(u'%s\t%s' % (_('Day (1 or 2 digits)'), 'd'), 'd'), (u'%s\t%s' % (_('Day (2 digits)'), 'dd'), 'dd'), None, (u'%s\t%s' % (_('Month (1 or 2 digits)'), 'm'), 'm'), (u'%s\t%s' % (_('Month (2 digits)'), 'mm'), 'mm'), (u'%s\t%s' % (_('Month Abbreviation'), 'mmm'), 'mmm'), (u'%s\t%s' % (_('Month Name'), 'mmmm'), 'mmmm'), None, (u'%s\t%s' % (_('Year (2 digits)'), 'yy'), 'yy'), (u'%s\t%s' % (_('Year (4 digits)'), 'yyyy'), 'yyyy'), None, (u'%s\t%s' % (_('Weekday (1 digit)'), 'w'), 'w'), (u'%s\t%s' % (_('Weekday Abbreviation'), 'www'), 'www'), (u'%s\t%s' % (_('Weekday Name'), 'wwww'), 'wwww')] hasEditChoices = True def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped if not in titleMode""" try: text = GenDate(storedText).dateStr(self.format) except GenDateError: text = _errorStr return TextFormat.formatOutput(self, text, titleMode, internal) def formatEditText(self, storedText): """Return tuple of text in edit format and bool validity, using edit format option""" format = globalref.options.strData('EditDateFormat', True) try: return (GenDate(storedText).dateStr(format), True) except GenDateError: return (storedText, not storedText) def storedText(self, editText): """Return tuple of stored text from edited text and bool validity, using edit format option""" format = globalref.options.strData('EditDateFormat', True) try: return (repr(GenDate().setFromStr(editText, format)), True) except GenDateError: return (editText, not editText and not self.isRequired) def getEditChoices(self, currentText=''): """Return list of choices for combo box, each a tuple of edit text and any annotation text""" format = globalref.options.strData('EditDateFormat', True) today = GenDate().dateStr(format) yesterday = (GenDate() - 1).dateStr(format) tomorrow = (GenDate() + 1).dateStr(format) return [(today, '(%s)' % _('today')), (yesterday, '(%s)' % _('yesterday')), (tomorrow, '(%s)' % _('tomorrow'))] def getInitDefault(self): """Return initial stored value for new nodes""" if self.initDefault in DateFormat.dateStampStrings: return GenDate().dateStr() return TextFormat.getInitDefault(self) def setInitDefault(self, editText): """Set initial value from editor version using edit format option""" if editText in DateFormat.dateStampStrings: self.initDefault = DateFormat.dateStampStrings[0] else: TextFormat.setInitDefault(self, editText) def getEditInitDefault(self): """Return initial value in edit format, found in edit format option""" if self.initDefault in DateFormat.dateStampStrings: return DateFormat.dateStampStrings[1] return TextFormat.getEditInitDefault(self) def initDefaultChoices(self): """Return a list of choices for setting the init default""" choices = [entry[0] for entry in self.getEditChoices()] choices.insert(0, DateFormat.dateStampStrings[1]) return choices def adjustedCompareValue(self, value): """Return conditional comparison value with real-time adjustments, used for date and time types' 'now' value""" if value.startswith('now'): return repr(GenDate()) return value class TimeFormat(TextFormat): """Holds format info for a time field""" typeName = 'Time' sortSequence = 6 #field format edit options: defaultFormat = u'h:MM:SS aa' timeStampStrings = ('Now', _('Now', 'time stamp setting')) formatMenuList = [(u'%s\t%s' % (_('Hour (0-23, 1 or 2 digits)'), 'H'), 'H'), (u'%s\t%s' % (_('Hour (00-23, 2 digits)'), 'HH'), 'HH'), (u'%s\t%s' % (_('Hour (1-12, 1 or 2 digits)'), 'h'), 'h'), (u'%s\t%s' % (_('Hour (01-12, 2 digits)'), 'hh'), 'hh'), None, (u'%s\t%s' % (_('Minute (1 or 2 digits)'), 'M'), 'M'), (u'%s\t%s' % (_('Minute (2 digits)'), 'MM'), 'MM'), None, (u'%s\t%s' % (_('Second (1 or 2 digits)'), 'S'), 'S'), (u'%s\t%s' % (_('Second (2 digits)'), 'SS'), 'SS'), (u'%s\t%s' % (_('Fractional Seconds'), 's'), 's'), None, (u'%s\t%s' % (_('AM/PM'), 'AA'), 'AA'), (u'%s\t%s' % (_('am/pm'), 'aa'),'aa')] hasEditChoices = True def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped if not in titleMode""" try: text = GenTime(storedText).timeStr(self.format) except GenTimeError: text = _errorStr return TextFormat.formatOutput(self, text, titleMode, internal) def formatEditText(self, storedText): """Return tuple of text in edit format and bool validity, using edit format option""" format = globalref.options.strData('EditTimeFormat', True) try: return (GenTime(storedText).timeStr(format), True) except GenTimeError: return (storedText, not storedText) def storedText(self, editText): """Return tuple of stored text from edited text and bool validity, using edit format option""" try: return (repr(GenTime(editText)), True) except GenTimeError: return (editText, not editText and not self.isRequired) def getEditChoices(self, currentText=''): """Return list of choices for combo box, each a tuple of edit text and annotated text""" format = globalref.options.strData('EditTimeFormat', True) now = GenTime().timeStr(format) choices = [(now, '(%s)' % _('now'))] for hr in (6, 9, 12, 15, 18, 21, 0): time = GenTime((hr, 0)).timeStr(format) choices.append((time, '')) return choices def getInitDefault(self): """Return initial stored value for new nodes""" if self.initDefault in TimeFormat.timeStampStrings: return GenTime().timeStr() return TextFormat.getInitDefault(self) def setInitDefault(self, editText): """Set initial value from editor version using edit format option""" if editText in TimeFormat.timeStampStrings: self.initDefault = TimeFormat.timeStampStrings[0] else: TextFormat.setInitDefault(self, editText) def getEditInitDefault(self): """Return initial value in edit format, found in edit format option""" if self.initDefault in TimeFormat.timeStampStrings: return TimeFormat.timeStampStrings[1] return TextFormat.getEditInitDefault(self) def initDefaultChoices(self): """Return a list of choices for setting the init default""" choices = [entry[0] for entry in self.getEditChoices()] choices.insert(0, TimeFormat.timeStampStrings[1]) return choices def adjustedCompareValue(self, value): """Return conditional comparison value with real-time adjustments, used for date and time types' 'now' value""" if value.startswith('now'): return repr(GenTime()) return value class BooleanFormat(ChoiceFormat): """Holds format info for a bool field""" typeName = 'Boolean' sortSequence = 1 #field format edit options: defaultFormat = _('yes/no') formatMenuList = [(_('true/false'), _('true/false')), (_('T/F'), _('T/F')), None, (_('yes/no'), _('yes/no')), (_('Y/N'), _('Y/N')), None, ('1/0', '1/0')] hasEditChoices = True def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" ChoiceFormat.__init__(self, name, attrs) def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped if not in titleMode""" if storedText not in self.formatList: try: storedText = GenBoolean(storedText).boolStr(self.format) except GenBooleanError: storedText = _errorStr return TextFormat.formatOutput(self, storedText, titleMode, internal) def formatEditText(self, storedText): """Return tuple of text in edit format and bool validity, using edit format option""" if storedText in self.formatList: return (storedText, True) try: return (GenBoolean(storedText).boolStr(self.format), True) except GenBooleanError: return (storedText, not storedText) def storedText(self, editText): """Return tuple of stored text from edited text and bool validity, using edit format option""" try: return (repr(GenBoolean(editText)), True) except GenBooleanError: if editText in self.formatList: return (editText, True) return (editText, not editText and not self.isRequired) def sortValue(self, data): """Return value to be compared for sorting and conditionals""" storedText = data.get(self.name, '') try: return repr(GenBoolean(storedText)) except GenBooleanError: return '' class UniqueIDFormat(TextFormat): """An unique ID automatically generated for new nodes""" typeName = 'UniqueID' sortSequence = 10 formatRe = re.compile('([^0-9]*)([0-9]+)(.*)') #field format edit options: defaultFormat = u'0001' formatMenuList = [(u'%s\t%s' % (_('Required Digit'), '0'), '0'), None, (u'%s\t%s' % (_('Start Num Example'), '0100'), '0100'), (u'%s\t%s' % (_('Prefix Example'), 'id0100'), 'id0100')] def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) def nextValue(self, increment=True): """Return the next value for a new node, increment format if increment is True""" try: prefix, numText, suffix = UniqueIDFormat.formatRe.\ match(self.format).groups() except AttributeError: self.format = UniqueIDFormat.defaultFormat return self.nextValue(increment) value = self.format if increment: pattern = u'%%s%%0.%dd%%s' % len(numText) num = int(numText) + 1 self.format = pattern % (prefix, num, suffix) return value def sortValue(self, data): """Return value to be compared for sorting and conditionals""" storedText = data.get(self.name, '') try: return int(UniqueIDFormat.formatRe.match(storedText).group(2)) except AttributeError: return 0 class URLFormat(TextFormat): """Holds format info for a field with a URL path""" typeName = 'URL' sortSequence = 8 htmlOption = False allowAltLinkText = True hasMethodRe = re.compile('[a-zA-Z][a-zA-Z]+:|#') URLMethod = u'http://' def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) def initFormat(self): """Called by base init, after class change or format text change""" self.html = True def outputText(self, item, titleMode, internal=False): """Return formatted text for this field""" if self.useFileInfo: item = globalref.docRef.fileInfoItem altText = '' if self.linkAltField: field = item.nodeFormat().findField(self.linkAltField) if field: altText = field.outputText(item, titleMode, internal) storedText = item.data.get(self.name, '') if storedText: return self.formatOutput(storedText, titleMode, altText, internal) return '' def formatOutput(self, storedText, titleMode, altText='', internal=False): """Return formatted text, properly escaped and with a link reference if not in titleMode""" if titleMode: return TextFormat.formatOutput(self, storedText, titleMode, internal) paths = storedText.split('\n') results = [] for url in paths: path = url if not URLFormat.hasMethodRe.match(path): path = u'%s%s' % (self.URLMethod, path) path = u'%s' % (escape(path, treedoc.escDict), altText or url) results.append(TextFormat.formatOutput(self, path, titleMode, internal)) return u'
'.join(results) def xslText(self): """Return what we need to write into an XSL file for this type""" return u'%s'\ ''\ ''\ ''\ '%s' % \ (self.name, xslEscape(self.prefix), self.URLMethod, xslEscape(self.suffix)) class PathFormat(URLFormat): """Holds format info for a field with a local path""" typeName = 'Path' URLMethod = u'file:///' hasFileBrowse = True def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" URLFormat.__init__(self, name, attrs) class EmailFormat(URLFormat): """Holds format info for a field with a local path""" typeName = 'Email' URLMethod = u'mailto:' def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" URLFormat.__init__(self, name, attrs) class InternalLinkFormat(URLFormat): """Holds format info for a field with a local path""" typeName = 'InternalLink' URLMethod = u'#' def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" URLFormat.__init__(self, name, attrs) class ExecuteLinkFormat(URLFormat): """Holds format info for an executable field""" typeName = 'ExecuteLink' URLMethod = u'exec:' hasFileBrowse = True def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" URLFormat.__init__(self, name, attrs) def formatOutput(self, storedText, titleMode, altText='', internal=False): """Return formatted text, properly escaped and with a link reference if not in titleMode""" if titleMode or not internal: return TextFormat.formatOutput(self, storedText, titleMode, internal) paths = storedText.split('\n') results = [] for url in paths: # add prefix/suffix within the executable path: url = TextFormat.formatOutput(self, url, titleMode, internal) path = url if not URLFormat.hasMethodRe.match(path): path = u'%s%s' % (self.URLMethod, path) results.append(u'%s' % (escape(path, treedoc.escDict), altText or url)) return u'
'.join(results) def xslText(self): """Return what we need to write into an XSL file for this type""" return TextFormat.xslText(self) class PictureFormat(TextFormat): """Holds format info for a field with a link to a picture""" typeName = 'Picture' sortSequence = 8 htmlOption = False hasFileBrowse = True def __init__(self, name, attrs={}): """Any format, prefix, suffix, html info in attrs dict""" TextFormat.__init__(self, name, attrs) def initFormat(self): """Called by base init, after class change or format text change""" self.html = True def formatOutput(self, storedText, titleMode, internal=False): """Return formatted text, properly escaped and with a link to the picture if not in titleMode""" if titleMode: return TextFormat.formatOutput(self, storedText, titleMode, internal) paths = storedText.split('\n') results = ['' % escape(url, treedoc.escDict) for url in paths] return u'
'.join(results) class ParentFormat(TextFormat): """Placeholder format for references to specific parents""" typeName = 'Parent' def __init__(self, name, parentLevel=1): TextFormat.__init__(self, name, {}) self.parentLevel = parentLevel def sepName(self, englishOnly=False): """Return name enclosed with {* *} separators""" name = englishOnly and self.enName or self.name return u'{*%s%s*}' % (self.parentLevel * '*', name) def outputText(self, item, titleMode, internal=False): """Return formatted text for this field""" for num in range(self.parentLevel): item = item.parent if not item: return '' field = item.nodeFormat().findField(self.name) if not field: return '' return field.outputText(item, titleMode, internal) def xslText(self): """Return what we need to write into an XSL file for this type""" return u'' % (self.parentLevel * '../', self.name) def xslTestText(self): """Return XSL file test for data existance""" return u'normalize-space(%s%s)' % (self.parentLevel * '../', self.name) class AncestorFormat(TextFormat): """Placeholder format for references to any parent with data""" typeName = 'Ancestor' def __init__(self, name): TextFormat.__init__(self, name, {}) self.parentLevel = 1000 def sepName(self, englishOnly=False): """Return name enclosed with {*? *} separators""" name = englishOnly and self.enName or self.name return u'{*?%s*}' % (name) def outputText(self, item, titleMode, internal=False): """Return formatted text for this field""" field = None while not field: item = item.parent if item: field = item.nodeFormat().findField(self.name) else: return '' return field.outputText(item, titleMode, internal) def xslText(self): """Return what we need to write into an XSL file for this type""" return u'' % self.name def xslTestText(self): """Return XSL file test for data existance""" return u'normalize-space(ancestor::*/%s)' % self.name class ChildFormat(TextFormat): """Placeholder format for references to a sequence of child data""" typeName = 'Child' def __init__(self, name): TextFormat.__init__(self, name, {}) self.parentLevel = -1 def sepName(self, englishOnly=False): """Return name enclosed with {*? *} separators""" name = englishOnly and self.enName or self.name return u'{*&%s*}' % (name) def outputText(self, item, titleMode, internal=False): """Return formatted text for this field""" result = [] for child in item.childList: field = child.nodeFormat().findField(self.name) if field: text = field.outputText(child, titleMode, internal) if text: result.append(text) return globalref.docRef.childFieldSep.join(result) def xslText(self): """Return what we need to write into an XSL file for this type""" return u'' % self.name def xslTestText(self): """Return XSL file test for data existance""" return u'normalize-space(child::*/%s)' % self.name class CountFormat(TextFormat): """Placeholder format for a count of children at the given level""" typeName = 'Count' def __init__(self, name, level): TextFormat.__init__(self, name, {}) self.parentLevel = -level def sepName(self, englishOnly=False): """Return name enclosed with {*? *} separators""" name = englishOnly and self.enName or self.name return u'{*#%s*}' % (name) def outputText(self, item, titleMode, internal=False): """Return formatted text for this field""" return repr(len(item.descendLevelList(-self.parentLevel))) TreeLine/source/treerightviews.py0000644000175000017500000002212411651514477016204 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treerightviews.py, provides classes for the title edit & data output views # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** import sys import os.path import webbrowser import xml.sax.saxutils from PyQt4 import QtCore, QtGui import treedoc import optiondefaults import globalref class DataOutView(QtGui.QTextBrowser): """Right pane view of database info, read-only""" def __init__(self, showChildren=True, parent=None): QtGui.QTextBrowser.__init__(self, parent) self.showChildren = showChildren self.showDescendants = False self.oldViewHeight = 0 self.setFocusPolicy(QtCore.Qt.NoFocus) self.connect(self, QtCore.SIGNAL('highlighted(const QString&)'), self.showLink) def updateView(self): """Replace contents with selected item data list""" path = os.path.dirname(globalref.docRef.fileName) self.setSearchPaths([path]) sep = globalref.docRef.lineBreaks and u'
\n' or u'\n' if self.showChildren and len(globalref.docRef.selection) == 1: if self.showDescendants: indent = globalref.options.intData('IndentOffset', 0, optiondefaults.maxIndentOffset) outGroup = globalref.docRef.selection[0].outputItemList(False) if globalref.docRef.lineBreaks: outGroup.addInnerBreaks() outGroup.joinPrefixItems() outGroup.addPrefix() for item in outGroup: item.addAbsoluteIndents(indent) lines = [] for item in outGroup: lines.extend(item.textLines) self.setHtml(u'\n'.join(lines)) else: self.setHtml(sep.join(globalref.docRef.selection[0]. formatChildText(True, True))) elif not self.showChildren and globalref.docRef.selection: self.setHtml(sep.join(globalref.docRef.selection[0]. formatTextItems(globalref.docRef.selection, True, True))) else: self.setHtml('') def setSource(self, url): """Called when user clicks on a URL, opens an internal link or an external browser""" name = xml.sax.saxutils.unescape(unicode(url.toString()), treedoc.unEscDict) if name.startswith(u'#'): globalref.docRef.selection.findRefField(name[1:]) elif name.startswith(u'exec:'): if not globalref.options.boolData('EnableExecLinks'): QtGui.QMessageBox.warning(self, 'TreeLine', _('Executable links are not enabled')) elif sys.platform.startswith('win'): name = name.replace("'", '"') # windows interprets first quoted text as a title! os.system(u'start "tl exec" %s' % name[5:]) else: os.system(u'%s &' % name[5:]) else: name = name.replace(' ', '%20') try: webbrowser.open(name, True) except: pass def showLink(self, text): """Send link text to the statusbar""" text = xml.sax.saxutils.unescape(unicode(text), treedoc.unEscDict) globalref.setStatusBar(text) def copyAvail(self): """Return True if there is selected text""" return self.textCursor().hasSelection() def cut(self): """Substitute copy for cut command""" self.copy() def highlightWords(self, wordList): """Highlight given search terms in thsi view""" if not hasattr(QtGui.QTextEdit, 'ExtraSelection'): return # requires Qt 4.2 or greater # backColor = self.palette().highlight() backColor = self.palette().brush(QtGui.QPalette.Active, QtGui.QPalette.Highlight) foreColor = self.palette().brush(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText) selections = [] for word in wordList: while self.find(word): extraSel = QtGui.QTextEdit.ExtraSelection() extraSel.cursor = self.textCursor() extraSel.format.setBackground(backColor) extraSel.format.setForeground(foreColor) selections.append(extraSel) cursor = QtGui.QTextCursor(self.document()) self.setTextCursor(cursor) # reset main cursor/selection self.setExtraSelections(selections) def scrollPage(self, numPages=1): """Scrolls down by numPages (negative for up) leaving a one-line overlap""" delta = self.height() - 2 * self.frameWidth() - \ self.fontMetrics().height() if delta > 0: scrollBar = self.verticalScrollBar() scrollBar.setValue(scrollBar.value() + numPages * delta) def resizeEvent(self, event): """Update view if was collaped by splitter""" if (event.oldSize().height() == 0 and event.size().height()) or \ (event.oldSize().width() == 0 and event.size().width()): self.setEnabled(True) self.updateView() return QtGui.QTextBrowser.resizeEvent(self, event) class TitleListView(QtGui.QTextEdit): """Right pane list edit view, titles of current selection or its children""" def __init__(self, showChildren=True, parent=None): QtGui.QTextEdit.__init__(self, parent) self.showChildren = showChildren self.oldViewHeight = 0 self.setAcceptRichText(False) self.setLineWrapMode(QtGui.QTextEdit.NoWrap) self.setTabChangesFocus(True) self.connect(self, QtCore.SIGNAL('textChanged()'), self.readChange) def updateView(self): """Replace contents with selected item child list""" items = [] if self.showChildren and len(globalref.docRef.selection) == 1: items = globalref.docRef.selection[0].childList elif not self.showChildren and globalref.docRef.selection: items = globalref.docRef.selection self.blockSignals(True) if items: self.setPlainText(u'\n'.join([item.title() for item in items])) else: self.clear() self.blockSignals(False) def readChange(self): """Update doc from edit view contents""" textList = filter(None, [u' '.join(text.split()) for text in unicode(self.toPlainText()).split('\n')]) if self.showChildren and len(globalref.docRef.selection) == 1: globalref.docRef.selection[0].editChildList(textList) globalref.docRef.selection.validateHistory() elif not self.showChildren and \ len(globalref.docRef.selection) == len(textList): for item, text in zip(globalref.docRef.selection, textList): if item.title() != text and item.setTitle(text, True): globalref.updateViewItem(item) else: self.updateView() # remove illegal changes globalref.updateViewMenuStat() def copyAvail(self): """Return True if there is selected text""" return self.textCursor().hasSelection() def insertFromMimeData(self, mimeData): """Override to paste properly from copied node""" try: text = unicode(mimeData.text()) except UnicodeError: return item = globalref.docRef.readXmlString(text) if item: text = item.title() self.insertPlainText(text) def scrollPage(self, numPages=1): """Scrolls down by numPages (negative for up) leaving a one-line overlap""" delta = self.height() - 2 * self.frameWidth() - \ self.fontMetrics().height() if delta > 0: scrollBar = self.verticalScrollBar() scrollBar.setValue(scrollBar.value() + numPages * delta) def keyPressEvent(self, event): """Bind keys to functions""" if not self.showChildren and event.key() in \ (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): pass else: QtGui.QTextEdit.keyPressEvent(self, event) def resizeEvent(self, event): """Update view if was collaped by splitter""" if (event.oldSize().height() == 0 and event.size().height()) or \ (event.oldSize().width() == 0 and event.size().width()): self.setEnabled(True) self.updateView() return QtGui.QTextEdit.resizeEvent(self, event) TreeLine/source/optiondlg.py0000644000175000017500000002066711651514477015142 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # optiondlg.py, provides classes for option setting dialogs # # Copyright (C) 2005, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** from PyQt4 import QtCore, QtGui class OptionDlg(QtGui.QDialog): """Works with Option class to provide a dialog for pref/options""" def __init__(self, option, parent=None): QtGui.QDialog.__init__(self, parent) self.setWindowFlags(QtCore.Qt.Dialog | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint) self.option = option topLayout = QtGui.QVBoxLayout(self) self.setLayout(topLayout) self.columnLayout = QtGui.QHBoxLayout() topLayout.addLayout(self.columnLayout) self.gridLayout = QtGui.QGridLayout() self.columnLayout.addLayout(self.gridLayout) self.oldLayout = self.gridLayout ctrlLayout = QtGui.QHBoxLayout() topLayout.addLayout(ctrlLayout) ctrlLayout.addStretch(0) okButton = QtGui.QPushButton(_('&OK'), self) ctrlLayout.addWidget(okButton) self.connect(okButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('accept()')) cancelButton = QtGui.QPushButton(_('&Cancel'), self) ctrlLayout.addWidget(cancelButton) self.connect(cancelButton, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('reject()')) self.setWindowTitle('Preferences') self.itemList = [] self.curGroup = None def addItem(self, dlgItem, widget, label=None): """Add a control with optional label, called by OptionDlgItem""" row = self.gridLayout.rowCount() if label: self.gridLayout.addWidget(label, row, 0) self.gridLayout.addWidget(widget, row, 1) else: self.gridLayout.addWidget(widget, row, 0, 1, 2) self.itemList.append(dlgItem) def startGroupBox(self, title, intSpace=5): """Use a group box for next added items""" self.curGroup = QtGui.QGroupBox(title, self) row = self.oldLayout.rowCount() self.oldLayout.addWidget(self.curGroup, row, 0, 1, 2) self.gridLayout = QtGui.QGridLayout(self.curGroup) def endGroupBox(self): """Cancel group box for next added items""" self.gridLayout = self.oldLayout self.curGroup = None def startNewColumn(self): """Cancel any group box and start a second column""" self.curGroup = None row = self.oldLayout.rowCount() self.gridLayout = QtGui.QGridLayout() self.columnLayout.addLayout(self.gridLayout) self.oldLayout = self.gridLayout def parentGroup(self): """Return parent for new widgets""" if self.curGroup: return self.curGroup return self def accept(self): """Called by dialog when OK button pressed""" for item in self.itemList: item.updateData() QtGui.QDialog.accept(self) class OptionDlgItem: """Base class for items to add to dialog""" def __init__(self, dlg, key, writeChg): self.dlg = dlg self.key = key self.writeChg = writeChg self.control = None def updateData(self): """Dummy update function""" pass class OptionDlgBool(OptionDlgItem): """Holds widget for bool checkbox""" def __init__(self, dlg, key, menuText, writeChg=True): OptionDlgItem.__init__(self, dlg, key, writeChg) self.control = QtGui.QCheckBox(menuText, dlg.parentGroup()) self.control.setChecked(dlg.option.boolData(key)) dlg.addItem(self, self.control) def updateData(self): """Update Option class based on checkbox status""" if self.control.isChecked() != self.dlg.option.boolData(self.key): if self.control.isChecked(): self.dlg.option.changeData(self.key, 'yes', self.writeChg) else: self.dlg.option.changeData(self.key, 'no', self.writeChg) class OptionDlgInt(OptionDlgItem): """Holds widget for int spinbox""" def __init__(self, dlg, key, menuText, min, max, writeChg=True, step=1, wrap=False, suffix=''): OptionDlgItem.__init__(self, dlg, key, writeChg) label = QtGui.QLabel(menuText, dlg.parentGroup()) self.control = QtGui.QSpinBox(dlg.parentGroup()) self.control.setMinimum(min) self.control.setMaximum(max) self.control.setSingleStep(step) self.control.setWrapping(wrap) self.control.setSuffix(suffix) self.control.setValue(dlg.option.intData(key, min, max)) dlg.addItem(self, self.control, label) def updateData(self): """Update Option class based on spinbox status""" if self.control.value() != int(self.dlg.option.numData(self.key)): self.dlg.option.changeData(self.key, str(self.control.value()), self.writeChg) class OptionDlgDbl(OptionDlgItem): """Holds widget for double line edit""" def __init__(self, dlg, key, menuText, min, max, writeChg=True): OptionDlgItem.__init__(self, dlg, key, writeChg) label = QtGui.QLabel(menuText, dlg.parentGroup()) self.control = QtGui.QLineEdit(str(dlg.option.numData(key, min, max)), dlg.parentGroup()) valid = QtGui.QDoubleValidator(min, max, 6, self.control) self.control.setValidator(valid) dlg.addItem(self, self.control, label) def updateData(self): """Update Option class based on edit status""" text = self.control.text() unusedPos = 0 if self.control.validator().validate(text, unusedPos)[0] != \ QtGui.QValidator.Acceptable: return num = float(str(text)) if num != self.dlg.option.numData(self.key): self.dlg.option.changeData(self.key, `num`, self.writeChg) class OptionDlgStr(OptionDlgItem): """Holds widget for string line edit""" def __init__(self, dlg, key, menuText, writeChg=True): OptionDlgItem.__init__(self, dlg, key, writeChg) label = QtGui.QLabel(menuText, dlg.parentGroup()) self.control = QtGui.QLineEdit(dlg.option.strData(key, True), dlg.parentGroup()) dlg.addItem(self, self.control, label) def updateData(self): """Update Option class based on edit status""" newStr = unicode(self.control.text()) if newStr != self.dlg.option.strData(self.key, True): self.dlg.option.changeData(self.key, newStr, self.writeChg) class OptionDlgRadio(OptionDlgItem): """Holds widget for exclusive radio button group""" def __init__(self, dlg, key, headText, textList, writeChg=True): # textList is list of tuples: optionText, labelText OptionDlgItem.__init__(self, dlg, key, writeChg) self.optionList = [x[0] for x in textList] buttonBox = QtGui.QGroupBox(headText, dlg.parentGroup()) self.control = QtGui.QButtonGroup(buttonBox) layout = QtGui.QVBoxLayout(buttonBox) buttonBox.setLayout(layout) optionSetting = dlg.option.strData(key) id = 0 for optionText, labelText in textList: button = QtGui.QRadioButton(labelText, buttonBox) layout.addWidget(button) self.control.addButton(button, id) id += 1 if optionText == optionSetting: button.setChecked(True) dlg.addItem(self, buttonBox) def updateData(self): """Update Option class based on button status""" data = self.optionList[self.control.checkedId()] if data != self.dlg.option.strData(self.key): self.dlg.option.changeData(self.key, data, self.writeChg) class OptionDlgPush(OptionDlgItem): """Holds widget for extra misc. push button""" def __init__(self, dlg, text, cmd): OptionDlgItem.__init__(self, dlg, '', 0) self.control = QtGui.QPushButton(text, dlg.parentGroup()) self.control.connect(self.control, QtCore.SIGNAL('clicked()'), cmd) dlg.addItem(self, self.control) TreeLine/source/treeline.pro0000644000175000017500000000222511651514477015110 0ustar dougdougSOURCES = cmdline.py \ conditional.py \ configdialog.py \ fieldformat.py \ genboolean.py \ gendate.py \ gennumber.py \ gentime.py \ globalref.py \ helpview.py \ icondict.py \ nodeformat.py \ numbering.py \ optiondefaults.py \ optiondlg.py \ option.py \ output.py \ p3.py \ plugininterface.py \ printdata.py \ printdialogs.py \ recentfiles.py \ spellcheck.py \ treedialogs.py \ treedoc.py \ treeeditviews.py \ treeflatview.py \ treeformats.py \ treeitem.py \ treeline.py \ treemainwin.py \ treerightviews.py \ treeselection.py \ treeview.py \ treexmlparse.py \ undo.py TRANSLATIONS = treeline_cs.ts \ treeline_de.ts \ treeline_es.ts \ treeline_fr.ts \ treeline_it.ts \ treeline_pt.ts \ treeline_ru.ts \ treeline_xx.ts TreeLine/source/treedoc.py0000644000175000017500000013740411656111622014554 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # treedoc.py, provides non-GUI base classes for document data # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #**************************************************************************** import sys import os.path import xml.sax.saxutils import codecs import gzip import zipfile import StringIO import p3 import nodeformat from treeformats import TreeFormats from treeitem import TreeItem import treeselection import treexmlparse import undo import optiondefaults import globalref try: from __main__ import __version__ except ImportError: __version__ = '??' escDict = {'"': '"', chr(12): ''} # added quotes for c in range(9) + range(11, 13) + range(14, 32): escDict[chr(c)] = '' # ignore low ascii chars unEscDict = {'"': '"'} encryptPrefix = '>>TL+enc' tabbedImport = 'readTabbed' tableImport = 'readTable' textLineImport = 'readLines' textParaImport = 'readPara' treepadImport = 'readTreepad' xbelImport = 'readXbel' mozillaImport = 'readMozilla' xmlImport = 'readXml' odfImport = 'readOdf' class TreeDoc(object): """Tree document class - stores root and has tree utilities""" passwordDict = {} childFieldSepDflt = ', ' rootTitleDefault = _('Main', 'default root title') folderName = _('FOLDER', 'bookmark format folder name') bookmarkName = _('BOOKMARK', 'bookmark format name') separatorName = _('SEPARATOR', 'bookmark format separator name') bookmarkRootTitle = _('Bookmarks') copyFormat = None def __init__(self, filePath=None, setNewDefaults=False, importType=None): """Open filePath (can also be file ref) if given, setNewDefaults uses user defaults for compression & encryption, importType gives an import method to read the file""" globalref.docRef = self self.root = None self.treeFormats = TreeFormats() self.fileInfoItem = TreeItem(None, nodeformat.FileInfoFormat.name) self.fileInfoFormat = None TreeDoc.copyFormat = nodeformat.NodeFormat('_DUMMY__ROOT_', {}, TreeFormats.fieldDefault) self.undoStore = undo.UndoRedoStore() self.redoStore = undo.UndoRedoStore() self.sortFields = [''] self.fileName = '' self.spaceBetween = True self.lineBreaks = True self.formHtml = True self.childFieldSep = TreeDoc.childFieldSepDflt self.spellChkLang = '' self.xlstLink = '' self.xslCssLink = '' self.tlVersion = __version__ self.fileInfoFormat = nodeformat.FileInfoFormat() if filePath: if importType: getattr(self, importType)(filePath) else: self.readFile(filePath) else: self.treeFormats = TreeFormats({}, True) self.root = TreeItem(None, TreeFormats.rootFormatDefault) self.root.setTitle(TreeDoc.rootTitleDefault) self.modified = False if setNewDefaults or not hasattr(self, 'compressFile'): self.compressFile = globalref.options.boolData('CompressNewFiles') self.encryptFile = globalref.options.boolData('EncryptNewFiles') elif not hasattr(self, 'encryptFile'): self.encryptFile = False self.selection = treeselection.TreeSelection([self.root]) self.fileInfoFormat.translateFields() self.fileInfoFormat.updateFileInfo() def hasPassword(self, filePath): """Return True if a password is available for filePath""" key = filePath.encode(sys.getfilesystemencoding()) return TreeDoc.passwordDict.has_key(key) def setPassword(self, filePath, password): """Set encrytion password for the filePath""" key = filePath.encode(sys.getfilesystemencoding()) TreeDoc.passwordDict[key] = password.encode('utf-8') def clearPassword(self, filePath): """Remove password for filePath if present""" key = filePath.encode(sys.getfilesystemencoding()) try: del TreeDoc.passwordDict[key] except KeyError: pass def getReadFileObj(self, fileRef): """Return file object and set self.compressFile to False/True, fileRef is either file path or file object""" if not hasattr(fileRef, 'read'): fileRef = file(fileRef.encode(sys.getfilesystemencoding()), 'rb') # binary mode req'd for encryption if hasattr(fileRef, 'seek'): fileRef.seek(0) prefix = fileRef.read(2) if hasattr(fileRef, 'seek'): fileRef.seek(0) else: oldFileRef = fileRef fileRef = StringIO.StringIO(prefix + oldFileRef.read()) fileRef.name = oldFileRef.name oldFileRef.close() if prefix == '\037\213': name = fileRef.name fileRef = gzip.GzipFile(fileobj=fileRef) fileRef.name = name # may already be a gzip object from before password prompt self.compressFile = isinstance(fileRef, gzip.GzipFile) return fileRef def decryptFile(self, fileObj): """Decrypt file if was encrypted""" name = fileObj.name prefix = fileObj.read(len(encryptPrefix)) self.encryptFile = prefix == encryptPrefix if self.encryptFile: password = TreeDoc.passwordDict.get(fileObj.name, '') if not password: fileObj.close() raise PasswordError, 'Missing password' try: text = p3.p3_decrypt(fileObj.read(), password) except p3.CryptError: fileObj.close() raise PasswordError, 'Incorrect password' fileObj.close() fileObj = StringIO.StringIO(text) fileObj.name = name else: fileObj.seek(0) return fileObj def getEncodedFileObj(self, fileRef, encoding, errors): """Return open file object with specified encoding""" return codecs.getreader(encoding)(self.getReadFileObj(fileRef), errors) def getWriteFileObj(self, fileRef, forceCompress): """Return write file object, compressed or not based on forceCompress, but always compress if has .gz extension, fileRef is either file path or file object""" if not hasattr(fileRef, 'read'): fileRef = file(fileRef.encode(sys.getfilesystemencoding()), 'wb') if fileRef.name.endswith('.gz') or forceCompress: name = fileRef.name fileRef = gzip.GzipFile(fileobj=fileRef) fileRef.name = name return fileRef def readFile(self, fileRef): """Open and read file - raise exception on failure, fileRef is either file path or file object""" filePath = hasattr(fileRef, 'read') and \ unicode(fileRef.name, sys.getfilesystemencoding()) or \ fileRef try: f = self.getReadFileObj(fileRef) f = self.decryptFile(f) handler = treexmlparse.TreeSaxHandler(self) input = xml.sax.InputSource() input.setByteStream(f) input.setEncoding('utf-8') reader = xml.sax.make_parser() reader.setContentHandler(handler) reader.setFeature(xml.sax.handler.feature_external_ges, 0) reader.parse(input) except IOError: print 'Error - could not read file', \ filePath.encode(globalref.localTextEncoding) raise except UnicodeError: print 'Error - bad Unicode in file', \ filePath.encode(globalref.localTextEncoding) f.close() raise except xml.sax.SAXException: f.close() raise ReadFileError(_('Could not open as treeline file')) f.close() self.root = handler.rootItem self.fileName = filePath self.treeFormats = TreeFormats(handler.formats) self.fileInfoFormat.replaceListFormat() self.treeFormats.updateAutoChoices() self.treeFormats.updateUniqueID() self.treeFormats.updateDerivedTypes() if not self.tlVersion: # file from before 0.12.80, fix number format for format in self.treeFormats.values(): for field in format.fieldList: if field.typeName == 'Number': field.format = field.format.replace(',', '\,') def readTabbed(self, fileRef, errors='strict'): """Import tabbed data into a flat tree - raise exception on failure""" try: f = self.getEncodedFileObj(fileRef, globalref.localTextEncoding, errors) filePath = unicode(f.name, sys.getfilesystemencoding()) textList = f.readlines() except UnicodeError: print 'Warning - bad unicode characters were replaced' if errors == 'strict': self.readTabbed(fileRef, 'replace') else: f.close() return f.close() bufList = [(text.count('\t', 0, len(text) - len(text.lstrip())), text.strip()) for text in textList if text.strip()] if bufList: buf = bufList.pop(0) if buf[0] == 0: # set default formats ROOT & DEFAULT self.treeFormats = TreeFormats({}, True) newRoot = TreeItem(None, TreeFormats.rootFormatDefault) newRoot.setTitle(buf[1]) if newRoot.loadTabbedChildren(bufList): self.root = newRoot self.fileName = filePath return raise ReadFileError(_('Error in tabbed list')) def readTable(self, fileRef, errors='strict'): """Import table data into a flat tree - raise exception on failure""" try: f = self.getEncodedFileObj(fileRef, globalref.localTextEncoding, errors) filePath = unicode(f.name, sys.getfilesystemencoding()) textList = f.readlines() except UnicodeError: print 'Warning - bad unicode characters were replaced' if errors == 'strict': self.readTable(fileRef, 'replace') else: f.close() return f.close() self.treeFormats = TreeFormats({}, True) # set defaults ROOT & DEFAULT newRoot = TreeItem(None, TreeFormats.rootFormatDefault) defaultFormat = self.treeFormats[TreeFormats.formatDefault] defaultFormat.fieldList = [] defaultFormat.lineList = [] defaultFormat.addTableFields(textList.pop(0).strip().split('\t')) newRoot.setTitle(TreeDoc.rootTitleDefault) for line in textList: newItem = TreeItem(newRoot, TreeFormats.formatDefault) newRoot.childList.append(newItem) lineList = line.strip().split('\t') try: for num in range(len(lineList)): newItem.data[self.treeFormats[TreeFormats.formatDefault]. fieldList[num].name] = lineList[num].strip() except IndexError: print 'Too few headings to read data as a table' raise ReadFileError(_('Too few headings to read data as table')) self.root = newRoot self.fileName = filePath def readLines(self, fileRef, errors='strict'): """Import plain text, node per line""" try: f = self.getEncodedFileObj(fileRef, globalref.localTextEncoding, errors) filePath = unicode(f.name, sys.getfilesystemencoding()) textList = f.readlines() except UnicodeError: print 'Warning - bad unicode characters were replaced' if errors == 'strict': self.readLines(fileRef, 'replace') else: f.close() return f.close() self.treeFormats = TreeFormats({}, True) # set defaults ROOT & DEFAULT newRoot = TreeItem(None, TreeFormats.rootFormatDefault) defaultFormat = self.treeFormats[TreeFormats.formatDefault] defaultFormat.fieldList = [] defaultFormat.lineList = [] defaultFormat.addTableFields([TreeFormats.textFieldName]) newRoot.setTitle(TreeDoc.rootTitleDefault) for line in textList: line = line.strip() if line: newItem = TreeItem(newRoot, TreeFormats.formatDefault) newRoot.childList.append(newItem) newItem.data[TreeFormats.textFieldName] = line self.root = newRoot self.fileName = filePath def readPara(self, fileRef, errors='strict'): """Import plain text, blank line delimitted""" try: f = self.getEncodedFileObj(fileRef, globalref.localTextEncoding, errors) filePath = unicode(f.name, sys.getfilesystemencoding()) fullText = f.read().replace('\r', '') except UnicodeError: print 'Warning - bad unicode characters were replaced' if errors == 'strict': self.readPara(fileRef, 'replace') else: f.close() return textList = fullText.split('\n\n') f.close() self.treeFormats = TreeFormats({}, True) # set defaults ROOT & DEFAULT newRoot = TreeItem(None, TreeFormats.rootFormatDefault) defaultFormat = self.treeFormats[TreeFormats.formatDefault] defaultFormat.fieldList = [] defaultFormat.lineList = [] defaultFormat.iconName = 'doc' defaultFormat.addTableFields([TreeFormats.textFieldName]) defaultFormat.fieldList[0].numLines = globalref.options.\ intData('MaxEditLines', 1, optiondefaults.maxNumLines) newRoot.setTitle(TreeDoc.rootTitleDefault) for line in textList: line = line.strip() if line: newItem = TreeItem(newRoot, TreeFormats.formatDefault) newRoot.childList.append(newItem) newItem.data[TreeFormats.textFieldName] = line self.root = newRoot self.fileName = filePath def readTreepad(self, fileRef, errors='strict'): """Read Treepad text-node file""" try: f = self.getEncodedFileObj(fileRef, globalref.localTextEncoding, errors) filePath = unicode(f.name, sys.getfilesystemencoding()) textList = f.read().split(' 5P9i0s8y19Z') f.close() except UnicodeError: # error common - broken unicode on windows print 'Warning - bad unicode characters were replaced' if errors == 'strict': self.readTreepad(fileRef, 'replace') else: f.close() return self.treeFormats = TreeFormats() format = nodeformat.NodeFormat(TreeFormats.formatDefault) titleFieldName = _('Title', 'title field name') format.addNewField(titleFieldName) format.addLine(u'{*%s*}' % titleFieldName) numLines = globalref.options.intData('MaxEditLines', 1, optiondefaults.maxNumLines) format.addNewField(TreeFormats.textFieldName, {'lines': repr(numLines)}) format.addLine(u'{*%s*}' % TreeFormats.textFieldName) self.treeFormats[format.name] = format itemList = [] for text in textList: text = text.strip() if text: try: text = text.split('', 1)[1].lstrip() lines = text.split('\n') title = lines[0] level = int(lines[1]) lines = lines[2:] except (ValueError, IndexError): print 'Error - bad file format in %s' % \ filePath.encode(globalref.localTextEncoding) raise ReadFileError(_('Bad file format in %s') % filePath) item = TreeItem(None, format.name) item.data[titleFieldName] = title item.data[TreeFormats.textFieldName] = '\n'.join(lines) item.level = level itemList.append(item) self.root = itemList[0] parentList = [] for item in itemList: if item.level != 0: parentList = parentList[:item.level] item.parent = parentList[-1] parentList[-1].childList.append(item) parentList.append(item) self.root = itemList[0] self.fileName = filePath def createBookmarkFormat(self): """Return a set of formats for bookmark imports""" treeFormats = TreeFormats() format = nodeformat.NodeFormat(TreeDoc.folderName) format.addNewField(TreeFormats.fieldDefault) format.addLine(u'{*%s*}' % TreeFormats.fieldDefault) format.addLine(u'{*%s*}' % TreeFormats.fieldDefault) format.iconName = 'folder_3' treeFormats[format.name] = format format = nodeformat.NodeFormat(TreeDoc.bookmarkName) format.addNewField(TreeFormats.fieldDefault) format.addLine(u'{*%s*}' % TreeFormats.fieldDefault) format.addLine(u'{*%s*}' % TreeFormats.fieldDefault) format.addNewField(TreeFormats.linkFieldName, {'type': 'URL'}) format.addLine(u'{*%s*}' % TreeFormats.linkFieldName) format.iconName = 'bookmark' treeFormats[format.name] = format format = nodeformat.NodeFormat(TreeDoc.separatorName) format.addNewField(TreeFormats.fieldDefault) format.addLine(u'------------------') format.addLine(u'
') treeFormats[format.name] = format return treeFormats def readXbel(self, fileRef): """Read XBEL format bookmarks""" formats = self.createBookmarkFormat() try: f = self.getReadFileObj(fileRef) filePath = unicode(f.name, sys.getfilesystemencoding()) handler = treexmlparse.\ XbelSaxHandler(formats[TreeDoc.folderName], formats[TreeDoc.bookmarkName], formats[TreeDoc.separatorName]) input = xml.sax.InputSource() input.setByteStream(f) input.setEncoding('utf-8') reader = xml.sax.make_parser() reader.setContentHandler(handler) reader.setFeature(xml.sax.handler.feature_external_ges, 0) reader.parse(input) except UnicodeError: print 'Error - bad Unicode in file', \ filePath.encode(globalref.localTextEncoding) f.close() raise ReadFileError(_('Problem with Unicode characters in file')) except xml.sax.SAXException: f.close() raise ReadFileError(_('Could not open as XBEL file')) f.close() if not handler.rootItem: raise ReadFileError(_('Could not open as XBEL file')) self.root = handler.rootItem if not self.root.data.get(TreeFormats.fieldDefault, ''): self.root.data[TreeFormats.fieldDefault] = \ TreeDoc.bookmarkRootTitle self.fileName = filePath self.treeFormats = formats def readMozilla(self, fileRef, errors='strict'): """Read Mozilla HTML format bookmarks""" formats = self.createBookmarkFormat() try: f = self.getEncodedFileObj(fileRef, 'utf-8', errors) filePath = unicode(f.name, sys.getfilesystemencoding()) fullText = f.read() except UnicodeError: print 'Warning - bad unicode characters were replaced' if errors == 'strict': self.readMozilla(fileRef, 'replace') else: f.close() return try: handler = treexmlparse.\ HtmlBookmarkHandler(formats[TreeDoc.folderName], formats[TreeDoc.bookmarkName], formats[TreeDoc.separatorName]) handler.feed(fullText) handler.close() except treexmlparse.HtmlParseError: raise ReadFileError(_('Could not open as HTML bookmark file')) if not handler.rootItem: raise ReadFileError(_('Could not open as HTML bookmark file')) self.root = handler.rootItem if not self.root.data.get(TreeFormats.fieldDefault, ''): self.root.data[TreeFormats.fieldDefault] = \ TreeDoc.bookmarkRootTitle self.fileName = filePath self.treeFormats = formats def readXml(self, fileRef): """Read a generic (non-TreeLine) XML file""" try: f = self.getReadFileObj(fileRef) filePath = unicode(f.name, sys.getfilesystemencoding()) handler = treexmlparse.GenericXmlHandler() input = xml.sax.InputSource() input.setByteStream(f) input.setEncoding('utf-8') reader = xml.sax.make_parser() reader.setContentHandler(handler) reader.setFeature(xml.sax.handler.feature_external_ges, 0) reader.parse(input) except UnicodeError: print 'Error - bad Unicode in file', \ filePath.encode(globalref.localTextEncoding) f.close() raise ReadFileError(_('Problem with Unicode characters in file')) except xml.sax.SAXException: f.close() raise ReadFileError(_('Could not open XML file')) f.close() if not handler.rootItem: raise ReadFileError(_('Could not open XML file')) self.root = handler.rootItem self.fileName = filePath self.treeFormats = TreeFormats(handler.formats) for format in self.treeFormats.values(): format.fixImportedFormat(treexmlparse.GenericXmlHandler. textFieldName) def readOdf(self, fileRef): """Read an Open Document Format (ODF) file""" self.treeFormats = TreeFormats(None, True) rootItem = TreeItem(None, TreeFormats.rootFormatDefault, TreeDoc.rootTitleDefault) defaultFormat = self.treeFormats[TreeFormats.formatDefault] defaultFormat.addNewField(TreeFormats.textFieldName, {u'html': 'n', u'lines': '6'}) defaultFormat.changeOutputLines([u'{*%s*}' % TreeFormats.fieldDefault, u'{*%s*}' % TreeFormats.textFieldName]) try: f = self.getReadFileObj(fileRef) filePath = unicode(f.name, sys.getfilesystemencoding()) zip = zipfile.ZipFile(f, 'r') text = zip.read('content.xml') handler = treexmlparse.OdfSaxHandler(rootItem, defaultFormat) xml.sax.parseString(text, handler) except (zipfile.BadZipfile, KeyError): f.close() raise ReadFileError(_('Could not unzip ODF file')) except UnicodeError: f.close() raise ReadFileError(_('Problem with Unicode characters in file')) except xml.sax.SAXException: f.close() raise ReadFileError(_('Could not open corrupt ODF file')) f.close() self.root = rootItem self.fileName = filePath def readXmlString(self, string): """Read xml string and return top item or None""" try: handler = treexmlparse.TreeSaxHandler(self) xml.sax.parseString(string.encode('utf-8'), handler) except xml.sax.SAXException: return None return handler.rootItem def readXmlStringAndFormat(self, string): """Read xml string and return tuple of top item and new formats""" try: handler = treexmlparse.TreeSaxHandler(self) xml.sax.parseString(string.encode('utf-8'), handler) except xml.sax.SAXException: return (None, []) formats = [format for format in handler.formats.values() if format.name not in self.treeFormats] try: formats.remove(TreeDoc.copyFormat) except ValueError: pass formatNames = [format.name for format in formats] + \ self.treeFormats.keys() for format in formats: if format.genericType not in formatNames: format.genericType = '' return (handler.rootItem, formats) def writeFile(self, fileRef, updateInfo=True): """Write file - raises IOError on failure""" lines = [u''] if self.xlstLink: lines.append(u'' % self.xlstLink) lines.extend(self.root.branchXml([], True)) text = '\n'.join(lines).encode('utf-8') try: f = self.getWriteFileObj(fileRef, self.compressFile) except IOError: print 'Error - could not write file' raise filePath = unicode(f.name, sys.getfilesystemencoding()) if self.encryptFile: key = filePath.encode(sys.getfilesystemencoding()) password = TreeDoc.passwordDict.get(key, '') if not password: if key.endswith('~'): # for auto-save filename password = TreeDoc.passwordDict.get(key[:-1], '') if not password: raise PasswordError, 'Missing password' text = encryptPrefix + p3.p3_encrypt(text, password) try: f.write(text) except IOError: print 'Error - could not write file', \ filePath.encode(globalref.localTextEncoding) raise f.close() if filePath.endswith('.gz'): self.compressFile = True if updateInfo: self.modified = False self.tlVersion = __version__ self.fileName = filePath self.fileInfoFormat.updateFileInfo() def exportHtml(self, fileRef, item, includeRoot, openOnly=False, indent=20, addHeader=False): """Save branch as html to file w/o columns""" outGroup = item.outputItemList(includeRoot, openOnly, True) self.exportHtmlColumns(fileRef, outGroup, 1, indent, addHeader) def exportHtmlColumns(self, fileRef, outGroup, numCol=1, indent=20, addHeader=False): """Save contents of outGroup as html to file in columns""" try: f = self.getWriteFileObj(fileRef, False) except IOError: print 'Error - could not write file' raise filePath = unicode(f.name, sys.getfilesystemencoding()) if self.lineBreaks: outGroup.addBreaks() outGroups = outGroup.splitColumns(numCol) for group in outGroups: group.addPrefix() group.addIndents() htmlTitle = os.path.splitext(os.path.basename(filePath))[0] lines = [u'', u'', u'', u'', u'%s' % htmlTitle, u'', u'', u''] if addHeader: header = self.fileInfoFormat.getHeaderFooter(True) if header: lines.append(header) lines.extend([u'', u'', u'
']) for item in outGroups[0]: lines.extend(item.textLines) for group in outGroups[1:]: lines.append(u'') for item in group: lines.extend(item.textLines) lines.extend([u'
']) if addHeader: footer = self.fileInfoFormat.getHeaderFooter(False) if footer: lines.append(footer) lines.extend([u'', u'']) try: f.writelines([(line + '\n').encode('utf-8') for line in lines]) except IOError: print 'Error - could not write file', \ filePath.encode(globalref.localTextEncoding) raise f.close() def exportDirTable(self, dirName, nodeList, addHeader=False): """Write tree to nested directory struct with html tables""" oldDir = os.getcwd() os.chdir(dirName.encode(sys.getfilesystemencoding())) if addHeader: header = self.fileInfoFormat.getHeaderFooter(True) footer = self.fileInfoFormat.getHeaderFooter(False) else: header = footer = '' if len(nodeList) > 1: self.treeFormats.addIfMissing(TreeDoc.copyFormat) item = TreeItem(None, TreeDoc.copyFormat.name) item.data[TreeFormats.fieldDefault] = TreeDoc.rootTitleDefault for child in nodeList: item.childList.append(child) child.parent = item else: item = nodeList[0] linkDict = {} item.createDirTableLinkDict(linkDict, os.getcwd()) item.exportDirTable(linkDict, None, header, footer) self.treeFormats.removeQuiet(TreeDoc.copyFormat) os.chdir(oldDir) def exportDirPage(self, dirName, nodeList): """Write tree to nested direct struct with html page for each node""" oldDir = os.getcwd() os.chdir(dirName.encode(sys.getfilesystemencoding())) cssLines = ['#sidebar {', 'width: 16em;', 'float: left;', 'clear: left;', 'border-right: 1px solid black;', 'margin-right: 1em;', '}'] try: f = codecs.open('default.css', 'w', 'utf-8') f.writelines([(line + '\n').encode('utf-8') for line in cssLines]) except (IOError, UnicodeError): print 'Error - could not write file to default.css' raise IOError(_('Error - cannot write file to %s') % 'default.css') f.close() if len(nodeList) > 1: self.treeFormats.addIfMissing(TreeDoc.copyFormat) item = TreeItem(None, TreeDoc.copyFormat.name) item.data[TreeFormats.fieldDefault] = TreeDoc.rootTitleDefault for child in nodeList: item.childList.append(child) child.parent = item else: item = nodeList[0] linkDict = {} item.createDirPageLinkDict(linkDict, os.getcwd()) item.exportDirPage(linkDict) self.treeFormats.removeQuiet(TreeDoc.copyFormat) os.chdir(oldDir) def exportXslt(self, fileRef, includeRoot, indent=20): """Write XSLT file and add link in treeline file""" try: f = self.getWriteFileObj(fileRef, False) except IOError: print 'Error - could not write file' raise filePath = unicode(f.name, sys.getfilesystemencoding()) title = os.path.splitext(os.path.basename(filePath))[0] lines = [u'', u"", u'', u'', u''] if self.xslCssLink: lines.append('' % self.xslCssLink) lines.extend([u'%s' % title, u'', u'', u'', u'', u'', u'']) if not includeRoot: lines.extend([u'', u'' % self.root.formatName, u'', u'']) for formatName in self.treeFormats: lines.extend(self.treeFormats[formatName]. xsltTemplate(indent, True)) lines.extend([u'', u'', u'', u'']) try: f.writelines([(line + '\n').encode('utf-8') for line in lines]) except IOError: print 'Error - could not write file', \ filePath.encode(globalref.localTextEncoding) raise f.close() # find relative link path trlPath = os.path.abspath(self.fileName).split(os.sep) xslPath = os.path.abspath(filePath).split(os.sep) while trlPath[0] == xslPath[0]: del trlPath[0] del xslPath[0] xslPath = '/'.join(['..'] * (len(trlPath) - len(xslPath)) + xslPath) link = u'xml-stylesheet type="text/xsl" href="%s"' % xslPath if self.xlstLink != link: self.xlstLink = link self.modified = True def exportTrlSubtree(self, fileRef, nodeList, addBranches=True): """Write subtree TRL file starting form item""" lines = [u''] if self.xlstLink: lines.append(u'' % self.xlstLink) if not addBranches: newList = [] for item in nodeList: # replace items with childless items newItem = TreeItem(item.parent, item.formatName) newItem.data = item.data newList.append(newItem) nodeList = newList if len(nodeList) > 1: format = nodeformat.NodeFormat(TreeFormats.rootFormatDefault, {}, TreeFormats.fieldDefault) self.treeFormats.addIfMissing(format) item = TreeItem(None, format.name) item.data[TreeFormats.fieldDefault] = TreeDoc.rootTitleDefault for child in nodeList: item.childList.append(child) child.parent = item else: item = nodeList[0] lines.extend(item.branchXml([], True)) try: f = self.getWriteFileObj(fileRef, self.compressFile) f.writelines([(line + '\n').encode('utf-8') for line in lines]) except IOError: print 'Error - could not write file' self.treeFormats.removeQuiet(TreeDoc.copyFormat) raise f.close() self.treeFormats.removeQuiet(TreeDoc.copyFormat) def exportTable(self, fileRef, nodeList, addBranches=True): """Write data to table for nodes or children of nodes""" if addBranches: newList = [] for item in nodeList: newList.extend(item.childList) nodeList = newList typeList = [] headings = [] tableList = [] for item in nodeList: itemFormat = item.nodeFormat() if itemFormat not in typeList: for field in itemFormat.fieldNames(): if field not in headings: headings.append(field) typeList.append(itemFormat) tableList.append(u'\t'.join([item.data.get(head, '') for head in headings])) tableList.insert(0, u'\t'.join([head for head in headings])) try: text = os.linesep.join(tableList).\ encode(globalref.localTextEncoding, 'strict') except (ValueError, UnicodeError): print 'Warning - bad unicode characters were replaced' text = os.linesep.join(tableList).\ encode(globalref.localTextEncoding, 'replace') try: f = self.getWriteFileObj(fileRef, False) f.write(text) except IOError: print 'Error - could not write file' raise f.close() def exportTabbedTitles(self, fileRef, nodeList, addBranches=True, includeRoot=True, openOnly=False): """Write tabbed titles for descendants of item""" if addBranches: initLevel = not includeRoot and -1 or 0 titleList = [] for item in nodeList: itemList = item.exportToText(initLevel, openOnly) if not includeRoot: del itemList[0] titleList.extend(itemList) else: titleList = [item.title() for item in nodeList] try: text = os.linesep.join(titleList).\ encode(globalref.localTextEncoding, 'strict') except (ValueError, UnicodeError): print 'Warning - bad unicode characters were replaced' text = os.linesep.join(titleList).\ encode(globalref.localTextEncoding, 'replace') try: f = self.getWriteFileObj(fileRef, False) f.write(text) except IOError: print 'Error - could not write file' raise f.close() def exportXbel(self, fileRef, nodeList, addBranches=True): """Export XBEL bookmarks""" if len(nodeList) > 1 or not addBranches: title = TreeDoc.bookmarkRootTitle level = 1 else: title = xml.sax.saxutils.escape(nodeList[0].title(), escDict) level = 0 lines = [u'', u'', u'%s' % title] for item in nodeList: lines.extend(item.exportXbelBookmarks(level, addBranches)) lines.append(u'') try: f = self.getWriteFileObj(fileRef, False) f.writelines([(line + '\n').encode('utf-8') for line in lines]) except IOError: print 'Error - could not write file' raise f.close() def exportHtmlBookmarks(self, fileRef, nodeList, addBranches=True): """Export HTML bookmarks""" if len(nodeList) > 1 or not addBranches: title = TreeDoc.bookmarkRootTitle level = 1 else: title = xml.sax.saxutils.escape(nodeList[0].title()) level = 0 lines = [u'', u'', u'%s' % title, u'

%s

' % title, ''] for item in nodeList: lines.extend(item.exportHtmlBookmarks(level, addBranches)) try: f = self.getWriteFileObj(fileRef, False) f.writelines([(line + '\n').encode('utf-8') for line in lines]) except IOError: print 'Error - could not write file' raise f.close() def exportGenericXml(self, fileRef, nodeList, addBranches=True): """Export generic XML""" lines = [u''] level = 0 if len(nodeList) > 1: lines.append(u'<%s>' % TreeFormats.rootFormatDefault) level = 1 for item in nodeList: lines.extend(item.exportGenericXml(treexmlparse.GenericXmlHandler. textFieldName, level)) if len(nodeList) > 1: lines.append(u'' % TreeFormats.rootFormatDefault) try: f = self.getWriteFileObj(fileRef, False) f.writelines([(line + '\n').encode('utf-8') for line in lines]) except IOError: print 'Error - could not write file' raise f.close() def exportOdf(self, fileRef, nodeList, fontName, fontSize, fontFixed=False, addBranches=True, includeRoot=True, openOnly=False): """Export an ODF format text file""" TreeItem.maxLevel = 0 commonHeader = u'' commonAttr = u' office:version="1.0" '\ 'xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:'\ 'xsl-fo-compatible:1.0" '\ 'xmlns:office="urn:oasis:names:tc:opendocument:'\ 'xmlns:office:1.0" '\ 'xmlns:style="urn:oasis:names:tc:opendocument:xmlns:'\ 'style:1.0" '\ 'xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:'\ 'svg-compatible:1.0" '\ 'xmlns:text="urn:oasis:names:tc:opendocument:'\ 'xmlns:text:1.0">' pitch = fontFixed and 'fixed' or 'variable' sizeDelta = 2 fontDecl = [u'', u'' % (pitch, fontName, fontName), u''] lines = [commonHeader, u'', u'']) for item in nodeList: lines.extend(item.exportOdf(1, addBranches, includeRoot, openOnly)) lines.extend([u'', u'', u'']) manifest = [commonHeader, u'', u'', u'', u'', u''] styles = [commonHeader, u'', u'', u'', u'' % (fontSize, fontName), u'', u'', u'', u'', u'', u'', u'', u'' % (fontSize + sizeDelta, fontName), u'']) outline = [u''] for level in range(1, TreeItem.maxLevel + 1): size = fontSize if level <= 2: size += 2 * sizeDelta elif level <=4: size += sizeDelta italic = ' ' if level % 2 == 0: italic = 'fo:font-style="italic" ' styles.extend([u'' % \ (level, level, level), u'' % \ (size, italic), u'']) outline.append(u'' % level) styles.extend(outline) styles.extend([u'', u'', u'', u'', u'', u'', u'', u'', u'', u'', u'']) try: f = zipfile.ZipFile(fileRef, 'w', zipfile.ZIP_DEFLATED) f.writestr('content.xml', u'\n'.join(lines).encode('utf-8') + '\n') f.writestr('META-INF/manifest.xml', u'\n'.join(manifest) + '\n') f.writestr('styles.xml', u'\n'.join(styles) + '\n') except IOError: print 'Error - could not write file' raise f.close() class ReadFileError(Exception): """Exception class for errors on reading file content""" pass class PasswordError(Exception): """Exception class for missing or invalid encryption passwords""" pass def testXmlParser(): """Return True if parser works correctly""" try: handler = xml.sax.ContentHandler() xml.sax.parseString('test', handler) except xml.sax.SAXException: return False return True def splitPath(path): """Return a list of elements of path, ignores drive spec""" result = [] path = os.path.splitdrive(path)[1] while True: path, tail = os.path.split(path) result.insert(0, tail) if path[-1:] in ('', '/', '\\'): return result def relativePath(base, destination): """Return a path to destination relative to base, assumes paths are absolute""" if os.path.splitdrive(base)[0] != os.path.splitdrive(destination)[0]: return destination base = splitPath(base) destination = splitPath(destination) while base and destination and base[0] == destination[0]: base = base[1:] destination = destination[1:] prefix = '..%s' % os.sep * len(base) if prefix: destination.insert(0, prefix) if not destination: return '' return os.path.join(*destination) if __name__ == '__main__': doc = TreeDoc() if len(sys.argv) > 1: doc.readFile(sys.argv[1]) print '\n'.join(doc.root.exportToText()) print print '\n'.join(doc.root.childList[0].formatChildText()) TreeLine/source/plugininterface.py0000644000175000017500000007627611651514477016331 0ustar dougdoug#!/usr/bin/env python #**************************************************************************** # plugininterface.py, provides an interface class for plugin extension modules # # TreeLine, an information storage program # Copyright (C) 2006, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, either Version 2 or any later # version. This program is distributed in the hope that it will be useful, # but WITTHOUT ANY WARRANTY. See the included LICENSE file for details. #***************************************************************************** """Plugin Interface Rules Plugins are python files located in the plugins directory (/lib/treeline/plugins/ on Linux or TreeLine\lib\plugins\ on Windows). Plugins must define a function named "main" that takes an instance of the PluginInterface class as its only argument. The function should initialize the plugin. It is called after the TreeLine GUI is initialized (with a new file) but before any other files are opened. The return value of the function is stored by TreeLine to avoid garbage collection of the reference (although this shouldn't be necessary for non-trivial plugins). There should be a module doc string defined by the plugin. The first line is used as the plugin listing in Help->About Plugins. It should contain the plugin name and a very brief description. To avoid problems with plugins breaking when TreeLine is revised, the plugin API is restricted to the methods of the PluginInterface class. References from within the method code and elsewhere in TreeLine code should not be used. Exceptions to this rule include certain data members of node objects (childList, parent and data) and the interface's mainWin data member (to be used only as a parent for new Qt objects). Of course, if a method returns a Qt object, the normal Qt API is available. There are methods that setup callback functions for various TreeLine operations. Probably the most useful is the view update callback, which is called any time something changes in TreeLine requiring view or control availability updates. Plugins used with windows binary installations are limited to the Python modules that are used somewhere in TreeLine itself. No other modules are available, with the exception of urllib2, which is specifically included in the binary for plugin use. """ import sys import os.path import tempfile from PyQt4 import QtCore, QtGui import treedoc import nodeformat import optiondefaults import globalref class PluginInterface(object): """Defines the available interface for the plugins""" def __init__(self, mainWin): self.mainWin = mainWin # may be used in plugin as widget parent only self.viewUpdateCallbacks = [] # internal use only self.dataChangeCallbacks = [] # internal use only self.fileNewCallbacks = [] # internal use only self.fileOpenCallbacks = [] # internal use only self.fileSaveCallbacks = [] # internal use only def pluginPath(self): """Return path of this plugin's directory""" try: frame = sys._getframe(1) fileName = frame.f_code.co_filename finally: del frame return os.path.dirname(fileName) def getLanguage(self): """Return language code used by TreeLine for translations""" return globalref.lang #************************************************************************* # Node Interfaces: #************************************************************************* def getCurrentNode(self): """Return a reference to the currently active node""" return globalref.docRef.selection.currentItem def getSelectedNodes(self): """Return a list of currently selected nodes""" return list(globalref.docRef.selection) def changeSelection(self, newSelectList): """Change tree selections, last item in newSelectList becomes the current item, views are updated""" globalref.docRef.selection.change(newSelectList) def getRootNode(self): """Return a reference to the root node""" return globalref.docRef.root def getNodeChildList(self, node): """Return a list of node's child nodes, this method provided for completeness - node.childList may be used directly""" return node.childList def getNodeParent(self, node): """Return node's parent node or None if node is root, this method provided for completeness - node.parent may be used directly""" return node.parent def getNodeDescendantList(self, node): """Return a list containing the current node and all of its descendant nodes""" return node.descendantList(True) def addChildNode(self, parent, text=u'New', pos=-1): """Add new child before position, -1 is at end - return new item""" return parent.addChild(text, pos) def insertSibling(self, siblingNode, text=u'New', inAfter=False): """Add new sibling before or after sibling - return new item""" return siblingNode.insertSibling(text, inAfter) def setNodeOpenClosed(self, node, setOpen=True): """Open children in tree if open is True, close children if False""" node.open = setOpen def getNodeDataDict(self, node): """Return a dictionary containing the node's raw field data field names are the dictionary keys, the data is unicode text, this method provided for completeness - node.data may be used directly""" return node.data def getNodeTitle(self, node): """Return the formatted unicode text for the node's title as shown in the tree""" return node.title() def setNodeTitle(self, node, titleText): """Set the node's title to titleText by modifying the field data used in the title format, returns True if successful, False otherwise""" return node.setTitle(titleText) def getNodeOutput(self, node, lineSep='
\n'): """Return the formatted unicode text for the node's output, separate lines using lineSep""" return lineSep.join(node.formatText()) def getChildNodeOutput(self, node, lineSep='
\n'): """Return the formatted unicode text for the node children's output, separate lines using lineSep""" return lineSep.join(node.formatChildText()) def getFieldOutput(self, node, fieldName): """Return formatted text for the given fieldName data""" field = node.nodeFormat().findField(fieldName) if field: return node.nodeFormat().fieldText(field, node) return '' def getNodeFormatName(self, node): """Return the format type name for the given node""" return node.formatName def setNodeFormat(self, node, formatName): """Set the given node to the given node format type""" if formatName in globalref.docRef.treeFormats: node.formatName = formatName def setDataChangeCallback(self, callbackFunc): """Set callback function to be called every time a node's dictionary data is changed. The callbackFunc must take two arguments: the node being changed and a list of changed fields""" self.dataChangeCallbacks.append(callbackFunc) #************************************************************************* # Format Interfaces: #************************************************************************* def getNodeFormatNames(self): """Return text list of available node format names""" return globalref.docRef.treeFormats.nameList(True) def newNodeFormat(self, formatName, defaultFieldName='Name'): """Create a new node format, names must only contain characters [a-zA-Z0-9_.-]. If defaultFieldName, a text field is created and added to the title line and the first output line""" format = nodeformat.NodeFormat(formatName, {}, defaultFieldName) globalref.docRef.treeFormats[formatName] = format def copyFileFormat(self, fileRef, password=''): """Copy the configuration from another TreeLine file, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined), passord is optional - used to open an encrypted TreeLine file, returns True/False on success/failure""" try: globalref.docRef.treeFormats.configCopy(fileRef, password) return True except (treedoc.PasswordError, IOError, UnicodeError, treedoc.ReadFileError): return False def getFormatIconName(self, formatName): """Return the node format's currently set icon name, a default setting will return an empty string, blank will return 'NoIcon'""" try: return globalref.docRef.treeFormats[formatName].iconName except KeyError: return '' def setFormatIconName(self, formatName, iconName): """Set the node format's icon to iconName, an empty string or unknown icon name will get the default icon, use 'NoIcon' to get a blank""" try: globalref.docRef.treeFormats[formatName].iconName = iconName except KeyError: pass def addTreeIcon(self, name, image): """Add an icon to those available for use in the tree, icon data can be in any image format supported by Qt, if name matches one already loaded, the earlier one is replaced""" icon = QtGui.QIcon() pixmap = QtGui.QPixmap(image) if not pixmap.isNull(): icon.addPixmap(pixmap) globalref.treeIcons[name] = icon def getTitleLineFormat(self, formatName): """Return the node format's title formatting line""" try: return globalref.docRef.treeFormats[formatName].getLines()[0] except KeyError: return '' def setTitleLineFormat(self, formatName, newLine): """Set the node format's title formatting line to newLine""" try: globalref.docRef.treeFormats[formatName].insertLine(newLine, 0) except KeyError: pass def getOutputFormatLines(self, formatName): """Return a list of the node format's output formatting lines""" try: return globalref.docRef.treeFormats[formatName].getLines()[1:] except KeyError: return [] def setOutputFormatLines(self, formatName, lineList): """Set the node format's output formatting lines to lineList""" try: format = globalref.docRef.treeFormats[formatName] except KeyError: return format.lineList = format.lineList[:1] for line in lineList: format.addLine(line) def getFormatFieldNames(self, formatName): """Return a list of the node format's field names""" try: return globalref.docRef.treeFormats[formatName].fieldNames() except KeyError: return [] def addNewFormatField(self, formatName, fieldName, fieldType='Text'): """Add a new field to the node format, type should be one of: Text, Number, Choice, Combination, AutoChoice, Date, Time, Boolean, URL, Path, Email, InternalLink, ExecuteLink, Picture""" try: globalref.docRef.treeFormats[formatName].\ addNewField(fieldName, {'type': fieldType}) except KeyError: pass def getFormatFieldType(self, formatName, fieldName): """Return the type of the given field in the given format""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return '' if field: return field.typeName return '' def changeFormatFieldType(self, formatName, fieldName, newFieldType): """Change the type of the given field in the given format, type should be one of: Text, Number, Choice, Combination, AutoChoice, Date, Time, Boolean, URL, Path, Email, InternalLink, ExecuteLink, Picture""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return if field: field.changeType(newFieldType) def getFormatFieldFormat(self, formatName, fieldName): """Return the format code string of the given field""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return '' if field: return field.format return '' def setFormatFieldFormat(self, formatName, fieldName, newFieldFormat): """Change the format code string of the given field""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return if field: field.format = newFieldFormat field.initFormat() def getFormatFieldExtraText(self, formatName, fieldName): """Return a tuple of the prefix and suffix text of the given field""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return ('', '') if field: return (field.prefix, field.suffix) return ('', '') def setFormatFieldExtraText(self, formatName, fieldName, newPrefix='', newSuffix=''): """Set the format prefix and suffix text of the given field""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return if field: field.prefix = newPrefix field.suffix = newSuffix def getFormatFieldHtmlProp(self, formatName, fieldName): """Return True if the given field is set to use HTML, False for plain text""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return False if field: return field.html return False def setFormatFieldHtmlProp(self, formatName, fieldName, htmlProp=True): """Change the HTML handling of the given field""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return if field: field.html = htmlProp def getFormatFieldNumLines(self, formatName, fieldName): """Return the number of lines set for the given field""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return 0 if field: return field.numLines return 0 def setFormatFieldNumLines(self, formatName, fieldName, numLines): """Set the number of lines set for the given field""" try: field = globalref.docRef.treeFormats[formatName].\ findField(fieldName) except KeyError: return if field: field.numLines = numLines #************************************************************************* # View Interfaces: #************************************************************************* def updateViews(self): """Refresh the tree view and the current right-side views to reflect current data""" globalref.updateViewAll() def setViewUpdateCallback(self, callbackFunc): """Set callback function to be called after every TreeLine view update and control availability change (it is called often but is a good way to check for specific changes)""" self.viewUpdateCallbacks.append(callbackFunc) def getActiveEditView(self): """Return the currently active text editor in the Data Editor right-hand view. This does not include the combo boxes used for some fields. Returns None if something else has the focus.""" return self.mainWin.focusWidgetWithAttr('addHtmlTag') def insertEditViewText(self, text): """Inserts the given text into the currently active text editor in the Data Editor right-hand view (if one of the editors has the focus""" editor = self.getActiveEditView() if editor: editor.insertPlainText(text) #************************************************************************* # File Interfaces: #************************************************************************* def openFile(self, fileRef, importOnFail=True, addToRecent=True, newWinOk=True): """Open file given by fileRef interactively (QMessageBox on failure), fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined), if importOnFail and not a TreeLine file, will prompt for import type, if addToRecent, will add filename to recently used file list, if newWinOk will allow to create new window based on user setting""" if hasattr(fileRef, 'read'): fd, fileName = tempfile.mkstemp() os.write(fd, fileRef.read()) os.close(fd) fileRef.close() else: fileName = fileRef globalref.treeControl.openFile(fileName, newWinOk, importOnFail, addToRecent) if hasattr(fileRef, 'read'): os.remove(fileName) def newFile(self): """Start a new file""" self.mainWin.fileNew() def readFile(self, fileRef, password=''): """Open TreeLine file given by fileRef non-interactively, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined), returns True/False on success/failure""" if password: if hasattr(fileRef, 'read'): fileName = unicode(fileRef.name, sys.getfilesystemencoding()) else: fileName = fileRef globalref.docRef.setPassword(fileName, password) try: globalref.docRef.readFile(fileRef) return True except (treedoc.PasswordError, IOError, UnicodeError): return False def saveFile(self, fileRef): """Save TreeLine file to fileRef interactively (QMessageBox on failure)""" globalref.treeControl.saveFile(fileRef) def writeFile(self, fileRef, password=''): """Save TreeLine file to fileRef non-interactively, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined), returns True/False on success/failure""" if password: if hasattr(fileRef, 'read'): fileName = unicode(fileRef.name, sys.getfilesystemencoding()) else: fileName = fileRef globalref.docRef.setPassword(fileName, password) try: globalref.docRef.writeFile(fileRef) return True except IOError: return False def getFileName(self, caption, defaultExt='', filterList=None, currentFilter='', saveMode=True): """Return user specified file name for open, save as & export, starts from directory of current or recently used file, caption is the dialog title, defaultExt is added to base file name in saveMode, filterList is a list of filters with extensions (defaults to *.trl), currentFilter is the active filter in saveMode, saveMode is True for save and export, False for file open""" if not filterList: filterList = [self.mainWin.tlGenFileFilter] if saveMode: return self.mainWin.getSaveFileName(caption, defaultExt, filterList, currentFilter) else: return self.mainWin.getOpenFileName(caption, filterList) def getCurrentFileName(self): """Return the currently open filename""" return globalref.docRef.fileName def getDocModified(self): """Return True if the current document is marked as modified, False otherwise""" return globalref.docRef.modified def setDocModified(self, value): """A value of True sets the document status to modified, a value of False is unmodified""" globalref.docRef.modified = value globalref.updateViewMenuStat() def setFileNewCallback(self, callbackFunc): """Set callback function to be called after a new file is started""" self.fileNewCallbacks.append(callbackFunc) def setFileOpenCallback(self, callbackFunc): """Set callback function to be called after opening a file""" self.fileOpenCallbacks.append(callbackFunc) def setFileSaveCallback(self, callbackFunc): """Set callback function to be called after a file is saved""" self.fileSaveCallbacks.append(callbackFunc) def fileExport(self): """Export data to html,xml, etc. via dialog and return the fileName""" return self.mainWin.fileExport() def exportHtml(self, fileRef, includeRoot=True, openOnly=False, indent=20, addHeader=False): """Export current branch to single-column HTML, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined), remaining parameters are options, returns True on success, False on failure""" try: globalref.docRef.exportHtml(fileRef, self.getCurrentNode(), includeRoot, openOnly, indent, addHeader) return True except IOError: return False def exportDirTable(self, dirName, nodeList, addHeader=False): """Export tree to nested directory struct with html tables, dirName is initial export directory, nodeList is list of nodes to export (defaults to selection)""" if not nodeList: nodeList = self.getSelectedNodes() try: globalref.docRef.exportDirTable(dirName, nodeList, addHeader) return True except IOError: return False def exportDirPage(self, dirName, nodeList): """Export tree to nested directory struct with html page for each node, dirName is initial export directory, nodeList is list of nodes to export (defaults to selection)""" if not nodeList: nodeList = self.getSelectedNodes() try: globalref.docRef.exportDirPage(dirName, nodeList) return True except IOError: return False def exportXslt(self, fileRef, includeRoot=True, indent=20): """Export XSLT file for the current formatting, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined), returns True on success, False on failure""" try: globalref.docRef.exportXslt(fileRef, includeRoot, indent) self.updateViews() return True except IOError: return False def exportTrlSubtree(self, fileRef, nodeList, addBranches=True): """Export current branch as a TreeLine subtree, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined), nodeList is list of nodes to export (defaults to selection), returns True on success, False on failure""" if not nodeList: nodeList = self.getSelectedNodes() try: globalref.docRef.exportTrlSubtree(fileRef, nodeList, addBranches) return True except IOError: return False def exportTable(self, fileRef, nodeList, addBranches=True): """Export current item's children as a table of data, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined) nodeList is list of nodes to export (defaults to selection), returns True on success, False on failure""" if not nodeList: nodeList = self.getSelectedNodes() try: globalref.docRef.exportTable(fileRef, nodeList, addBranches) return True except IOError: return False def exportTabbedTitles(self, fileRef, nodeList, addBranches=True, includeRoot=True, openOnly=False): """Export current branch to tabbed text titles, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined) nodeList is list of nodes to export (defaults to selection), returns True on success, False on failure""" if not nodeList: nodeList = self.getSelectedNodes() try: globalref.docRef.exportTabbedTitles(fileRef, nodeList, addBranches, includeRoot, openOnly) return True except IOError: return False def exportXbelBookmarks(self, fileRef, nodeList, addBranches=True): """Export current branch to XBEL format bookmarks, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined) nodeList is list of nodes to export (defaults to selection), returns True on success, False on failure""" if not nodeList: nodeList = self.getSelectedNodes() try: globalref.docRef.exportXbel(fileRef, nodeList, addBranches) return True except IOError: return False def exportHtmlBookmarks(self, fileRef, nodeList, addBranches=True): """Export current branch to HTML format bookmarks, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined) nodeList is list of nodes to export (defaults to selection), returns True on success, False on failure""" if not nodeList: nodeList = self.getSelectedNodes() try: globalref.docRef.exportHtmlBookmarks(fileRef, nodeList, addBranches) return True except IOError: return False def exportGenericXml(self, fileRef, nodeList, addBranches=True): """Export current branch to generic XML (non-TreeLine) file, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined) nodeList is list of nodes to export (defaults to selection), returns True on success, False on failure""" if not nodeList: nodeList = self.getSelectedNodes() try: globalref.docRef.exportGenericXml(fileRef, nodeList, addBranches) return True except IOError: return False def exportOdf(self, fileRef, nodeList, addBranches=True, includeRoot=True, openOnly=False): """Export an ODF format text file, fileRef is either a file path string or a file-like object (if it is a file-like object, fileRef.name must be defined) nodeList is list of nodes to export (defaults to selection), returns True on success, False on failure""" if not nodeList: nodeList = self.getSelectedNodes() fontInfo = QtGui.QFontInfo(self.mainWin.dataOutSplit.widget(0).font()) try: globalref.docRef.exportOdf(fileRef, nodeList, fontInfo.family(), fontInfo.pointSize(), fontInfo.fixedPitch(), addBranches, includeRoot, openOnly) return True except IOError: return False #************************************************************************* # Menu Interfaces: #************************************************************************* def getMenuBar(self): """Return the main window's top menu bar (QMenuBar)""" return self.mainWin.menuBar() def getPulldownMenu(self, index): """Return top pulldown menu at position index (QMenu), return None if index is not valid""" try: return self.mainWin.pulldownMenuList[index] except IndexError: return None def addMenuAction(self, name, action, initKey=''): """Adds a QAction to the shortcut key editor (menu section) and to the toolbar editor (if it has an icon). This does not add it to a menu (use menu.insertAction(...) on a menu from functions above). Give a name without punctuation or spaces (spaces are added in the editor based on CamelCase splits); initKey is optional control key string""" optiondefaults.menuKeyBindList.append((name, initKey)) optiondefaults.cmdTranslationDict[name] = name globalref.options.addDefaultKey(name, initKey) self.mainWin.actions[name] = action icon = action.icon() if icon and not icon.isNull(): self.mainWin.toolIcons[name.lower()] = icon self.mainWin.setupShortcuts() self.mainWin.setupToolbars() def addShortcutKey(self, name, function, initKey=''): """Adds a non-menu keyboard shortcut that the user can set in the Shortcut Editor. Give a name without punctuation or spaces (spaces are added in the editor based on CamelCase splits), function is any Python function, initKey is optional control key string""" optiondefaults.otherKeyBindList.append((name, initKey)) optiondefaults.cmdTranslationDict[name] = name globalref.options.addDefaultKey(name, initKey) shortcut = QtGui.QShortcut(QtGui.QKeySequence(), self.mainWin, function) self.mainWin.shortcuts[name] = shortcut self.mainWin.setupShortcuts() #************************************************************************* # Internal methods (not for plugin use): #************************************************************************* def execCallback(self, funcList, *args): """Call functions in funcList with given args if any""" for func in funcList: func(*args) TreeLine/templates/0000755000175000017500000000000011651514477013254 5ustar dougdougTreeLine/templates/230en_ToDo_List.trl0000644000175000017500000000250011651514477016543 0ustar dougdoug Conditional Task List Home Tasks Mow lawn false false Work Tasks Write documents false false TreeLine/templates/110en_Long_Plain_Text.trl0000644000175000017500000000046211651514477017733 0ustar dougdoug Parent Parent text Child Child text TreeLine/templates/220en_Book_List.trl0000644000175000017500000000212211651514477016567 0ustar dougdoug SF Books Orson Scott Card Ender's Game 1985 2007/04/30 Young boy is taught fighting and leadership Isaac Asimov Foundation 1951 Psychohistory predicts the fall of empire TreeLine/templates/120en_Long_HTML_Text.trl0000644000175000017500000000050411651514477017432 0ustar dougdoug Parent Parent text Child Child text TreeLine/templates/210en_Contact_List.trl0000644000175000017500000000245511651514477017300 0ustar dougdoug Main Friends John Doe 1492 Columbus Lane Atlantis CA 98765 (123) 555-4567 (123) 555-9876 john.doe@nowhere.com Family Jane Roe 1812 War Lane Britania NM 87560 (123) 490-4909 1933/03/13